Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5347a33
Added ACP support...
prem315 Sep 24, 2026
cf39091
fix(acp): replay conversation history on session/load
prem315 Sep 25, 2026
7c5aec1
chore: merge dev into feature/acp-support
claude Sep 26, 2026
9e6fc40
docs: add ACP integration review report
claude Sep 26, 2026
6972716
refactor: map Effect errors to ACP request errors and clean up module…
prem315 Sep 26, 2026
fce0eb8
Merge branch 'dev' into feature/acp-support
sanchitrk Sep 29, 2026
1988fe7
feat(harness): expose tool call input and plugin load failures as events
sanchitrk Sep 29, 2026
0546993
feat(acp): serve ACP v1 on a vendored Effect-native protocol
sanchitrk Sep 29, 2026
7c7c66c
feat(harness): title sessions from their first prompt
sanchitrk Sep 29, 2026
565a158
feat(acp): announce session titles to the editor
sanchitrk Sep 29, 2026
822db8f
feat(aikit): export getEnvApiKey
sanchitrk Sep 29, 2026
df6f84f
feat(harness): list models by credential with ModelCatalog.available
sanchitrk Sep 29, 2026
2d11b1a
fix(acp): offer models from the harness credential file
sanchitrk Sep 29, 2026
ac82cc6
feat(settings): read custom catalog entries from settings.models
sanchitrk Sep 29, 2026
d986339
feat(acp): offer settings-defined models
sanchitrk Sep 29, 2026
581ac92
feat(harness): persist session model and thinking level
sanchitrk Sep 29, 2026
296a0a0
refactor(acp): read session configuration from the harness
sanchitrk Sep 29, 2026
a5cd11d
fix(aikit): replace images with placeholders for non-vision models
sanchitrk Sep 29, 2026
94e99ce
docs: add @codeworksh/aikit@0.9.3 release notes
sanchitrk Sep 29, 2026
a5fdf9d
release: @codeworksh/aikit@0.9.3
sanchitrk Sep 29, 2026
4006c2a
rm
sanchitrk Sep 29, 2026
9b9a34c
chore
sanchitrk Sep 30, 2026
2607ca6
feat(harness): accept image parts in prompts
sanchitrk Sep 30, 2026
eadbc08
feat(acp): accept image prompts
sanchitrk Sep 30, 2026
2b4deec
test(acp): run live cases against six providers
sanchitrk Sep 30, 2026
c6234e9
test(harness): run live cases against six providers
sanchitrk Sep 30, 2026
2b9d1a3
test(aikit): run OpenRouter suites on three models and Codex from the…
sanchitrk Sep 30, 2026
a661cf4
chore: update AGENTS.md and CLAUDE.md
sanchitrk Sep 30, 2026
2db10a3
refactor(acp): serve over the official ACP SDK
sanchitrk Sep 30, 2026
6d3c55f
test(cli): load .env.local for the ACP live suites
sanchitrk Sep 30, 2026
c1aa54d
test(cli): move the test env loader to test/utils like aikit
sanchitrk Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

## Commands

- Don't run dev server commands (e.g `pnpm run dev`) - assume it's already running.
- Don't run dev server commands (e.g `pnpm run dev`, `bun run dev`) - assume it's already running.
- Don't run build commands unless specifically told to.
- Focus on checking commands like `pnpm run typecheck`, `pnpm run check`, `bun run lint`, etc
- Focus on checking commands like `pnpm run check`, `bun run lint` `bun run typecheck`, etc.

## Package Managers

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ This file is the canonical source for unreleased changes and published release n

## [Unreleased]

## [@codeworksh/aikit@0.9.3]

### Added

- Added the `getEnvApiKey` export, so a caller can check whether a provider has an API key in the environment using the same lookup aikit applies when it opens that provider.

### Fixed

- Fixed images reaching models that cannot read them. For a model whose `input` does not include `"image"`, each image in a user message is now replaced with `(image omitted: model does not support images)` and each image in a tool result with `(tool image omitted: model does not support images)`; consecutive images collapse to one placeholder. The message and tool result are kept, so the model still knows an image was there. Previously a user image was silently dropped, which removed an image-only message entirely, and a tool-result image was sent anyway, which providers often reject. Vision models are unchanged.

