Skip to content

Commit 1f8fbd0

Browse files
authored
Merge pull request #38 from codeworksh/feature/acp-support
Added ACP support...
2 parents 9a08cc2 + c1aa54d commit 1f8fbd0

83 files changed

Lines changed: 3171 additions & 311 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
## Commands
99

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

1414
## Package Managers
1515

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ This file is the canonical source for unreleased changes and published release n
1111

1212
## [Unreleased]
1313

14+
## [@codeworksh/aikit@0.9.3]
15+
16+
### Added
17+
18+
- 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.
19+
20+
### Fixed
21+
22+
- 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.
23+
1424
## [@codeworksh/aikit@0.9.2]
1525

1626
### Added

‎CLAUDE.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
## Commands
99

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

1414
## Package Managers
1515

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

3232
- If asked to do too much work at once, stop and state that clearly.
33-
- If computer use is helpful for completing or verifying work, shell out to gpt-5.6 with Codex for it
33+
- 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.
3434
- Avoid defensive future-facing design. This leads to pre-mature assumptions which are never true.
3535

3636
## Maintainability

‎packages/aikit/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@codeworksh/aikit",
3-
"version": "0.9.2",
3+
"version": "0.9.3",
44
"description": "TypeScript SDK that provides a unified API for working with multiple LLM providers, automatic model discovery, and more.",
55
"keywords": [
66
"agent",

‎packages/aikit/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * as Event from "./event/event.ts";
22
export { llm } from "./llm.ts";
33
export * as Failure from "./llm/failure.ts";
44
export * as Protocol from "./llm/protocol.ts";
5+
export { getEnvApiKey } from "./llm/runtime.ts";
56
export { ThinkingBudgets } from "./llm/shared.ts";
67
export * as Message from "./message/message.ts";
78
export * as Model from "./model/model.ts";

‎packages/aikit/src/llm/runtime.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
type ApiKeyProvider = {
1+
/** Where a provider's API key may come from: its catalog `key` and `env` variable names. */
2+
export type ApiKeyProvider = {
23
env?: string[];
34
key?: string;
45
};
@@ -15,6 +16,7 @@ export function mergeHeaders(
1516
return merged;
1617
}
1718

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

‎packages/aikit/src/llm/transform.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function createAssistantMessage(model: Model.Info): Message.AssistantMess
4343
});
4444
}
4545

