@ahpd/agent-claude 0.3.0 → 0.5.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/README.md +6 -0
- package/dist/claude.d.ts +6 -8
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +93 -17
- package/dist/claude.js.map +1 -1
- package/dist/kinds.d.ts +29 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/kinds.js +42 -0
- package/dist/kinds.js.map +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +198 -9
- package/dist/session.js.map +1 -1
- package/dist/transcript.d.ts.map +1 -1
- package/dist/transcript.js +5 -0
- package/dist/transcript.js.map +1 -1
- package/package.json +2 -2
- package/src/claude.ts +94 -27
- package/src/kinds.ts +46 -0
- package/src/session.ts +191 -11
- package/src/transcript.ts +5 -0
package/src/kinds.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What kind of thing a tool call is, said where the reference client reads it.
|
|
3
|
+
*
|
|
4
|
+
* `_meta.toolKind` is not protocol. It is the one well-known key VS Code's
|
|
5
|
+
* agent window routes a tool call by: `terminal` goes to the command-and-output
|
|
6
|
+
* renderer, `subagent` to the subagent view, `search` and `read` to theirs, and
|
|
7
|
+
* a call with none is drawn as a generic tool - a name and a box. The reference
|
|
8
|
+
* host stamps it in its own adapters, and derives it for a "remote host" from
|
|
9
|
+
* a Copilot-internal permission payload this backend does not have; so it is
|
|
10
|
+
* stamped here, from the harness's own tool names, which are the one thing
|
|
11
|
+
* about a tool call this backend can be sure of.
|
|
12
|
+
*
|
|
13
|
+
* Names only. Nothing else about the call decides the kind, and a tool this
|
|
14
|
+
* table has not heard of is left unstamped rather than guessed at - the
|
|
15
|
+
* generic renderer is right for a tool nobody here knows.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type ToolKind = 'terminal' | 'read' | 'search' | 'subagent';
|
|
19
|
+
|
|
20
|
+
const KINDS: Readonly<Record<string, ToolKind>> = {
|
|
21
|
+
Bash: 'terminal',
|
|
22
|
+
terminal: 'terminal',
|
|
23
|
+
Read: 'read',
|
|
24
|
+
Glob: 'search',
|
|
25
|
+
Grep: 'search',
|
|
26
|
+
WebSearch: 'search',
|
|
27
|
+
WebFetch: 'search',
|
|
28
|
+
Task: 'subagent',
|
|
29
|
+
Agent: 'subagent',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The kind of a tool by its harness name, or nothing for one the table lacks. */
|
|
33
|
+
export const toolKindOf = (name: string): ToolKind | undefined => KINDS[name];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The `_meta` a tool call carries from the moment it is announced.
|
|
37
|
+
*
|
|
38
|
+
* The reducer replaces a call's whole `_meta` whenever an action carries one,
|
|
39
|
+
* so anything added later - a progress line while it runs - has to be spread
|
|
40
|
+
* over this rather than sent alone. Absent for a tool with no kind, so a call
|
|
41
|
+
* that has nothing to say carries no empty bag.
|
|
42
|
+
*/
|
|
43
|
+
export const toolMetaOf = (name: string): Record<string, unknown> | undefined => {
|
|
44
|
+
const toolKind = toolKindOf(name);
|
|
45
|
+
return toolKind === undefined ? undefined : { toolKind };
|
|
46
|
+
};
|
package/src/session.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import { createSdkMcpServer, query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
5
|
import { z } from 'zod';
|
|
3
|
-
import type { PermissionMode } from '@anthropic-ai/claude-agent-sdk';
|
|
6
|
+
import type { HookCallback, PermissionMode } from '@anthropic-ai/claude-agent-sdk';
|
|
4
7
|
import { protectedResource, urlOf } from './mcp.js';
|
|
8
|
+
import { toolMetaOf } from './kinds.js';
|
|
5
9
|
import type { ActiveTurn, McpServerState, ToolCallCompletedState, ToolCallRunningState, ToolResultContent, ToolResultTerminalContent, ToolResultTextContent } from '@microsoft/agent-host-protocol';
|
|
6
10
|
import { Status, idOf, tail } from '@ahpd/sdk';
|
|
7
|
-
import type { Bag, BoundTool, Chosen, OnWire, Session, SessionOptions, WireTurn } from '@ahpd/sdk';
|
|
11
|
+
import type { Bag, BoundTool, Chosen, MessageFrom, OnWire, Session, SessionOptions, WireTurn } from '@ahpd/sdk';
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* The effort levels this backend has, weakest first.
|
|
@@ -57,6 +61,43 @@ const bag = (value: unknown): Bag => (typeof value === 'object' && value !== nul
|
|
|
57
61
|
const list = (value: unknown): unknown[] => (Array.isArray(value) ? value : []);
|
|
58
62
|
const str = (value: unknown): string | undefined => (typeof value === 'string' ? value : undefined);
|
|
59
63
|
|
|
64
|
+
/** One client-generated script, sourced before every shell command. */
|
|
65
|
+
interface ShellInitScript { shell: 'bash' | 'powershell'; script: string }
|
|
66
|
+
|
|
67
|
+
/** A generated script is a few hundred bytes; anything near this is not one. */
|
|
68
|
+
const MAX_SHELL_INIT_SCRIPT = 64 * 1024;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The `shellInitScripts` value, checked to the reference host's rule.
|
|
72
|
+
*
|
|
73
|
+
* A list of `{ shell, script }`, each script non-empty and no longer than a
|
|
74
|
+
* generated one could be. `undefined` for anything else, which is what lets
|
|
75
|
+
* `setConfig` refuse it rather than write it to disk.
|
|
76
|
+
*/
|
|
77
|
+
const shellInitScripts = (value: unknown): ShellInitScript[] | undefined => {
|
|
78
|
+
if (!Array.isArray(value)) return undefined;
|
|
79
|
+
const list: ShellInitScript[] = [];
|
|
80
|
+
for (const entry of value) {
|
|
81
|
+
const one = bag(entry);
|
|
82
|
+
if ((one.shell !== 'bash' && one.shell !== 'powershell') || typeof one.script !== 'string') return undefined;
|
|
83
|
+
if (one.script.length === 0 || one.script.length > MAX_SHELL_INIT_SCRIPT) return undefined;
|
|
84
|
+
list.push({ shell: one.shell, script: one.script });
|
|
85
|
+
}
|
|
86
|
+
return list;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What goes in front of a shell command while a script is in force.
|
|
91
|
+
*
|
|
92
|
+
* Sourced, so what it sets is there for the command; its stderr dropped, as
|
|
93
|
+
* the reference runtime drops it; and a nonzero status reported rather than
|
|
94
|
+
* hidden, since a profile that fails is something the model should hear.
|
|
95
|
+
*/
|
|
96
|
+
const sourcing = (path: string): string => `{ . '${path.replaceAll("'", "'\\''")}'; } 2>/dev/null || printf 'shell init script exited %s\\n' "$?"`;
|
|
97
|
+
|
|
98
|
+
/** The CLI's sandbox setting for the reference host's three words; `null` clears it back to the settings files. */
|
|
99
|
+
const sandboxOf = (value: unknown): { enabled: boolean } | null => (value === 'on' ? { enabled: true } : value === 'off' ? { enabled: false } : null);
|
|
100
|
+
|
|
60
101
|
interface PendingInput {
|
|
61
102
|
id: string;
|
|
62
103
|
entry: Bag;
|
|
@@ -536,6 +577,43 @@ export function createSession(options: SessionOptions): Session {
|
|
|
536
577
|
*/
|
|
537
578
|
const settings: Record<string, unknown> = { permissionMode: 'default', ...options.settings };
|
|
538
579
|
|
|
580
|
+
/*
|
|
581
|
+
* The shell init script, on disk where a shell can source it.
|
|
582
|
+
*
|
|
583
|
+
* The reference client pushes `shellInitScripts` for the profile and the
|
|
584
|
+
* Python environment it has selected, and the SDK's shell tool has no
|
|
585
|
+
* setting for one - so a `PreToolUse` hook on `Bash` puts a `source` of
|
|
586
|
+
* this file in front of every command. One path for the session's life,
|
|
587
|
+
* rewritten on each change, because the hook is built once with the query
|
|
588
|
+
* and reads the file by name. The bash entry only: the CLI's shell tool is
|
|
589
|
+
* bash on every platform it runs on.
|
|
590
|
+
*/
|
|
591
|
+
const initScript = join(tmpdir(), `ahpd-shell-init-${crypto.randomUUID()}.sh`);
|
|
592
|
+
let sourced = false;
|
|
593
|
+
const setShellInit = (value: unknown): true | string => {
|
|
594
|
+
const list = shellInitScripts(value);
|
|
595
|
+
if (list === undefined) return 'shellInitScripts takes a list of { shell, script }';
|
|
596
|
+
const bash = list.find((one) => one.shell === 'bash');
|
|
597
|
+
try {
|
|
598
|
+
if (bash === undefined) rmSync(initScript, { force: true });
|
|
599
|
+
else writeFileSync(initScript, bash.script, { mode: 0o600 });
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
return `Could not write the shell init script: ${error instanceof Error ? error.message : String(error)}`;
|
|
603
|
+
}
|
|
604
|
+
sourced = bash !== undefined;
|
|
605
|
+
settings.shellInitScripts = list;
|
|
606
|
+
return true;
|
|
607
|
+
};
|
|
608
|
+
if (settings.shellInitScripts !== undefined) setShellInit(settings.shellInitScripts);
|
|
609
|
+
const sourceFirst: HookCallback = async (input) => {
|
|
610
|
+
if (!sourced || input.hook_event_name !== 'PreToolUse') return {};
|
|
611
|
+
const given = bag(input.tool_input);
|
|
612
|
+
const command = str(given.command);
|
|
613
|
+
if (command === undefined) return {};
|
|
614
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', updatedInput: { ...given, command: `${sourcing(initScript)}\n${command}` } } };
|
|
615
|
+
};
|
|
616
|
+
|
|
539
617
|
/**
|
|
540
618
|
* The MCP server a tool belongs to, out of its name.
|
|
541
619
|
*
|
|
@@ -926,12 +1004,17 @@ export function createSession(options: SessionOptions): Session {
|
|
|
926
1004
|
const contributor = from === undefined
|
|
927
1005
|
? undefined
|
|
928
1006
|
: { kind: 'mcp' as const, customizationId: `mcp:${from}` };
|
|
1007
|
+
// What kind of row to draw, from the name and from the first frame:
|
|
1008
|
+
// a client that waited for the arguments to know it was a shell
|
|
1009
|
+
// command would draw a generic box and then redraw it.
|
|
1010
|
+
const meta = toolMetaOf(name);
|
|
929
1011
|
const call: Bag = {
|
|
930
1012
|
toolCallId: id,
|
|
931
1013
|
toolName: name,
|
|
932
1014
|
displayName: name,
|
|
933
1015
|
status: 'streaming',
|
|
934
1016
|
...(contributor ? { contributor } : {}),
|
|
1017
|
+
...(meta ? { _meta: meta } : {}),
|
|
935
1018
|
};
|
|
936
1019
|
const part: Bag = { id, kind: 'toolCall', toolCall: call };
|
|
937
1020
|
parts.set(id, part);
|
|
@@ -943,6 +1026,7 @@ export function createSession(options: SessionOptions): Session {
|
|
|
943
1026
|
toolName: name,
|
|
944
1027
|
displayName: name,
|
|
945
1028
|
...(contributor ? { contributor } : {}),
|
|
1029
|
+
...(meta ? { _meta: meta } : {}),
|
|
946
1030
|
});
|
|
947
1031
|
return;
|
|
948
1032
|
}
|
|
@@ -1055,12 +1139,14 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1055
1139
|
// Running against somebody else's server, and so a call that can end
|
|
1056
1140
|
// up waiting on a sign-in rather than on its own work.
|
|
1057
1141
|
if (from !== undefined) onServer.set(id, { server: from, turnId: str(turn.id) ?? '', blocked: false });
|
|
1142
|
+
const meta = toolMetaOf(name);
|
|
1058
1143
|
const call: Bag = open !== undefined ? bag(open.toolCall) : {
|
|
1059
1144
|
toolCallId: id,
|
|
1060
1145
|
toolName: name,
|
|
1061
1146
|
displayName: name,
|
|
1062
1147
|
status: 'running',
|
|
1063
1148
|
...(contributor ? { contributor } : {}),
|
|
1149
|
+
...(meta ? { _meta: meta } : {}),
|
|
1064
1150
|
/*
|
|
1065
1151
|
* On the call, and not only on the action that announces it.
|
|
1066
1152
|
*
|
|
@@ -1111,6 +1197,7 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1111
1197
|
toolName: name,
|
|
1112
1198
|
displayName: name,
|
|
1113
1199
|
...(contributor ? { contributor } : {}),
|
|
1200
|
+
...(meta ? { _meta: meta } : {}),
|
|
1114
1201
|
});
|
|
1115
1202
|
}
|
|
1116
1203
|
emit('chat', {
|
|
@@ -1191,6 +1278,22 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1191
1278
|
* itself, and it is checked against the state it completes.
|
|
1192
1279
|
*/
|
|
1193
1280
|
Object.assign(call, result);
|
|
1281
|
+
/*
|
|
1282
|
+
* The progress line goes with the running state it described.
|
|
1283
|
+
*
|
|
1284
|
+
* Meaningful only while the call runs, and a completed row that still
|
|
1285
|
+
* carries "Running Grep" is a row that says two things. Sent on the
|
|
1286
|
+
* completion only when there was one to take off, because an action
|
|
1287
|
+
* carrying `_meta` replaces the bag whole and an absent one leaves
|
|
1288
|
+
* the kind stamped at the start alone.
|
|
1289
|
+
*/
|
|
1290
|
+
const meta = bag(call._meta);
|
|
1291
|
+
const progressed = meta.progressMessage !== undefined;
|
|
1292
|
+
if (progressed) {
|
|
1293
|
+
const { progressMessage: _gone, ...rest } = meta;
|
|
1294
|
+
if (Object.keys(rest).length > 0) call._meta = rest;
|
|
1295
|
+
else delete call._meta;
|
|
1296
|
+
}
|
|
1194
1297
|
// And as it is now the tool has run. Paired with the `before` above by
|
|
1195
1298
|
// the call's own id, which is the only thing that survives the gap.
|
|
1196
1299
|
const changed = id === undefined ? undefined : editing.get(id);
|
|
@@ -1203,6 +1306,7 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1203
1306
|
turnId: active?.id,
|
|
1204
1307
|
toolCallId: id,
|
|
1205
1308
|
result,
|
|
1309
|
+
...(progressed ? { _meta: call._meta ?? {} } : {}),
|
|
1206
1310
|
});
|
|
1207
1311
|
}
|
|
1208
1312
|
};
|
|
@@ -1296,11 +1400,13 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1296
1400
|
// The call the assistant message opened, if it arrived first. Which of
|
|
1297
1401
|
// the two comes first is the CLI's business; either order is one call.
|
|
1298
1402
|
const held = parts.get(id);
|
|
1403
|
+
const meta = toolMetaOf(toolName);
|
|
1299
1404
|
const call = held ? bag(held.toolCall) : {
|
|
1300
1405
|
toolCallId: id,
|
|
1301
1406
|
toolName,
|
|
1302
1407
|
displayName,
|
|
1303
1408
|
...(command ? { toolInput: command } : {}),
|
|
1409
|
+
...(meta ? { _meta: meta } : {}),
|
|
1304
1410
|
} as Bag;
|
|
1305
1411
|
call.status = 'pending-confirmation';
|
|
1306
1412
|
call.confirmationTitle = confirmationTitle;
|
|
@@ -1312,7 +1418,10 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1312
1418
|
const part: Bag = { id, kind: 'toolCall', toolCall: call };
|
|
1313
1419
|
parts.set(id, part);
|
|
1314
1420
|
holdPart(turn, part);
|
|
1315
|
-
emit('chat', {
|
|
1421
|
+
emit('chat', {
|
|
1422
|
+
type: 'chat/toolCallStart', turnId: turn.id, toolCallId: id, toolName, displayName,
|
|
1423
|
+
...(meta ? { _meta: meta } : {}),
|
|
1424
|
+
});
|
|
1316
1425
|
}
|
|
1317
1426
|
emit('chat', {
|
|
1318
1427
|
type: 'chat/toolCallReady',
|
|
@@ -1359,6 +1468,17 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1359
1468
|
* `setMcpServers` does not touch servers that came from a settings file.
|
|
1360
1469
|
*/
|
|
1361
1470
|
...(Object.keys(declared).length > 0 ? { mcpServers: declared as never } : {}),
|
|
1471
|
+
/*
|
|
1472
|
+
* What the host wants said, after the CLI's own prompt.
|
|
1473
|
+
*
|
|
1474
|
+
* The preset with an `append`, not a prompt of this backend's own: the
|
|
1475
|
+
* CLI's prompt is what makes it the CLI. `snapshot`, so the prompt is
|
|
1476
|
+
* recorded once for the conversation and a resume does not rewrite it
|
|
1477
|
+
* under the model's reasoning.
|
|
1478
|
+
*/
|
|
1479
|
+
...(options.instructions && options.instructions.length > 0
|
|
1480
|
+
? { systemPrompt: { type: 'preset' as const, preset: 'claude_code' as const, append: options.instructions.join('\n\n'), snapshot: true } }
|
|
1481
|
+
: {}),
|
|
1362
1482
|
includePartialMessages: true,
|
|
1363
1483
|
/*
|
|
1364
1484
|
* Over the daemon's own environment, never instead of it.
|
|
@@ -1374,6 +1494,11 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1374
1494
|
// From the settings, which is where it lives: it is a config key like
|
|
1375
1495
|
// the others, and a second way in was a second thing to keep in step.
|
|
1376
1496
|
...(typeof settings.permissionMode === 'string' ? { permissionMode: settings.permissionMode } : {}),
|
|
1497
|
+
// The flag settings layer, which `applyFlagSettings` moves later: one
|
|
1498
|
+
// place for the sandbox, whether it was set at creation or since.
|
|
1499
|
+
...(sandboxOf(settings.sandboxEnabled) !== null ? { settings: { sandbox: sandboxOf(settings.sandboxEnabled) } } : {}),
|
|
1500
|
+
// Before every shell command, while a client has a script in force.
|
|
1501
|
+
hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [sourceFirst] }] },
|
|
1377
1502
|
/*
|
|
1378
1503
|
* The lists, at the moment the query is built.
|
|
1379
1504
|
*
|
|
@@ -1459,7 +1584,7 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1459
1584
|
*/
|
|
1460
1585
|
const ends = new Map<string, string>();
|
|
1461
1586
|
|
|
1462
|
-
const beginTurn = (turnId: string, text: string, model?: Chosen, queuedMessageId?: string): void => {
|
|
1587
|
+
const beginTurn = (turnId: string, text: string, model?: Chosen, queuedMessageId?: string, from?: MessageFrom): void => {
|
|
1463
1588
|
if (model !== undefined && model.id !== chosen) {
|
|
1464
1589
|
chosen = model.id;
|
|
1465
1590
|
void handle.setModel(model.id === 'default' ? undefined : model.id).catch(() => {});
|
|
@@ -1484,7 +1609,8 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1484
1609
|
startedAt: new Date().toISOString(),
|
|
1485
1610
|
message: {
|
|
1486
1611
|
text,
|
|
1487
|
-
origin: { kind: 'user' },
|
|
1612
|
+
origin: from?.origin ?? { kind: 'user' },
|
|
1613
|
+
...(from?._meta ? { _meta: from._meta } : {}),
|
|
1488
1614
|
...(chosen ? { model: { id: chosen, ...(model?.config ? { config: model.config } : {}) } } : {}),
|
|
1489
1615
|
},
|
|
1490
1616
|
responseParts: [],
|
|
@@ -1542,7 +1668,12 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1542
1668
|
let model: Chosen | undefined;
|
|
1543
1669
|
if (id !== undefined)
|
|
1544
1670
|
model = named.config ? { id, config: named.config as NonNullable<Chosen['config']> } : { id };
|
|
1545
|
-
|
|
1671
|
+
// With whose it was: a message an agent queued is still an agent's when
|
|
1672
|
+
// its turn comes.
|
|
1673
|
+
const from: MessageFrom = {};
|
|
1674
|
+
if (message.origin !== undefined) from.origin = bag(message.origin) as NonNullable<MessageFrom['origin']>;
|
|
1675
|
+
if (message._meta !== undefined) from._meta = bag(message._meta);
|
|
1676
|
+
beginTurn(crypto.randomUUID(), str(message.text) ?? '', model, str(next.id), from);
|
|
1546
1677
|
};
|
|
1547
1678
|
|
|
1548
1679
|
/**
|
|
@@ -1764,6 +1895,40 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1764
1895
|
if (entry !== undefined) ends.set(String(active.id), entry);
|
|
1765
1896
|
}
|
|
1766
1897
|
|
|
1898
|
+
/*
|
|
1899
|
+
* A running subagent, saying how far it has got.
|
|
1900
|
+
*
|
|
1901
|
+
* `task_progress` is the harness's own status line for a `Task`
|
|
1902
|
+
* that is still running: a model-written summary when the option
|
|
1903
|
+
* is on, or the last tool it reached for. It goes on the call as
|
|
1904
|
+
* `_meta.progressMessage` - the reference client's word for a line
|
|
1905
|
+
* drawn on a running row and dropped when the row ends - and never
|
|
1906
|
+
* into the result, which is what the tool answered and not what it
|
|
1907
|
+
* was doing on the way. The reducer replaces a call's whole `_meta`
|
|
1908
|
+
* on any action that carries one, so the kind stamped at the start
|
|
1909
|
+
* is carried along rather than lost. The same line twice is said
|
|
1910
|
+
* once.
|
|
1911
|
+
*/
|
|
1912
|
+
if (type === 'system' && str(message.subtype) === 'task_progress') {
|
|
1913
|
+
const id = str(message.tool_use_id);
|
|
1914
|
+
const part = id === undefined ? undefined : parts.get(id);
|
|
1915
|
+
const call = part === undefined ? undefined : bag(part.toolCall);
|
|
1916
|
+
const line = str(message.summary)
|
|
1917
|
+
?? (str(message.last_tool_name) !== undefined ? `Running ${String(message.last_tool_name)}` : undefined);
|
|
1918
|
+
if (call !== undefined && line !== undefined && str(call.status) === 'running'
|
|
1919
|
+
&& str(bag(call._meta).progressMessage) !== line) {
|
|
1920
|
+
call._meta = { ...bag(call._meta), progressMessage: line };
|
|
1921
|
+
emit('chat', {
|
|
1922
|
+
type: 'chat/toolCallContentChanged',
|
|
1923
|
+
turnId: active?.id,
|
|
1924
|
+
toolCallId: id,
|
|
1925
|
+
content: list(call.content),
|
|
1926
|
+
_meta: call._meta,
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1767
1932
|
if (type === 'stream_event') { streamed(bag(message.event)); continue; }
|
|
1768
1933
|
if (type === 'assistant') { assistant(bag(message.message)); continue; }
|
|
1769
1934
|
if (type === 'user') {
|
|
@@ -1984,7 +2149,14 @@ export function createSession(options: SessionOptions): Session {
|
|
|
1984
2149
|
settings.permissions = held;
|
|
1985
2150
|
return true;
|
|
1986
2151
|
}
|
|
2152
|
+
if (key === 'shellInitScripts') return setShellInit(value);
|
|
1987
2153
|
const said = typeof value === 'string' ? value : '';
|
|
2154
|
+
if (key === 'sandboxEnabled') {
|
|
2155
|
+
if (said !== 'default' && said !== 'on' && said !== 'off') return `sandboxEnabled takes default, on or off, not ${said}`;
|
|
2156
|
+
settings.sandboxEnabled = said;
|
|
2157
|
+
void handle.applyFlagSettings({ sandbox: sandboxOf(said) }).catch(() => {});
|
|
2158
|
+
return true;
|
|
2159
|
+
}
|
|
1988
2160
|
if (key === 'model') {
|
|
1989
2161
|
try {
|
|
1990
2162
|
await handle.setModel(said === 'default' ? undefined : said);
|
|
@@ -2171,7 +2343,8 @@ export function createSession(options: SessionOptions): Session {
|
|
|
2171
2343
|
* that cannot, because the transcript would then credit a turn to a model
|
|
2172
2344
|
* that never ran it.
|
|
2173
2345
|
*/
|
|
2174
|
-
begin: (turnId, text, model) => beginTurn(turnId, text, model),
|
|
2346
|
+
begin: (turnId, text, model, from) => beginTurn(turnId, text, model, undefined, from),
|
|
2347
|
+
setTitle: (said) => { if (said !== '') title = said; },
|
|
2175
2348
|
|
|
2176
2349
|
/**
|
|
2177
2350
|
* A turn this host answered itself, with a shell rather than the agent.
|
|
@@ -2224,11 +2397,15 @@ export function createSession(options: SessionOptions): Session {
|
|
|
2224
2397
|
// The person typed it themselves, so there is nobody left to ask.
|
|
2225
2398
|
confirmed: 'not-needed',
|
|
2226
2399
|
status: 'running',
|
|
2400
|
+
_meta: { toolKind: 'terminal' },
|
|
2227
2401
|
} satisfies OnWire<ToolCallRunningState> as Bag;
|
|
2228
|
-
|
|
2402
|
+
// As a part, the way every other call is held: the bare call went
|
|
2403
|
+
// into the snapshot with no `kind`, so a client that subscribed after
|
|
2404
|
+
// the command ran had a row it could not draw.
|
|
2405
|
+
holdPart(turn, { id: toolCallId, kind: 'toolCall', toolCall: call });
|
|
2229
2406
|
emit('chat', {
|
|
2230
2407
|
type: 'chat/toolCallStart', turnId, toolCallId, toolName: 'terminal',
|
|
2231
|
-
displayName: 'Terminal', intention: command,
|
|
2408
|
+
displayName: 'Terminal', intention: command, _meta: { toolKind: 'terminal' },
|
|
2232
2409
|
});
|
|
2233
2410
|
emit('chat', {
|
|
2234
2411
|
type: 'chat/toolCallReady', turnId, toolCallId,
|
|
@@ -2320,12 +2497,13 @@ export function createSession(options: SessionOptions): Session {
|
|
|
2320
2497
|
* immediately started, which is a queue entry a client sees appear and
|
|
2321
2498
|
* leave rather than one that was never there.
|
|
2322
2499
|
*/
|
|
2323
|
-
queue: (id, text, model) => {
|
|
2500
|
+
queue: (id, text, model, from) => {
|
|
2324
2501
|
const entry: Bag = {
|
|
2325
2502
|
id,
|
|
2326
2503
|
message: {
|
|
2327
2504
|
text,
|
|
2328
|
-
origin: { kind: 'user' },
|
|
2505
|
+
origin: from?.origin ?? { kind: 'user' },
|
|
2506
|
+
...(from?._meta ? { _meta: from._meta } : {}),
|
|
2329
2507
|
...(model ? { model: { id: model.id, ...(model.config ? { config: model.config } : {}) } } : {}),
|
|
2330
2508
|
},
|
|
2331
2509
|
};
|
|
@@ -2629,6 +2807,8 @@ export function createSession(options: SessionOptions): Session {
|
|
|
2629
2807
|
one.settle({ behavior: 'deny', message: 'The session was disposed' });
|
|
2630
2808
|
}
|
|
2631
2809
|
releaseCalls('The session was disposed');
|
|
2810
|
+
try { rmSync(initScript, { force: true }); }
|
|
2811
|
+
catch { /* a script that was never written */ }
|
|
2632
2812
|
handle.close();
|
|
2633
2813
|
},
|
|
2634
2814
|
};
|
package/src/transcript.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getSessionMessages } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import type { ResponsePart, ToolCallCompletedState, ToolResultContent, Turn } from '@microsoft/agent-host-protocol';
|
|
3
3
|
import type { Bag, OnWire, WireTurn } from '@ahpd/sdk';
|
|
4
|
+
import { toolMetaOf } from './kinds.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Reads a session that already happened, as turns.
|
|
@@ -149,6 +150,7 @@ export async function turnsOf(sessionId: string, dir: string): Promise<WireTurn<
|
|
|
149
150
|
} else if (kind === 'tool_use') {
|
|
150
151
|
const name = str(block.name) ?? 'tool';
|
|
151
152
|
const command = summarize(name, bag(block.input));
|
|
153
|
+
const meta = toolMetaOf(name);
|
|
152
154
|
/*
|
|
153
155
|
* Checked against the state it claims to be in, at the moment it is
|
|
154
156
|
* built.
|
|
@@ -167,6 +169,9 @@ export async function turnsOf(sessionId: string, dir: string): Promise<WireTurn<
|
|
|
167
169
|
// a call still reading `running` would be a spinner that never stops.
|
|
168
170
|
status: 'completed',
|
|
169
171
|
...(command ? { toolInput: command } : {}),
|
|
172
|
+
// The same hint a live call carries, so a transcript read back off
|
|
173
|
+
// disk draws its shell commands as shell commands.
|
|
174
|
+
...(meta ? { _meta: meta } : {}),
|
|
170
175
|
/*
|
|
171
176
|
* Required on a completed call, all four of them, and this builder
|
|
172
177
|
* sent one of them sometimes.
|