## [@codeworksh/aikit@0.9.2]

### Added
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

## Commands

- Don't run dev server commands (e.g `bun run dev`, `pnpm run dev`) - assume it's already running.
- Don't run dev server commands (e.g `pnpm run dev`, `bun run dev`) - assume it's already running.
- Don't run build commands unless specifically told to.
- Focus on checking commands like `bun run typecheck`, `pnpm run check`, `bun run lint`, etc.
- Focus on checking commands like `pnpm run check`, `bun run lint` `bun run typecheck`, etc.

## Package Managers

Expand All @@ -30,7 +30,7 @@ When uncertain, prefer: Effect, Tailwind, TypeScript, React, Clerk, TanStack, Ve
## General Preferences

- If asked to do too much work at once, stop and state that clearly.
- If computer use is helpful for completing or verifying work, shell out to gpt-5.6 with Codex for it
- After you yourself have verified the work, get the work peer reviewed by another agent: `codex` for `code reviews`, `focused deep checks`, `tweaks`, `tests review` shell out to `gpt-6.1-sol` with thinking `low` or `medium` depending on complexity.
- Avoid defensive future-facing design. This leads to pre-mature assumptions which are never true.

## Maintainability
Expand Down
2 changes: 1 addition & 1 deletion packages/aikit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codeworksh/aikit",
"version": "0.9.2",
"version": "0.9.3",
"description": "TypeScript SDK that provides a unified API for working with multiple LLM providers, automatic model discovery, and more.",
"keywords": [
"agent",
Expand Down
1 change: 1 addition & 0 deletions packages/aikit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * as Event from "./event/event.ts";
export { llm } from "./llm.ts";
export * as Failure from "./llm/failure.ts";
export * as Protocol from "./llm/protocol.ts";
export { getEnvApiKey } from "./llm/runtime.ts";
export { ThinkingBudgets } from "./llm/shared.ts";
export * as Message from "./message/message.ts";
export * as Model from "./model/model.ts";
Expand Down
4 changes: 3 additions & 1 deletion packages/aikit/src/llm/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
type ApiKeyProvider = {
/** Where a provider's API key may come from: its catalog `key` and `env` variable names. */
export type ApiKeyProvider = {
env?: string[];
key?: string;
};
Expand All @@ -15,6 +16,7 @@ export function mergeHeaders(
return merged;
}

/** The provider's API key from the environment, checking its `key` then each `env` name. */
export function getEnvApiKey(provider: ApiKeyProvider): string | undefined {
if (typeof process === "undefined") return;

Expand Down
8 changes: 3 additions & 5 deletions packages/aikit/src/llm/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function createAssistantMessage(model: Model.Info): Message.AssistantMess
});
}