46-
function userContent(parts: Message.UserMessage["parts"], supportsImages: boolean): ModelMessage[] {
46+
function userContent(parts: Message.UserMessage["parts"]): ModelMessage[] {
4747
const content: Exclude<UserModelMessage["content"], string> = [];
4848
for (const part of parts) {
4949
if (part.type === "text") {
@@ -53,9 +53,7 @@ function userContent(parts: Message.UserMessage["parts"], supportsImages: boolea
5353
}
5454
continue;
5555
}
56-
if (supportsImages) {
57-
content.push({ type: "file", data: part.data, mediaType: part.mimeType });
58-
}
56+
content.push({ type: "file", data: part.data, mediaType: part.mimeType });
5957
}
6058

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

332330
for (const msg of transformedMessages) {
333331
if (msg.role === "user") {
334-
messages.push(...userContent(msg.parts, model.input.includes("image")));
332+
messages.push(...userContent(msg.parts));
335333
continue;
336334
}
337335
messages.push(...assistantMessages(msg, model));

‎packages/aikit/src/message/message.ts‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,17 +329,39 @@ function syntheticSkippedToolCall(toolCall: ToolCallPendingPart | ToolCallRunnin
329329
};
330330
}
331331

332+
const USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
333+
const TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
334+
335+
// Consecutive images collapse into a single placeholder.
336+
function omitImages(content: (TextContent | ImageContent)[], placeholder: string): TextContent[] {
337+
const result: TextContent[] = [];
338+
for (const [index, block] of content.entries()) {
339+
if (block.type === "text") result.push(block);
340+
else if (content[index - 1]?.type !== "image") result.push({ type: "text", text: placeholder });
341+
}
342+
return result;
343+
}
344+
345+
function omitToolImages(toolCall: ToolCall): ToolCall {
346+
if (!("result" in toolCall) || !toolCall.result.content.some((content) => content.type === "image")) return toolCall;
347+
return {
348+
...toolCall,
349+
result: { ...toolCall.result, content: omitImages(toolCall.result.content, TOOL_IMAGE_PLACEHOLDER) },
350+
} as ToolCall;
351+
}
352+
332353
export function transformMessages<TProtocol extends Model.KnownProviderEnum>(
333354
messages: Message[],
334355
model: Model.TModel<TProtocol>,
335356
normalizeToolCallId?: (id: string, model: Model.TModel<TProtocol>, source: AssistantMessage) => string,
336357
): Message[] {
337358
const toolCallIDMap = new Map<string, string>();
359+
const supportsImages = model.input.includes("image");
338360

339361
const transformed: Message[] = [];
340362
for (const msg of messages) {
341363
if (msg.role === "user") {
342-
transformed.push(msg);
364+
transformed.push(supportsImages ? msg : { ...msg, parts: omitImages(msg.parts, USER_IMAGE_PLACEHOLDER) });
343365
continue;
344366
}
345367

@@ -394,10 +416,9 @@ export function transformMessages<TProtocol extends Model.KnownProviderEnum>(
394416
return block;
395417
});
396418
const parts = transformedParts.map((block) => {
397-
if (block.type === "toolCall" && isUnresolvedToolCall(block)) {
398-
return syntheticSkippedToolCall(block);
399-
}
400-
return block;
419+
if (block.type !== "toolCall") return block;
420+
const toolCall = isUnresolvedToolCall(block) ? syntheticSkippedToolCall(block) : block;
421+
return supportsImages ? toolCall : omitToolImages(toolCall);
401422
});
402423

403424
transformed.push({
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
[
2+
{
3+
"role": "user",
4+
"content": [
5+
{
6+
"type": "text",
7+
"text": "Here are two photos."
8+
},
9+
{
10+
"type": "text",
11+
"text": "(image omitted: model does not support images)"
12+
}
13+
]
14+
},
15+
{
16+
"role": "assistant",
17+
"content": [
18+
{
19+
"type": "tool-call",
20+
"toolCallId": "call_screenshot",
21+
"toolName": "screenshot",
22+
"input": {}
23+
}
24+
]
25+
},
26+
{
27+
"role": "tool",
28+
"content": [
29+
{
30+
"type": "tool-result",
31+
"toolCallId": "call_screenshot",
32+
"toolName": "screenshot",
33+
"output": {
34+
"type": "text",
35+
"value": "rendered\n(tool image omitted: model does not support images)"
36+
}
37+
}
38+
]
39+
},
40+
{
41+
"role": "user",
42+
"content": [
43+
{
44+
"type": "text",
45+
"text": "Quote verbatim every parenthesised note in this conversation, one per line, and nothing else."
46+
}
47+
]
48+
}
49+
]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
[
2+
{
3+
"role": "user",
4+
"content": [
5+
{
6+
"type": "text",
7+
"text": "Here are two photos."
8+
},
9+
{
10+
"type": "text",
11+
"text": "(image omitted: model does not support images)"
12+
}
13+
]
14+
},
15+
{
16+
"role": "assistant",
17+
"content": [
18+
{
19+
"type": "tool-call",
20+
"toolCallId": "call_screenshot",
21+
"toolName": "screenshot",
22+
"input": {}
23+
}
24+
]
25+
},
26+
{
27+
"role": "tool",
28+
"content": [
29+
{
30+
"type": "tool-result",
31+
"toolCallId": "call_screenshot",
32+
"toolName": "screenshot",
33+
"output": {
34+
"type": "text",
35+
"value": "rendered\n(tool image omitted: model does not support images)"
36+
}
37+
}
38+
]
39+
},
40+
{
41+
"role": "user",
42+
"content": [
43+
{
44+
"type": "text",
45+
"text": "Quote verbatim every parenthesised note in this conversation, one per line, and nothing else."
46+
}
47+
]
48+
}
49+
]

0 commit comments

Comments
 (0)