@windowkit/agent-chat 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +147 -2
- package/dist/core/driver.d.ts +183 -0
- package/dist/core/driver.d.ts.map +1 -0
- package/dist/core/driver.js +12 -0
- package/dist/core/driver.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +11 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/markdown.d.ts +7 -0
- package/dist/core/markdown.d.ts.map +1 -0
- package/dist/core/markdown.js +53 -0
- package/dist/core/markdown.js.map +1 -0
- package/dist/core/policy.d.ts +18 -0
- package/dist/core/policy.d.ts.map +1 -0
- package/dist/core/policy.js +67 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/reducer.d.ts +61 -0
- package/dist/core/reducer.d.ts.map +1 -0
- package/dist/core/reducer.js +391 -0
- package/dist/core/reducer.js.map +1 -0
- package/dist/core/session.d.ts +105 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +648 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/tools.d.ts +120 -0
- package/dist/core/tools.d.ts.map +1 -0
- package/dist/core/tools.js +120 -0
- package/dist/core/tools.js.map +1 -0
- package/dist/core/types.d.ts +295 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +21 -0
- package/dist/core/types.js.map +1 -0
- package/dist/drivers/claude/index.d.ts +20 -0
- package/dist/drivers/claude/index.d.ts.map +1 -0
- package/dist/drivers/claude/index.js +727 -0
- package/dist/drivers/claude/index.js.map +1 -0
- package/dist/drivers/claude/input.d.ts +7 -0
- package/dist/drivers/claude/input.d.ts.map +1 -0
- package/dist/drivers/claude/input.js +46 -0
- package/dist/drivers/claude/input.js.map +1 -0
- package/dist/drivers/claude/peers.d.ts +16 -0
- package/dist/drivers/claude/peers.d.ts.map +1 -0
- package/dist/drivers/claude/peers.js +137 -0
- package/dist/drivers/claude/peers.js.map +1 -0
- package/dist/drivers/claude/schema.d.ts +5 -0
- package/dist/drivers/claude/schema.d.ts.map +1 -0
- package/dist/drivers/claude/schema.js +77 -0
- package/dist/drivers/claude/schema.js.map +1 -0
- package/dist/drivers/claude/sdk.d.ts +86 -0
- package/dist/drivers/claude/sdk.d.ts.map +1 -0
- package/dist/drivers/claude/sdk.js +47 -0
- package/dist/drivers/claude/sdk.js.map +1 -0
- package/dist/drivers/claude/tools.d.ts +18 -0
- package/dist/drivers/claude/tools.d.ts.map +1 -0
- package/dist/drivers/claude/tools.js +119 -0
- package/dist/drivers/claude/tools.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/fake.d.ts +63 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +270 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/view/AgentChat.d.ts +53 -0
- package/dist/view/AgentChat.d.ts.map +1 -0
- package/dist/view/AgentChat.js +87 -0
- package/dist/view/AgentChat.js.map +1 -0
- package/dist/view/CodeBlock.d.ts +9 -0
- package/dist/view/CodeBlock.d.ts.map +1 -0
- package/dist/view/CodeBlock.js +60 -0
- package/dist/view/CodeBlock.js.map +1 -0
- package/dist/view/Composer.d.ts +24 -0
- package/dist/view/Composer.d.ts.map +1 -0
- package/dist/view/Composer.js +144 -0
- package/dist/view/Composer.js.map +1 -0
- package/dist/view/Header.d.ts +3 -0
- package/dist/view/Header.d.ts.map +1 -0
- package/dist/view/Header.js +30 -0
- package/dist/view/Header.js.map +1 -0
- package/dist/view/MessageList.d.ts +9 -0
- package/dist/view/MessageList.d.ts.map +1 -0
- package/dist/view/MessageList.js +111 -0
- package/dist/view/MessageList.js.map +1 -0
- package/dist/view/QuestionCard.d.ts +6 -0
- package/dist/view/QuestionCard.d.ts.map +1 -0
- package/dist/view/QuestionCard.js +39 -0
- package/dist/view/QuestionCard.js.map +1 -0
- package/dist/view/TextBlock.d.ts +9 -0
- package/dist/view/TextBlock.d.ts.map +1 -0
- package/dist/view/TextBlock.js +22 -0
- package/dist/view/TextBlock.js.map +1 -0
- package/dist/view/ToolCard.d.ts +7 -0
- package/dist/view/ToolCard.d.ts.map +1 -0
- package/dist/view/ToolCard.js +155 -0
- package/dist/view/ToolCard.js.map +1 -0
- package/dist/view/Transcript.d.ts +5 -0
- package/dist/view/Transcript.d.ts.map +1 -0
- package/dist/view/Transcript.js +47 -0
- package/dist/view/Transcript.js.map +1 -0
- package/dist/view/context.d.ts +21 -0
- package/dist/view/context.d.ts.map +1 -0
- package/dist/view/context.js +37 -0
- package/dist/view/context.js.map +1 -0
- package/dist/view/fences.d.ts +13 -0
- package/dist/view/fences.d.ts.map +1 -0
- package/dist/view/fences.js +59 -0
- package/dist/view/fences.js.map +1 -0
- package/dist/view/files.d.ts +11 -0
- package/dist/view/files.d.ts.map +1 -0
- package/dist/view/files.js +109 -0
- package/dist/view/files.js.map +1 -0
- package/dist/view/look.d.ts +18 -0
- package/dist/view/look.d.ts.map +1 -0
- package/dist/view/look.js +37 -0
- package/dist/view/look.js.map +1 -0
- package/dist/view/types.d.ts +50 -0
- package/dist/view/types.d.ts.map +1 -0
- package/dist/view/types.js +14 -0
- package/dist/view/types.js.map +1 -0
- package/dist/view/use-agent.d.ts +11 -0
- package/dist/view/use-agent.d.ts.map +1 -0
- package/dist/view/use-agent.js +33 -0
- package/dist/view/use-agent.js.map +1 -0
- package/package.json +92 -4
- package/src/core/driver.ts +191 -0
- package/src/core/index.ts +54 -0
- package/src/core/markdown.ts +56 -0
- package/src/core/policy.ts +77 -0
- package/src/core/reducer.ts +490 -0
- package/src/core/session.ts +824 -0
- package/src/core/tools.ts +233 -0
- package/src/core/types.ts +321 -0
- package/src/drivers/claude/index.ts +825 -0
- package/src/drivers/claude/input.ts +47 -0
- package/src/drivers/claude/peers.ts +153 -0
- package/src/drivers/claude/schema.ts +99 -0
- package/src/drivers/claude/sdk.ts +138 -0
- package/src/drivers/claude/tools.ts +141 -0
- package/src/index.ts +51 -0
- package/src/testing/fake.ts +374 -0
- package/src/testing/index.ts +11 -0
- package/src/view/AgentChat.tsx +204 -0
- package/src/view/CodeBlock.tsx +108 -0
- package/src/view/Composer.tsx +230 -0
- package/src/view/Header.tsx +73 -0
- package/src/view/MessageList.tsx +290 -0
- package/src/view/QuestionCard.tsx +107 -0
- package/src/view/TextBlock.tsx +58 -0
- package/src/view/ToolCard.tsx +283 -0
- package/src/view/Transcript.tsx +81 -0
- package/src/view/context.ts +70 -0
- package/src/view/dom-globals.d.ts +10 -0
- package/src/view/fences.ts +73 -0
- package/src/view/files.ts +111 -0
- package/src/view/look.ts +53 -0
- package/src/view/types.ts +71 -0
- package/src/view/use-agent.ts +40 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// An async iterable fed by `push`: the SDK's streaming input, which is read
|
|
2
|
+
// for the life of the session and ends when the session closes.
|
|
3
|
+
|
|
4
|
+
export class InputQueue<T> implements AsyncIterable<T> {
|
|
5
|
+
#items: T[] = [];
|
|
6
|
+
#waiting: ((r: IteratorResult<T>) => void) | null = null;
|
|
7
|
+
#ended = false;
|
|
8
|
+
|
|
9
|
+
push(item: T): void {
|
|
10
|
+
if (this.#ended) throw new Error('The session is closed.');
|
|
11
|
+
if (this.#waiting) {
|
|
12
|
+
const resolve = this.#waiting;
|
|
13
|
+
this.#waiting = null;
|
|
14
|
+
resolve({ value: item, done: false });
|
|
15
|
+
} else {
|
|
16
|
+
this.#items.push(item);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
end(): void {
|
|
21
|
+
this.#ended = true;
|
|
22
|
+
if (this.#waiting) {
|
|
23
|
+
const resolve = this.#waiting;
|
|
24
|
+
this.#waiting = null;
|
|
25
|
+
resolve({ value: undefined, done: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
30
|
+
return {
|
|
31
|
+
next: () => {
|
|
32
|
+
const item = this.#items.shift();
|
|
33
|
+
if (item !== undefined)
|
|
34
|
+
return Promise.resolve({ value: item, done: false });
|
|
35
|
+
if (this.#ended)
|
|
36
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
this.#waiting = resolve;
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
return: () => {
|
|
42
|
+
this.end();
|
|
43
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Messages that are not the user's, in Claude Code's own terms.
|
|
2
|
+
//
|
|
3
|
+
// Claude Code tells who wrote a user turn by its `origin`. A host that types
|
|
4
|
+
// for the user says `{ kind: 'human' }`: a turn with no origin is
|
|
5
|
+
// unattributed, and fails closed where Claude Code asks whether a human
|
|
6
|
+
// said it. A message from another session is `{ kind: 'peer', from }`, its
|
|
7
|
+
// text wrapped in the envelope Claude Code itself writes for one — so the
|
|
8
|
+
// model reads it as reported speech from that session, which is how Claude
|
|
9
|
+
// Desktop hands a session its peers' messages. And what Claude Code takes
|
|
10
|
+
// from another session or an MCP channel comes back out with its origin, the
|
|
11
|
+
// envelope still on its text.
|
|
12
|
+
//
|
|
13
|
+
// The envelope's shape is Claude Code 2.1's, read from what it accepts: the
|
|
14
|
+
// attributes in this order, each held to the characters it allows, and the
|
|
15
|
+
// body on lines of its own.
|
|
16
|
+
|
|
17
|
+
import type { MessageSource, PeerSource } from '../../core/types.js';
|
|
18
|
+
import type { SdkMessage } from './sdk.js';
|
|
19
|
+
|
|
20
|
+
const PEER_TAG = 'cross-session-message';
|
|
21
|
+
const CHANNEL_TAG = 'channel';
|
|
22
|
+
|
|
23
|
+
/** `from` keeps to these characters; anything else is percent-encoded, as
|
|
24
|
+
* Claude Code encodes the ids it makes. */
|
|
25
|
+
const ID_UNSAFE = /[^A-Za-z0-9:_/.\\-]/gu;
|
|
26
|
+
const ID_MAX = 300;
|
|
27
|
+
const SESSION_ID = /^[A-Za-z0-9_-]{1,80}$/;
|
|
28
|
+
const NAME_MAX = 64;
|
|
29
|
+
|
|
30
|
+
/** The origin a turn is sent with: the user's, or a peer's. */
|
|
31
|
+
export function originOf(
|
|
32
|
+
from: PeerSource | undefined,
|
|
33
|
+
): Record<string, unknown> {
|
|
34
|
+
if (!from) return { kind: 'human' };
|
|
35
|
+
const name = nameOf(from.name);
|
|
36
|
+
return {
|
|
37
|
+
kind: 'peer',
|
|
38
|
+
from: idOf(from.id),
|
|
39
|
+
...(name ? { name } : {}),
|
|
40
|
+
...(from.session && SESSION_ID.test(from.session)
|
|
41
|
+
? { fromSession: from.session }
|
|
42
|
+
: {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A peer's message, wrapped as Claude Code wraps one. */
|
|
47
|
+
export function peerEnvelope(from: PeerSource, body: string): string {
|
|
48
|
+
const attrs = [`from="${idOf(from.id)}"`];
|
|
49
|
+
if (from.session && SESSION_ID.test(from.session)) {
|
|
50
|
+
attrs.push(`from-session="${from.session}"`);
|
|
51
|
+
}
|
|
52
|
+
const name = nameOf(from.name);
|
|
53
|
+
if (name) attrs.push(`from-name="${name}"`);
|
|
54
|
+
// a body cannot close the envelope it is in
|
|
55
|
+
const safe = body.replace(new RegExp(`<\\/?(?=${PEER_TAG})`, 'gi'), '<\\');
|
|
56
|
+
return `<${PEER_TAG} ${attrs.join(' ')}>\n${safe}\n</${PEER_TAG}>`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A user turn Claude Code took from someone other than the user — another
|
|
61
|
+
* session, a channel — as the session draws it. `null` for anything else:
|
|
62
|
+
* the user's own turns, tool results, a subagent's input.
|
|
63
|
+
*/
|
|
64
|
+
export function incomingOf(
|
|
65
|
+
m: SdkMessage,
|
|
66
|
+
): { text: string; from: MessageSource } | null {
|
|
67
|
+
if (m.parent_tool_use_id) return null;
|
|
68
|
+
const origin = m.origin as Record<string, unknown> | undefined;
|
|
69
|
+
if (!origin || (origin.kind !== 'peer' && origin.kind !== 'channel')) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const text = textOf(
|
|
73
|
+
(m.message as { content?: unknown } | undefined)?.content,
|
|
74
|
+
);
|
|
75
|
+
if (text === null) return null;
|
|
76
|
+
if (origin.kind === 'channel') {
|
|
77
|
+
const server = typeof origin.server === 'string' ? origin.server : '';
|
|
78
|
+
const body = unwrap(text, CHANNEL_TAG);
|
|
79
|
+
return body.trim()
|
|
80
|
+
? { text: body, from: { kind: 'channel', server } }
|
|
81
|
+
: null;
|
|
82
|
+
}
|
|
83
|
+
const body =
|
|
84
|
+
typeof origin.body === 'string' ? origin.body : unwrap(text, PEER_TAG);
|
|
85
|
+
if (!body.trim()) return null;
|
|
86
|
+
const id = typeof origin.from === 'string' ? decode(origin.from) : 'unknown';
|
|
87
|
+
return {
|
|
88
|
+
text: body,
|
|
89
|
+
from: {
|
|
90
|
+
kind: 'peer',
|
|
91
|
+
id,
|
|
92
|
+
...(typeof origin.name === 'string' && origin.name
|
|
93
|
+
? { name: origin.name }
|
|
94
|
+
: {}),
|
|
95
|
+
...(typeof origin.fromSession === 'string'
|
|
96
|
+
? { session: origin.fromSession }
|
|
97
|
+
: {}),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function idOf(id: string): string {
|
|
103
|
+
const bytes = new TextEncoder();
|
|
104
|
+
const encoded = id.replace(ID_UNSAFE, (c) =>
|
|
105
|
+
Array.from(
|
|
106
|
+
bytes.encode(c),
|
|
107
|
+
(b) => `%${b.toString(16).toUpperCase().padStart(2, '0')}`,
|
|
108
|
+
).join(''),
|
|
109
|
+
);
|
|
110
|
+
// a cut must not leave half an escape
|
|
111
|
+
return (encoded || 'app').slice(0, ID_MAX).replace(/%[0-9A-F]?$/, '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function nameOf(name: string | undefined): string {
|
|
115
|
+
if (!name) return '';
|
|
116
|
+
const clean = name
|
|
117
|
+
.replace(/["<>]/g, '')
|
|
118
|
+
// control, format and separator characters: what Claude Code strips
|
|
119
|
+
.replace(/[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/gu, '')
|
|
120
|
+
.trim();
|
|
121
|
+
return [...clean].slice(0, NAME_MAX).join('');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function decode(id: string): string {
|
|
125
|
+
try {
|
|
126
|
+
return decodeURIComponent(id);
|
|
127
|
+
} catch {
|
|
128
|
+
return id;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The text of a turn's content, or `null` when it is not text alone. */
|
|
133
|
+
function textOf(content: unknown): string | null {
|
|
134
|
+
if (typeof content === 'string') return content;
|
|
135
|
+
if (!Array.isArray(content)) return null;
|
|
136
|
+
const texts: string[] = [];
|
|
137
|
+
for (const block of content as Array<Record<string, unknown>>) {
|
|
138
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
139
|
+
texts.push(block.text);
|
|
140
|
+
} else if (block.type === 'tool_result') {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return texts.length ? texts.join('\n') : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** What is inside `<tag …>…</tag>`, when that is the whole of the text. */
|
|
148
|
+
function unwrap(text: string, tag: string): string {
|
|
149
|
+
const match = new RegExp(
|
|
150
|
+
`^\\s*<${tag}(?:\\s[^>]*)?>\\n?([\\s\\S]*?)\\n?</${tag}>\\s*$`,
|
|
151
|
+
).exec(text);
|
|
152
|
+
return match ? match[1]!.trim() : text;
|
|
153
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// A tool's parameters as the zod shape the SDK's `tool()` takes.
|
|
2
|
+
//
|
|
3
|
+
// A zod object is handed over by its `shape`. A JSON Schema is translated:
|
|
4
|
+
// the subset tool arguments use — objects, strings, numbers, integers,
|
|
5
|
+
// booleans, arrays, enums, constants, nullable unions — with `description`
|
|
6
|
+
// carried, and anything else as `unknown`, which the model still sees the
|
|
7
|
+
// description of and the tool's own validation still checks.
|
|
8
|
+
|
|
9
|
+
import { isStandardSchema, type ToolParameters } from '../../core/tools.js';
|
|
10
|
+
import type { ZodLike, ZodTypeLike } from './sdk.js';
|
|
11
|
+
|
|
12
|
+
export function zodShapeOf(
|
|
13
|
+
parameters: ToolParameters,
|
|
14
|
+
): Record<string, unknown> | null {
|
|
15
|
+
if (isStandardSchema(parameters)) {
|
|
16
|
+
const shape = (parameters as { shape?: unknown }).shape;
|
|
17
|
+
if (shape && typeof shape === 'object')
|
|
18
|
+
return shape as Record<string, unknown>;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function shapeFromJsonSchema(
|
|
25
|
+
schema: Record<string, unknown>,
|
|
26
|
+
z: ZodLike,
|
|
27
|
+
): Record<string, ZodTypeLike> {
|
|
28
|
+
const properties = (schema.properties ?? {}) as Record<
|
|
29
|
+
string,
|
|
30
|
+
Record<string, unknown>
|
|
31
|
+
>;
|
|
32
|
+
const required = new Set((schema.required as string[] | undefined) ?? []);
|
|
33
|
+
const shape: Record<string, ZodTypeLike> = {};
|
|
34
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
35
|
+
const type = typeOf(prop, z);
|
|
36
|
+
shape[key] = required.has(key) ? type : type.optional();
|
|
37
|
+
}
|
|
38
|
+
return shape;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function typeOf(schema: Record<string, unknown>, z: ZodLike): ZodTypeLike {
|
|
42
|
+
let t = baseType(schema, z);
|
|
43
|
+
if (typeof schema.description === 'string')
|
|
44
|
+
t = t.describe(schema.description);
|
|
45
|
+
return t;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function baseType(schema: Record<string, unknown>, z: ZodLike): ZodTypeLike {
|
|
49
|
+
if (
|
|
50
|
+
Array.isArray(schema.enum) &&
|
|
51
|
+
schema.enum.every((v) => typeof v === 'string')
|
|
52
|
+
) {
|
|
53
|
+
const values = schema.enum as string[];
|
|
54
|
+
if (values.length) return z.enum(values as [string, ...string[]]);
|
|
55
|
+
}
|
|
56
|
+
if ('const' in schema) {
|
|
57
|
+
const v = schema.const;
|
|
58
|
+
if (v === null || ['string', 'number', 'boolean'].includes(typeof v)) {
|
|
59
|
+
return z.literal(v as string | number | boolean | null);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const types = Array.isArray(schema.type)
|
|
63
|
+
? (schema.type as string[])
|
|
64
|
+
: [schema.type];
|
|
65
|
+
const nonNull = types.filter((t) => t !== 'null');
|
|
66
|
+
const nullable = nonNull.length < types.length;
|
|
67
|
+
const one =
|
|
68
|
+
nonNull.length === 1 ? single(nonNull[0], schema, z) : z.unknown();
|
|
69
|
+
return nullable ? z.union([one, z.null()]) : one;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function single(
|
|
73
|
+
type: unknown,
|
|
74
|
+
schema: Record<string, unknown>,
|
|
75
|
+
z: ZodLike,
|
|
76
|
+
): ZodTypeLike {
|
|
77
|
+
switch (type) {
|
|
78
|
+
case 'string':
|
|
79
|
+
return z.string();
|
|
80
|
+
case 'number':
|
|
81
|
+
return z.number();
|
|
82
|
+
case 'integer':
|
|
83
|
+
return z.number().int();
|
|
84
|
+
case 'boolean':
|
|
85
|
+
return z.boolean();
|
|
86
|
+
case 'array':
|
|
87
|
+
return z.array(
|
|
88
|
+
schema.items && typeof schema.items === 'object'
|
|
89
|
+
? typeOf(schema.items as Record<string, unknown>, z)
|
|
90
|
+
: z.unknown(),
|
|
91
|
+
);
|
|
92
|
+
case 'object':
|
|
93
|
+
return schema.properties
|
|
94
|
+
? z.object(shapeFromJsonSchema(schema, z))
|
|
95
|
+
: z.record(z.string(), z.unknown());
|
|
96
|
+
default:
|
|
97
|
+
return z.unknown();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The slice of `@anthropic-ai/claude-agent-sdk` this driver uses, written out
|
|
2
|
+
// structurally, and the loader that reaches it.
|
|
3
|
+
//
|
|
4
|
+
// Two reasons it is not an `import type` from the package. The SDK is an
|
|
5
|
+
// optional peer — some 200 MB with its native binary — and an `import type`
|
|
6
|
+
// would put it in the type graph, so an app that never installed it could no
|
|
7
|
+
// longer type-check against this package. And tests hand the driver a fake
|
|
8
|
+
// SDK, which is all a structural type asks of them.
|
|
9
|
+
|
|
10
|
+
import { AgentUnavailableError } from '../../core/driver.js';
|
|
11
|
+
import type { ToolResult } from '../../core/tools.js';
|
|
12
|
+
|
|
13
|
+
/** One user turn, as streaming input takes it. */
|
|
14
|
+
export interface SdkUserMessage {
|
|
15
|
+
type: 'user';
|
|
16
|
+
message: {
|
|
17
|
+
role: 'user';
|
|
18
|
+
content: string | Array<Record<string, unknown>>;
|
|
19
|
+
};
|
|
20
|
+
parent_tool_use_id: null;
|
|
21
|
+
priority?: 'now' | 'next' | 'later';
|
|
22
|
+
/** Who wrote it: `{ kind: 'human' }` for the user, a peer's for another
|
|
23
|
+
* session (`peers.ts`). */
|
|
24
|
+
origin?: Record<string, unknown>;
|
|
25
|
+
/** Delivered as written: no `@path` expansion, no slash commands. */
|
|
26
|
+
client_composed?: true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What the query yields. Read loosely: the union grows every month. */
|
|
30
|
+
export interface SdkMessage {
|
|
31
|
+
type: string;
|
|
32
|
+
subtype?: string;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SdkQuery extends AsyncIterable<SdkMessage> {
|
|
37
|
+
interrupt(): Promise<unknown>;
|
|
38
|
+
setPermissionMode?(mode: string): Promise<void>;
|
|
39
|
+
setModel?(model?: string): Promise<void>;
|
|
40
|
+
supportedModels?(): Promise<
|
|
41
|
+
Array<{ value: string; displayName: string; description?: string }>
|
|
42
|
+
>;
|
|
43
|
+
supportedCommands?(): Promise<Array<{ name: string; description?: string }>>;
|
|
44
|
+
close?(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type SdkToolHandler = (
|
|
48
|
+
args: Record<string, unknown>,
|
|
49
|
+
extra: unknown,
|
|
50
|
+
) => Promise<ToolResult>;
|
|
51
|
+
|
|
52
|
+
export interface ClaudeSdk {
|
|
53
|
+
query(args: {
|
|
54
|
+
prompt: AsyncIterable<SdkUserMessage>;
|
|
55
|
+
options?: Record<string, unknown>;
|
|
56
|
+
}): SdkQuery;
|
|
57
|
+
tool(
|
|
58
|
+
name: string,
|
|
59
|
+
description: string,
|
|
60
|
+
inputSchema: Record<string, unknown>,
|
|
61
|
+
handler: SdkToolHandler,
|
|
62
|
+
): unknown;
|
|
63
|
+
createSdkMcpServer(options: {
|
|
64
|
+
name: string;
|
|
65
|
+
version?: string;
|
|
66
|
+
tools?: unknown[];
|
|
67
|
+
}): unknown;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const CLAUDE_SDK = '@anthropic-ai/claude-agent-sdk';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The SDK, or `AgentUnavailableError` saying what is wrong: not installed —
|
|
74
|
+
* with the line that installs it — or installed and failing to load, with
|
|
75
|
+
* the reason. A missing package the SDK itself needs is the second kind, not
|
|
76
|
+
* the first: telling someone who has just installed the SDK to install it
|
|
77
|
+
* sends them round in a circle.
|
|
78
|
+
*/
|
|
79
|
+
export async function loadClaudeSdk(
|
|
80
|
+
specifier: string = CLAUDE_SDK,
|
|
81
|
+
): Promise<ClaudeSdk> {
|
|
82
|
+
try {
|
|
83
|
+
// The specifier is a variable so neither the compiler nor a bundler
|
|
84
|
+
// follows it: the package is an optional peer.
|
|
85
|
+
return (await import(specifier)) as ClaudeSdk;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
88
|
+
const code = (err as { code?: unknown } | null)?.code;
|
|
89
|
+
const missing =
|
|
90
|
+
(code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') &&
|
|
91
|
+
message.includes(`'${specifier}'`);
|
|
92
|
+
throw new AgentUnavailableError(
|
|
93
|
+
missing
|
|
94
|
+
? `The Claude Agent SDK is not installed. Install it beside this ` +
|
|
95
|
+
`package: npm install ${specifier}`
|
|
96
|
+
: `The Claude Agent SDK is installed but did not load: ${message}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Zod, for the SDK's in-process tools: `tool()` takes a zod shape. An app
|
|
103
|
+
* whose parameters are zod already hands its own over; a JSON Schema is
|
|
104
|
+
* turned into one with the zod the SDK brought (a peer of the SDK, so
|
|
105
|
+
* installed beside it).
|
|
106
|
+
*/
|
|
107
|
+
export interface ZodLike {
|
|
108
|
+
string(): ZodTypeLike;
|
|
109
|
+
number(): ZodTypeLike & { int(): ZodTypeLike };
|
|
110
|
+
boolean(): ZodTypeLike;
|
|
111
|
+
array(item: ZodTypeLike): ZodTypeLike;
|
|
112
|
+
object(shape: Record<string, ZodTypeLike>): ZodTypeLike;
|
|
113
|
+
enum(values: [string, ...string[]]): ZodTypeLike;
|
|
114
|
+
unknown(): ZodTypeLike;
|
|
115
|
+
record(key: ZodTypeLike, value: ZodTypeLike): ZodTypeLike;
|
|
116
|
+
union(options: [ZodTypeLike, ZodTypeLike, ...ZodTypeLike[]]): ZodTypeLike;
|
|
117
|
+
literal(value: string | number | boolean | null): ZodTypeLike;
|
|
118
|
+
null(): ZodTypeLike;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ZodTypeLike {
|
|
122
|
+
optional(): ZodTypeLike;
|
|
123
|
+
describe(description: string): ZodTypeLike;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function loadZod(): Promise<ZodLike> {
|
|
127
|
+
try {
|
|
128
|
+
const name: string = 'zod';
|
|
129
|
+
const mod = (await import(name)) as { z?: ZodLike } & ZodLike;
|
|
130
|
+
return mod.z ?? mod;
|
|
131
|
+
} catch {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'A tool with a JSON Schema needs zod to reach the Claude Agent SDK, ' +
|
|
134
|
+
'which installs it as a peer. Install zod, or define the parameters ' +
|
|
135
|
+
'with z.object().',
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Claude Code's built-in tools: which `coreTools` category each is in, its
|
|
2
|
+
// kind, and the one line its card shows.
|
|
3
|
+
|
|
4
|
+
import type { CoreToolKey, ToolKind, ToolOutput } from '../../core/types.js';
|
|
5
|
+
|
|
6
|
+
/** The built-ins each category names. A name Claude Code no longer has is
|
|
7
|
+
* harmless in `disallowedTools`. */
|
|
8
|
+
export const CATEGORY_TOOLS: Readonly<Record<CoreToolKey, readonly string[]>> =
|
|
9
|
+
{
|
|
10
|
+
shell: ['Bash', 'BashOutput', 'KillShell'],
|
|
11
|
+
read: ['Read', 'Glob', 'Grep', 'LS'],
|
|
12
|
+
edit: ['Edit', 'MultiEdit', 'Write', 'NotebookEdit'],
|
|
13
|
+
web: ['WebFetch', 'WebSearch'],
|
|
14
|
+
agents: ['Agent', 'Task'],
|
|
15
|
+
todos: ['TodoWrite', 'TaskCreate', 'TaskUpdate', 'TaskList', 'TaskGet'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** The prefix in-process tools are named under: `mcp__app__openTicket`. */
|
|
19
|
+
export const APP_SERVER = 'app';
|
|
20
|
+
export const APP_PREFIX = `mcp__${APP_SERVER}__`;
|
|
21
|
+
|
|
22
|
+
const KINDS: Readonly<Record<string, ToolKind>> = {
|
|
23
|
+
Bash: 'execute',
|
|
24
|
+
BashOutput: 'execute',
|
|
25
|
+
KillShell: 'execute',
|
|
26
|
+
Read: 'read',
|
|
27
|
+
LS: 'read',
|
|
28
|
+
NotebookRead: 'read',
|
|
29
|
+
Glob: 'search',
|
|
30
|
+
Grep: 'search',
|
|
31
|
+
Edit: 'edit',
|
|
32
|
+
MultiEdit: 'edit',
|
|
33
|
+
Write: 'edit',
|
|
34
|
+
NotebookEdit: 'edit',
|
|
35
|
+
WebFetch: 'fetch',
|
|
36
|
+
WebSearch: 'fetch',
|
|
37
|
+
Agent: 'think',
|
|
38
|
+
Task: 'think',
|
|
39
|
+
ExitPlanMode: 'think',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The tools this driver draws as something other than a tool card. */
|
|
43
|
+
export const HIDDEN_TOOLS: ReadonlySet<string> = new Set([
|
|
44
|
+
'TodoWrite',
|
|
45
|
+
'AskUserQuestion',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export function labelOf(name: string): string {
|
|
49
|
+
return name.startsWith(APP_PREFIX) ? name.slice(APP_PREFIX.length) : name;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isAppTool(name: string): boolean {
|
|
53
|
+
return name.startsWith(APP_PREFIX);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function kindOf(name: string): ToolKind {
|
|
57
|
+
return KINDS[name] ?? 'other';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The card's line: `Read src/app.ts`, `make test`, `Fetch https://…`. */
|
|
61
|
+
export function titleOf(name: string, input: unknown): string {
|
|
62
|
+
const label = labelOf(name);
|
|
63
|
+
const i = (input && typeof input === 'object' ? input : {}) as Record<
|
|
64
|
+
string,
|
|
65
|
+
unknown
|
|
66
|
+
>;
|
|
67
|
+
const s = (k: string) => (typeof i[k] === 'string' ? (i[k] as string) : '');
|
|
68
|
+
switch (name) {
|
|
69
|
+
case 'Bash':
|
|
70
|
+
return s('description') || firstLine(s('command')) || 'Bash';
|
|
71
|
+
case 'Read':
|
|
72
|
+
case 'Edit':
|
|
73
|
+
case 'MultiEdit':
|
|
74
|
+
case 'Write':
|
|
75
|
+
return s('file_path') ? `${name} ${s('file_path')}` : name;
|
|
76
|
+
case 'NotebookEdit':
|
|
77
|
+
return s('notebook_path') ? `${name} ${s('notebook_path')}` : name;
|
|
78
|
+
case 'Glob':
|
|
79
|
+
return s('pattern') ? `Find ${s('pattern')}` : name;
|
|
80
|
+
case 'Grep':
|
|
81
|
+
return s('pattern') ? `Search for ${s('pattern')}` : name;
|
|
82
|
+
case 'LS':
|
|
83
|
+
return s('path') ? `List ${s('path')}` : name;
|
|
84
|
+
case 'WebFetch':
|
|
85
|
+
return s('url') ? `Fetch ${s('url')}` : name;
|
|
86
|
+
case 'WebSearch':
|
|
87
|
+
return s('query') ? `Search the web for ${s('query')}` : name;
|
|
88
|
+
case 'Agent':
|
|
89
|
+
case 'Task':
|
|
90
|
+
return s('description') || 'Agent';
|
|
91
|
+
case 'ExitPlanMode':
|
|
92
|
+
return 'Approve the plan';
|
|
93
|
+
default:
|
|
94
|
+
return label;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** What a finished built-in call shows, worked out from its input where the
|
|
99
|
+
* input says more than the result: an edit is its diff. */
|
|
100
|
+
export function outputOf(
|
|
101
|
+
name: string,
|
|
102
|
+
input: unknown,
|
|
103
|
+
resultText: string,
|
|
104
|
+
isError: boolean,
|
|
105
|
+
): ToolOutput[] {
|
|
106
|
+
const i = (input && typeof input === 'object' ? input : {}) as Record<
|
|
107
|
+
string,
|
|
108
|
+
unknown
|
|
109
|
+
>;
|
|
110
|
+
if (!isError) {
|
|
111
|
+
if (name === 'Edit' && typeof i.file_path === 'string') {
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
type: 'diff',
|
|
115
|
+
path: i.file_path,
|
|
116
|
+
oldText: String(i.old_string ?? ''),
|
|
117
|
+
newText: String(i.new_string ?? ''),
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
if (name === 'Write' && typeof i.file_path === 'string') {
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
type: 'diff',
|
|
125
|
+
path: i.file_path,
|
|
126
|
+
oldText: null,
|
|
127
|
+
newText: String(i.content ?? ''),
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!resultText) return [];
|
|
133
|
+
if (kindOf(name) === 'execute')
|
|
134
|
+
return [{ type: 'terminal', text: resultText }];
|
|
135
|
+
return [{ type: 'text', text: resultText }];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function firstLine(text: string): string {
|
|
139
|
+
const line = text.split('\n')[0] ?? '';
|
|
140
|
+
return line.length > 120 ? `${line.slice(0, 117)}…` : line;
|
|
141
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// `@windowkit/agent-chat`: `<AgentChat>` and the hooks around it, with the
|
|
2
|
+
// core's types and helpers re-exported, so an app imports from one place.
|
|
3
|
+
//
|
|
4
|
+
// The drivers are not re-exported: each is its own subpath
|
|
5
|
+
// (`/drivers/claude`), loaded when a session starts, so a bundle carries the
|
|
6
|
+
// one it uses.
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
AgentChat,
|
|
10
|
+
type AgentChatProps,
|
|
11
|
+
type AgentChatViewProps,
|
|
12
|
+
} from './view/AgentChat.js';
|
|
13
|
+
export { useAgent, useSessionSnapshot } from './view/use-agent.js';
|
|
14
|
+
export type { ComposerOptions } from './view/Composer.js';
|
|
15
|
+
export {
|
|
16
|
+
copyCode,
|
|
17
|
+
defineTool,
|
|
18
|
+
type CodeAction,
|
|
19
|
+
type CodeActionContext,
|
|
20
|
+
type ComponentEntry,
|
|
21
|
+
type FenceEntry,
|
|
22
|
+
type FenceInfo,
|
|
23
|
+
type ToolRenderer,
|
|
24
|
+
} from './view/types.js';
|
|
25
|
+
export { splitFences, type Segment } from './view/fences.js';
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
AgentUnavailableError,
|
|
29
|
+
categoryOf,
|
|
30
|
+
createAgentSession,
|
|
31
|
+
describeRenderers,
|
|
32
|
+
messageMarkdown,
|
|
33
|
+
sourceLabel,
|
|
34
|
+
transcriptMarkdown,
|
|
35
|
+
type AgentDriver,
|
|
36
|
+
type AgentOptions,
|
|
37
|
+
type AgentSession,
|
|
38
|
+
type AnyToolDef,
|
|
39
|
+
type CoreToolSetting,
|
|
40
|
+
type DriverConfig,
|
|
41
|
+
type DriverEvent,
|
|
42
|
+
type DriverHost,
|
|
43
|
+
type DriverSession,
|
|
44
|
+
type SessionSnapshot,
|
|
45
|
+
type ToolContext,
|
|
46
|
+
type ToolDef,
|
|
47
|
+
type ToolParameters,
|
|
48
|
+
type ToolRenderInfo,
|
|
49
|
+
type ToolResult,
|
|
50
|
+
} from './core/index.js';
|
|
51
|
+
export type * from './core/types.js';
|