function userContent(parts: Message.UserMessage["parts"], supportsImages: boolean): ModelMessage[] {
function userContent(parts: Message.UserMessage["parts"]): ModelMessage[] {
const content: Exclude<UserModelMessage["content"], string> = [];
for (const part of parts) {
if (part.type === "text") {
Expand All @@ -53,9 +53,7 @@ function userContent(parts: Message.UserMessage["parts"], supportsImages: boolea
}
continue;
}
if (supportsImages) {
content.push({ type: "file", data: part.data, mediaType: part.mimeType });
}
content.push({ type: "file", data: part.data, mediaType: part.mimeType });
}

if (content.length === 0) return [];
Expand Down Expand Up @@ -331,7 +329,7 @@ export function convertMessages(context: Message.Context, model: Model.Info): Mo

for (const msg of transformedMessages) {
if (msg.role === "user") {
messages.push(...userContent(msg.parts, model.input.includes("image")));
messages.push(...userContent(msg.parts));
continue;
}
messages.push(...assistantMessages(msg, model));
Expand Down
31 changes: 26 additions & 5 deletions packages/aikit/src/message/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,17 +329,39 @@ function syntheticSkippedToolCall(toolCall: ToolCallPendingPart | ToolCallRunnin
};
}

const USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
const TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";

// Consecutive images collapse into a single placeholder.
function omitImages(content: (TextContent | ImageContent)[], placeholder: string): TextContent[] {
const result: TextContent[] = [];
for (const [index, block] of content.entries()) {
if (block.type === "text") result.push(block);
else if (content[index - 1]?.type !== "image") result.push({ type: "text", text: placeholder });
}
return result;
}

function omitToolImages(toolCall: ToolCall): ToolCall {
if (!("result" in toolCall) || !toolCall.result.content.some((content) => content.type === "image")) return toolCall;
return {
...toolCall,
result: { ...toolCall.result, content: omitImages(toolCall.result.content, TOOL_IMAGE_PLACEHOLDER) },
} as ToolCall;
}

export function transformMessages<TProtocol extends Model.KnownProviderEnum>(
messages: Message[],
model: Model.TModel<TProtocol>,
normalizeToolCallId?: (id: string, model: Model.TModel<TProtocol>, source: AssistantMessage) => string,
): Message[] {
const toolCallIDMap = new Map<string, string>();
const supportsImages = model.input.includes("image");

const transformed: Message[] = [];
for (const msg of messages) {
if (msg.role === "user") {
transformed.push(msg);
transformed.push(supportsImages ? msg : { ...msg, parts: omitImages(msg.parts, USER_IMAGE_PLACEHOLDER) });
continue;
}

Expand Down Expand Up @@ -394,10 +416,9 @@ export function transformMessages<TProtocol extends Model.KnownProviderEnum>(
return block;
});
const parts = transformedParts.map((block) => {
if (block.type === "toolCall" && isUnresolvedToolCall(block)) {
return syntheticSkippedToolCall(block);
}
return block;
if (block.type !== "toolCall") return block;
const toolCall = isUnresolvedToolCall(block) ? syntheticSkippedToolCall(block) : block;
return supportsImages ? toolCall : omitToolImages(toolCall);
});

transformed.push({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"text": "Here are two photos."
},
{
"type": "text",
"text": "(image omitted: model does not support images)"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool-call",
"toolCallId": "call_screenshot",
"toolName": "screenshot",
"input": {}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool-result",
"toolCallId": "call_screenshot",
"toolName": "screenshot",
"output": {
"type": "text",
"value": "rendered\n(tool image omitted: model does not support images)"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "Quote verbatim every parenthesised note in this conversation, one per line, and nothing else."
}
]
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"text": "Here are two photos."
},
{
"type": "text",
"text": "(image omitted: model does not support images)"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool-call",
"toolCallId": "call_screenshot",
"toolName": "screenshot",
"input": {}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool-result",
"toolCallId": "call_screenshot",
"toolName": "screenshot",
"output": {
"type": "text",
"value": "rendered\n(tool image omitted: model does not support images)"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "Quote verbatim every parenthesised note in this conversation, one per line, and nothing else."
}
]
}
]
49 changes: 49 additions & 0 deletions packages/aikit/test/e2e/__artifacts__/text-only-images.openai.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"text": "Here are two photos."
},
{
"type": "text",
"text": "(image omitted: model does not support images)"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool-call",
"toolCallId": "call_screenshot",
"toolName": "screenshot",
"input": {}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool-result",
"toolCallId": "call_screenshot",
"toolName": "screenshot",
"output": {
"type": "text",
"value": "rendered\n(tool image omitted: model does not support images)"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "Quote verbatim every parenthesised note in this conversation, one per line, and nothing else."
}
]
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"text": "Here are two photos."
},
{
"type": "text",
"text": "(image omitted: model does not support images)"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool-call",
"toolCallId": "call_screenshot",
"toolName": "screenshot",
"input": {}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool-result",
"toolCallId": "call_screenshot",
"toolName": "screenshot",
"output": {
"type": "text",
"value": "rendered\n(tool image omitted: model does not support images)"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "Quote verbatim every parenthesised note in this conversation, one per line, and nothing else."
}
]
}
]
Loading