@ahpd/agent-claude 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 +56 -0
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +28 -0
- package/dist/catalog.js.map +1 -0
- package/dist/claude.d.ts +28 -0
- package/dist/claude.d.ts.map +1 -0
- package/dist/claude.js +311 -0
- package/dist/claude.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +31 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +78 -0
- package/dist/mcp.js.map +1 -0
- package/dist/probe.d.ts +3 -0
- package/dist/probe.d.ts.map +1 -0
- package/dist/probe.js +102 -0
- package/dist/probe.js.map +1 -0
- package/dist/session.d.ts +70 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +2582 -0
- package/dist/session.js.map +1 -0
- package/dist/transcript.d.ts +11 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +214 -0
- package/dist/transcript.js.map +1 -0
- package/package.json +64 -0
- package/src/catalog.ts +29 -0
- package/src/claude.ts +353 -0
- package/src/index.ts +21 -0
- package/src/mcp.ts +75 -0
- package/src/probe.ts +104 -0
- package/src/session.ts +2635 -0
- package/src/transcript.ts +217 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { getSessionMessages } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { ResponsePart, ToolCallCompletedState, ToolResultContent, Turn } from '@microsoft/agent-host-protocol';
|
|
3
|
+
import type { Bag, OnWire, WireTurn } from '@ahpd/server';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads a session that already happened, as turns.
|
|
7
|
+
*
|
|
8
|
+
* Used for sessions in the catalogue that this host is not running. Opening
|
|
9
|
+
* one costs a file read; no agent process is started until somebody sends a
|
|
10
|
+
* turn to it.
|
|
11
|
+
*
|
|
12
|
+
* The frames are the Claude harness's own, which is what makes this the
|
|
13
|
+
* backend's rather than the host's: a transcript is written by whatever ran
|
|
14
|
+
* the session, and only the thing that ran it knows the shape. Paging over
|
|
15
|
+
* the result is not - that is `paging.ts`, and the host does it to any
|
|
16
|
+
* backend's turns.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const bag = (value: unknown): Bag => (typeof value === 'object' && value !== null ? value as Bag : {});
|
|
20
|
+
const list = (value: unknown): unknown[] => (Array.isArray(value) ? value : []);
|
|
21
|
+
const str = (value: unknown): string | undefined => (typeof value === 'string' ? value : undefined);
|
|
22
|
+
|
|
23
|
+
function summarize(name: string, input: Bag): string | undefined {
|
|
24
|
+
if (name === 'Bash') return str(input.command);
|
|
25
|
+
if (name === 'Read' || name === 'Write' || name === 'Edit') return str(input.file_path);
|
|
26
|
+
if (name === 'Glob' || name === 'Grep') return str(input.pattern);
|
|
27
|
+
if (name === 'Task' || name === 'Agent') return str(input.description);
|
|
28
|
+
return Object.keys(input).length > 0 ? JSON.stringify(input).slice(0, 400) : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resultText(content: unknown): string | undefined {
|
|
32
|
+
if (typeof content === 'string') return content;
|
|
33
|
+
const parts = list(content).map((b) => str(bag(b).text)).filter((t): t is string => t !== undefined);
|
|
34
|
+
return parts.length > 0 ? parts.join('\n') : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read one session's history, best effort.
|
|
39
|
+
*
|
|
40
|
+
* A transcript that will not parse is an empty session, not a refusal: the
|
|
41
|
+
* catalogue said the session exists and the catalogue is right. Refusing to
|
|
42
|
+
* open a row because its file is odd would be the host arguing with itself.
|
|
43
|
+
*/
|
|
44
|
+
export async function turnsOf(sessionId: string, dir: string): Promise<WireTurn<Turn>[]> {
|
|
45
|
+
let messages: unknown[];
|
|
46
|
+
try {
|
|
47
|
+
messages = await getSessionMessages(sessionId, { dir });
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const built: WireTurn<Turn>[] = [];
|
|
53
|
+
const calls = new Map<string, Bag>();
|
|
54
|
+
|
|
55
|
+
for (const entry of messages) {
|
|
56
|
+
const frame = bag(entry);
|
|
57
|
+
const role = str(frame.type);
|
|
58
|
+
const message = bag(frame.message);
|
|
59
|
+
const at = str(frame.timestamp) ?? new Date(0).toISOString();
|
|
60
|
+
|
|
61
|
+
if (role === 'user') {
|
|
62
|
+
const said = typeof message.content === 'string'
|
|
63
|
+
? message.content
|
|
64
|
+
: list(message.content).map((b) => str(bag(b).text)).filter(Boolean).join('\n');
|
|
65
|
+
|
|
66
|
+
// A user frame carrying only tool results is the SDK reporting calls
|
|
67
|
+
// finishing, not somebody saying something. Turning it into a turn puts
|
|
68
|
+
// the agent's own tool output in the person's voice.
|
|
69
|
+
for (const raw of list(message.content)) {
|
|
70
|
+
const block = bag(raw);
|
|
71
|
+
if (str(block.type) !== 'tool_result') continue;
|
|
72
|
+
const call = calls.get(str(block.tool_use_id) ?? '');
|
|
73
|
+
if (!call) continue;
|
|
74
|
+
/*
|
|
75
|
+
* A tool that failed is `completed`, and says so in its result.
|
|
76
|
+
*
|
|
77
|
+
* `ToolCallStatus` has no `failed`: the seven are `streaming`,
|
|
78
|
+
* `pending-confirmation`, `running`, `auth-required`,
|
|
79
|
+
* `pending-result-confirmation`, `completed` and `cancelled`. What
|
|
80
|
+
* went wrong is `success` and `error`, which is the only place a
|
|
81
|
+
* client looks for it. This builder said `failed` and matched no
|
|
82
|
+
* variant at all.
|
|
83
|
+
*/
|
|
84
|
+
const ok = block.is_error !== true;
|
|
85
|
+
call.status = 'completed';
|
|
86
|
+
call.success = ok;
|
|
87
|
+
call.pastTenseMessage = str(call.invocationMessage) ?? str(call.displayName) ?? 'the tool';
|
|
88
|
+
const text = resultText(block.content);
|
|
89
|
+
// `type` on every block: these are MCP's content blocks and it is what
|
|
90
|
+
// tells them apart. Checked, because this is an assignment onto a
|
|
91
|
+
// `Bag` and so outside the literal the call was built as.
|
|
92
|
+
if (text !== undefined)
|
|
93
|
+
call.content = [{ type: 'text', text }] satisfies OnWire<ToolResultContent>[];
|
|
94
|
+
if (!ok) call.error = { message: text ?? 'The tool failed' };
|
|
95
|
+
}
|
|
96
|
+
if (!said) continue;
|
|
97
|
+
|
|
98
|
+
built.push({
|
|
99
|
+
id: str(frame.uuid) ?? `u${built.length}`,
|
|
100
|
+
startedAt: at,
|
|
101
|
+
// Who produced it, which `Message` requires and this never sent.
|
|
102
|
+
message: { text: said, origin: { kind: 'user' } },
|
|
103
|
+
responseParts: [],
|
|
104
|
+
// A turn out of a transcript is one that already happened, so it is
|
|
105
|
+
// complete by definition. `Turn.state` is required and used to be
|
|
106
|
+
// left off, which put every past turn on the wire without one.
|
|
107
|
+
state: 'complete',
|
|
108
|
+
// Required too, and meaning "not measured" rather than "none": the
|
|
109
|
+
// transcript does not record token counts.
|
|
110
|
+
usage: undefined,
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (role !== 'assistant') continue;
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* What the turn cost and which model answered it, off the transcript
|
|
119
|
+
* rather than left out.
|
|
120
|
+
*
|
|
121
|
+
* `Turn.usage` is required, and a rebuilt turn used to carry none at all -
|
|
122
|
+
* so a client could not name the model on a past turn or size the context
|
|
123
|
+
* window that turn used. The transcript records both on every assistant
|
|
124
|
+
* frame; this is the same mapping a live turn does, from the same fields.
|
|
125
|
+
*/
|
|
126
|
+
const counted = bag(message.usage);
|
|
127
|
+
const count = (value: unknown): number | undefined => (typeof value === 'number' ? value : undefined);
|
|
128
|
+
const spent: Bag = {
|
|
129
|
+
...(count(counted.input_tokens) !== undefined ? { inputTokens: count(counted.input_tokens) } : {}),
|
|
130
|
+
...(count(counted.output_tokens) !== undefined ? { outputTokens: count(counted.output_tokens) } : {}),
|
|
131
|
+
...(count(counted.cache_read_input_tokens) !== undefined
|
|
132
|
+
? { cacheReadTokens: count(counted.cache_read_input_tokens) }
|
|
133
|
+
: {}),
|
|
134
|
+
...(str(message.model) !== undefined ? { model: str(message.model) as string } : {}),
|
|
135
|
+
};
|
|
136
|
+
const used = Object.keys(spent).length > 0 ? spent : undefined;
|
|
137
|
+
|
|
138
|
+
const parts: Bag[] = [];
|
|
139
|
+
const blocks = list(message.content);
|
|
140
|
+
for (let index = 0; index < blocks.length; index++) {
|
|
141
|
+
const block = bag(blocks[index]);
|
|
142
|
+
const kind = str(block.type);
|
|
143
|
+
const id = str(block.id) ?? `${str(frame.uuid) ?? 'a'}:${index}`;
|
|
144
|
+
|
|
145
|
+
if (kind === 'text') {
|
|
146
|
+
parts.push({ id, kind: 'markdown', content: str(block.text) ?? '' } satisfies OnWire<ResponsePart>);
|
|
147
|
+
} else if (kind === 'thinking') {
|
|
148
|
+
parts.push({ id, kind: 'reasoning', content: str(block.thinking) ?? '' } satisfies OnWire<ResponsePart>);
|
|
149
|
+
} else if (kind === 'tool_use') {
|
|
150
|
+
const name = str(block.name) ?? 'tool';
|
|
151
|
+
const command = summarize(name, bag(block.input));
|
|
152
|
+
/*
|
|
153
|
+
* Checked against the state it claims to be in, at the moment it is
|
|
154
|
+
* built.
|
|
155
|
+
*
|
|
156
|
+
* This is the gap `WireTurn` was named for. Everything inside
|
|
157
|
+
* `responseParts` was a `Bag`, and it is where this builder wrote a
|
|
158
|
+
* `status` that is not one of the seven, left off three fields the
|
|
159
|
+
* completed state requires, and gave its content blocks no `type` -
|
|
160
|
+
* four defects in one object, none of them a compile error.
|
|
161
|
+
*/
|
|
162
|
+
const call: Bag = {
|
|
163
|
+
toolCallId: id,
|
|
164
|
+
toolName: name,
|
|
165
|
+
displayName: name,
|
|
166
|
+
// Completed unless a result says otherwise: the session is over, so
|
|
167
|
+
// a call still reading `running` would be a spinner that never stops.
|
|
168
|
+
status: 'completed',
|
|
169
|
+
...(command ? { toolInput: command } : {}),
|
|
170
|
+
/*
|
|
171
|
+
* Required on a completed call, all four of them, and this builder
|
|
172
|
+
* sent one of them sometimes.
|
|
173
|
+
*
|
|
174
|
+
* `invocationMessage` is the sentence the row draws; without it
|
|
175
|
+
* there is nothing to draw. `confirmed` says nothing is being asked,
|
|
176
|
+
* and without it a client reads every call in the transcript as a
|
|
177
|
+
* question waiting on somebody. `success` and `pastTenseMessage`
|
|
178
|
+
* stand until a result says otherwise - a call with no result
|
|
179
|
+
* recorded is one that finished with nothing to report, not one
|
|
180
|
+
* that failed.
|
|
181
|
+
*/
|
|
182
|
+
invocationMessage: command ?? name,
|
|
183
|
+
confirmed: 'not-needed',
|
|
184
|
+
success: true,
|
|
185
|
+
pastTenseMessage: command ?? name,
|
|
186
|
+
} satisfies OnWire<ToolCallCompletedState>;
|
|
187
|
+
calls.set(id, call);
|
|
188
|
+
// The part is not re-checked: `call` is a `Bag` from here on, because
|
|
189
|
+
// a tool result arriving later mutates it. The literal above is what
|
|
190
|
+
// the protocol changes under, and the literal is what is checked.
|
|
191
|
+
parts.push({ id, kind: 'toolCall', toolCall: call });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (parts.length === 0) continue;
|
|
195
|
+
|
|
196
|
+
// The agent answering the message just above it, if that is what this is.
|
|
197
|
+
// A history where every reply is its own turn reads as a monologue with
|
|
198
|
+
// the questions removed.
|
|
199
|
+
const previous = built[built.length - 1];
|
|
200
|
+
if (previous && (previous.responseParts as Bag[]).length === 0) {
|
|
201
|
+
previous.responseParts = parts;
|
|
202
|
+
previous.usage = used as WireTurn<Turn>['usage'];
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
built.push({
|
|
206
|
+
id: str(frame.uuid) ?? `a${built.length}`,
|
|
207
|
+
startedAt: at,
|
|
208
|
+
// The agent's own turn: there is no user message in front of it.
|
|
209
|
+
message: { text: '', origin: { kind: 'agent' } },
|
|
210
|
+
responseParts: parts,
|
|
211
|
+
state: 'complete',
|
|
212
|
+
usage: used as WireTurn<Turn>['usage'],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return built;
|
|
217
|
+
}
|