claude-code-rust 0.12.1 → 0.12.3
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 +3 -7
- package/agent-sdk/README.md +1 -1
- package/agent-sdk/dist/bridge/account_metadata.js +44 -0
- package/agent-sdk/dist/bridge/available_commands.js +129 -0
- package/agent-sdk/dist/bridge/commands.js +58 -36
- package/agent-sdk/dist/bridge/error_classification.js +20 -7
- package/agent-sdk/dist/bridge/events.js +18 -0
- package/agent-sdk/dist/bridge/history.js +183 -11
- package/agent-sdk/dist/bridge/logger.js +3 -0
- package/agent-sdk/dist/bridge/mcp.js +49 -79
- package/agent-sdk/dist/bridge/mcp_metadata.js +369 -0
- package/agent-sdk/dist/bridge/message_handlers.js +401 -57
- package/agent-sdk/dist/bridge/model_metadata.js +228 -0
- package/agent-sdk/dist/bridge/session_lifecycle.js +197 -326
- package/agent-sdk/dist/bridge/state_parsing.js +7 -1
- package/agent-sdk/dist/bridge/task_links.js +34 -0
- package/agent-sdk/dist/bridge/tasks.js +862 -0
- package/agent-sdk/dist/bridge/tool_calls.js +88 -31
- package/agent-sdk/dist/bridge/tooling.js +1278 -42
- package/agent-sdk/dist/bridge.js +96 -44
- package/agent-sdk/dist/bridge.test.js +3691 -252
- package/package.json +8 -3
- package/scripts/jscpd-warning-summary.mjs +132 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
import { emitSessionUpdate } from "./events.js";
|
|
2
|
+
import { asRecordOrNull } from "./shared.js";
|
|
3
|
+
import { linkTaskToolUse, unlinkTaskToolUse } from "./task_links.js";
|
|
4
|
+
const TASK_TOOL_NAMES = new Set([
|
|
5
|
+
"TaskCreate",
|
|
6
|
+
"TaskUpdate",
|
|
7
|
+
"TaskGet",
|
|
8
|
+
"TaskList",
|
|
9
|
+
"TaskOutput",
|
|
10
|
+
"TaskStop",
|
|
11
|
+
]);
|
|
12
|
+
export function isTaskToolName(name) {
|
|
13
|
+
return TASK_TOOL_NAMES.has(name);
|
|
14
|
+
}
|
|
15
|
+
export function taskToolTitle(name, input, context = {}) {
|
|
16
|
+
if (name === "TaskCreate") {
|
|
17
|
+
const subject = typeof input.subject === "string" ? input.subject : "";
|
|
18
|
+
return subject ? `Create task: ${subject}` : "Create task";
|
|
19
|
+
}
|
|
20
|
+
if (name === "TaskUpdate") {
|
|
21
|
+
const subject = typeof input.subject === "string" ? input.subject : "";
|
|
22
|
+
const taskId = typeof input.taskId === "string" ? input.taskId : "";
|
|
23
|
+
const label = subject || context.taskSubject || taskId;
|
|
24
|
+
return label ? `Update task: ${label}` : "Update task";
|
|
25
|
+
}
|
|
26
|
+
if (name === "TaskGet") {
|
|
27
|
+
const taskId = typeof input.taskId === "string" ? input.taskId : "";
|
|
28
|
+
return taskId ? `Get task: ${taskId}` : "Get task";
|
|
29
|
+
}
|
|
30
|
+
if (name === "TaskList") {
|
|
31
|
+
return "List tasks";
|
|
32
|
+
}
|
|
33
|
+
if (name === "TaskOutput") {
|
|
34
|
+
const taskId = nonEmptyString(input.task_id) ?? "";
|
|
35
|
+
const label = context.taskSubject || taskId;
|
|
36
|
+
return label ? `Task output: ${label}` : "Task output";
|
|
37
|
+
}
|
|
38
|
+
if (name === "TaskStop") {
|
|
39
|
+
const taskId = nonEmptyString(input.task_id) ?? nonEmptyString(input.shell_id) ?? "";
|
|
40
|
+
const label = context.taskSubject || taskId;
|
|
41
|
+
return label ? `Stop task: ${label}` : "Stop task";
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function cloneTask(task) {
|
|
46
|
+
return {
|
|
47
|
+
...task,
|
|
48
|
+
blocks: [...task.blocks],
|
|
49
|
+
blocked_by: [...task.blocked_by],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function jsonValue(value) {
|
|
53
|
+
if (value === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const text = JSON.stringify(value);
|
|
58
|
+
if (text === undefined) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return JSON.parse(text);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function jsonRecord(value) {
|
|
68
|
+
const json = jsonValue(value);
|
|
69
|
+
return json && typeof json === "object" && !Array.isArray(json)
|
|
70
|
+
? json
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
function nonEmptyString(value) {
|
|
74
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
75
|
+
}
|
|
76
|
+
function stringArray(value) {
|
|
77
|
+
if (!Array.isArray(value)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return value.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
81
|
+
}
|
|
82
|
+
function uniqueStrings(values) {
|
|
83
|
+
return [...new Set(values)];
|
|
84
|
+
}
|
|
85
|
+
function normalizeTaskStatus(value) {
|
|
86
|
+
switch (value) {
|
|
87
|
+
case "pending":
|
|
88
|
+
return "pending";
|
|
89
|
+
case "running":
|
|
90
|
+
case "in_progress":
|
|
91
|
+
return "in_progress";
|
|
92
|
+
case "completed":
|
|
93
|
+
return "completed";
|
|
94
|
+
default:
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function normalizeLifecycleTaskStatus(value) {
|
|
99
|
+
const status = normalizeTaskStatus(value);
|
|
100
|
+
if (status) {
|
|
101
|
+
return status;
|
|
102
|
+
}
|
|
103
|
+
switch (value) {
|
|
104
|
+
case "failed":
|
|
105
|
+
case "killed":
|
|
106
|
+
case "stopped":
|
|
107
|
+
return "completed";
|
|
108
|
+
default:
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function toolNameFromToolCall(base) {
|
|
113
|
+
const meta = asRecordOrNull(base?.meta);
|
|
114
|
+
const claudeCode = asRecordOrNull(meta?.claudeCode);
|
|
115
|
+
return nonEmptyString(claudeCode?.toolName) ?? "";
|
|
116
|
+
}
|
|
117
|
+
function inputRecord(base) {
|
|
118
|
+
return asRecordOrNull(base?.raw_input) ?? {};
|
|
119
|
+
}
|
|
120
|
+
function extractText(value) {
|
|
121
|
+
if (typeof value === "string") {
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
return value
|
|
126
|
+
.map((entry) => {
|
|
127
|
+
if (typeof entry === "string") {
|
|
128
|
+
return entry;
|
|
129
|
+
}
|
|
130
|
+
const record = asRecordOrNull(entry);
|
|
131
|
+
return typeof record?.text === "string" ? record.text : "";
|
|
132
|
+
})
|
|
133
|
+
.filter((part) => part.length > 0)
|
|
134
|
+
.join("\n");
|
|
135
|
+
}
|
|
136
|
+
const record = asRecordOrNull(value);
|
|
137
|
+
return typeof record?.text === "string" ? record.text : "";
|
|
138
|
+
}
|
|
139
|
+
function visitCandidate(value, records, depth = 0) {
|
|
140
|
+
if (value === undefined || value === null || depth > 6) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (typeof value === "string") {
|
|
144
|
+
const trimmed = value.trim();
|
|
145
|
+
if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
visitCandidate(JSON.parse(trimmed), records, depth + 1);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (Array.isArray(value)) {
|
|
157
|
+
for (const entry of value) {
|
|
158
|
+
visitCandidate(entry, records, depth + 1);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const record = asRecordOrNull(value);
|
|
163
|
+
if (!record) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
records.push(record);
|
|
167
|
+
for (const key of ["result", "data", "content"]) {
|
|
168
|
+
visitCandidate(record[key], records, depth + 1);
|
|
169
|
+
}
|
|
170
|
+
if (typeof record.text === "string") {
|
|
171
|
+
visitCandidate(record.text, records, depth + 1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function resultCandidates(rawResult, rawContent) {
|
|
175
|
+
const records = [];
|
|
176
|
+
visitCandidate(rawResult, records);
|
|
177
|
+
visitCandidate(rawContent, records);
|
|
178
|
+
const text = extractText(rawContent);
|
|
179
|
+
if (text) {
|
|
180
|
+
visitCandidate(text, records);
|
|
181
|
+
}
|
|
182
|
+
return records;
|
|
183
|
+
}
|
|
184
|
+
function normalizeFieldKey(key) {
|
|
185
|
+
return key.replace(/[^a-zA-Z0-9]+/g, "").toLowerCase();
|
|
186
|
+
}
|
|
187
|
+
function humanizeFieldLabel(key) {
|
|
188
|
+
const spaced = key
|
|
189
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
190
|
+
.replace(/[_-]+/g, " ")
|
|
191
|
+
.trim()
|
|
192
|
+
.toLowerCase();
|
|
193
|
+
if (!spaced) {
|
|
194
|
+
return "";
|
|
195
|
+
}
|
|
196
|
+
return spaced
|
|
197
|
+
.split(/\s+/)
|
|
198
|
+
.map((word, index) => {
|
|
199
|
+
if (word === "id") {
|
|
200
|
+
return "ID";
|
|
201
|
+
}
|
|
202
|
+
return index === 0 ? `${word.charAt(0).toUpperCase()}${word.slice(1)}` : word;
|
|
203
|
+
})
|
|
204
|
+
.join(" ");
|
|
205
|
+
}
|
|
206
|
+
function displayScalarValue(value) {
|
|
207
|
+
if (typeof value === "string") {
|
|
208
|
+
const trimmed = value.trim();
|
|
209
|
+
if (!trimmed) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
return /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/u.test(trimmed) ? trimmed.replace(/_/g, " ") : trimmed;
|
|
213
|
+
}
|
|
214
|
+
if (typeof value === "boolean") {
|
|
215
|
+
return value ? "yes" : "no";
|
|
216
|
+
}
|
|
217
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
218
|
+
return `${value}`;
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
function decodeXmlText(value) {
|
|
223
|
+
return value.replace(/&(?:#(\d+)|#x([0-9a-fA-F]+)|amp|lt|gt|quot|apos);/g, (match, dec, hex) => {
|
|
224
|
+
if (typeof dec === "string" && dec.length > 0) {
|
|
225
|
+
const codePoint = Number.parseInt(dec, 10);
|
|
226
|
+
return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff
|
|
227
|
+
? String.fromCodePoint(codePoint)
|
|
228
|
+
: match;
|
|
229
|
+
}
|
|
230
|
+
if (typeof hex === "string" && hex.length > 0) {
|
|
231
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
232
|
+
return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff
|
|
233
|
+
? String.fromCodePoint(codePoint)
|
|
234
|
+
: match;
|
|
235
|
+
}
|
|
236
|
+
switch (match) {
|
|
237
|
+
case "&":
|
|
238
|
+
return "&";
|
|
239
|
+
case "<":
|
|
240
|
+
return "<";
|
|
241
|
+
case ">":
|
|
242
|
+
return ">";
|
|
243
|
+
case """:
|
|
244
|
+
return "\"";
|
|
245
|
+
case "'":
|
|
246
|
+
return "'";
|
|
247
|
+
default:
|
|
248
|
+
return match;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function xmlLeafFields(text) {
|
|
253
|
+
const fields = [];
|
|
254
|
+
if (text.length > 20_000 || !text.includes("<")) {
|
|
255
|
+
return fields;
|
|
256
|
+
}
|
|
257
|
+
const pattern = /<([A-Za-z][\w.-]{0,79})>([\s\S]*?)<\/\1>/gu;
|
|
258
|
+
for (const match of text.matchAll(pattern)) {
|
|
259
|
+
if (fields.length >= 100) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
const [, key, rawValue] = match;
|
|
263
|
+
if (!key || rawValue === undefined || /<[A-Za-z][\w.-]{0,79}[\s>]/u.test(rawValue)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const value = decodeXmlText(rawValue).trim();
|
|
267
|
+
if (value) {
|
|
268
|
+
fields.push([key, value]);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return fields;
|
|
272
|
+
}
|
|
273
|
+
function firstTaskRecord(candidates) {
|
|
274
|
+
for (const candidate of candidates) {
|
|
275
|
+
const nestedTask = asRecordOrNull(candidate.task);
|
|
276
|
+
if (nestedTask && typeof nestedTask.id === "string") {
|
|
277
|
+
return nestedTask;
|
|
278
|
+
}
|
|
279
|
+
if (typeof candidate.id === "string" && typeof candidate.subject === "string") {
|
|
280
|
+
return candidate;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
function firstRecordWithTaskProperty(candidates) {
|
|
286
|
+
return candidates.find((candidate) => Object.hasOwn(candidate, "task"));
|
|
287
|
+
}
|
|
288
|
+
function firstTaskUpdateOutput(candidates) {
|
|
289
|
+
return candidates.find((candidate) => typeof candidate.success === "boolean" && typeof candidate.taskId === "string");
|
|
290
|
+
}
|
|
291
|
+
function firstTaskListOutput(candidates) {
|
|
292
|
+
return candidates.find((candidate) => Array.isArray(candidate.tasks));
|
|
293
|
+
}
|
|
294
|
+
function firstTaskStopOutput(candidates) {
|
|
295
|
+
return candidates.find((candidate) => typeof candidate.message === "string" &&
|
|
296
|
+
typeof candidate.task_id === "string" &&
|
|
297
|
+
typeof candidate.task_type === "string");
|
|
298
|
+
}
|
|
299
|
+
function taskOutputResultText(rawResult, rawContent, rawInput) {
|
|
300
|
+
const lines = [];
|
|
301
|
+
const seen = new Set();
|
|
302
|
+
const input = asRecordOrNull(rawInput);
|
|
303
|
+
const markSeen = (key, value) => {
|
|
304
|
+
seen.add(`${normalizeFieldKey(key)}\0${value}`);
|
|
305
|
+
};
|
|
306
|
+
const pushField = (key, value) => {
|
|
307
|
+
const label = humanizeFieldLabel(key);
|
|
308
|
+
const displayValue = displayScalarValue(value);
|
|
309
|
+
if (!label || displayValue === undefined) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const seenKey = `${normalizeFieldKey(key)}\0${displayValue}`;
|
|
313
|
+
if (seen.has(seenKey)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
seen.add(seenKey);
|
|
317
|
+
lines.push(`${label}: ${displayValue}`);
|
|
318
|
+
};
|
|
319
|
+
const inputTaskId = displayScalarValue(input?.task_id);
|
|
320
|
+
if (inputTaskId) {
|
|
321
|
+
markSeen("task_id", inputTaskId);
|
|
322
|
+
}
|
|
323
|
+
const candidates = resultCandidates(rawResult, undefined);
|
|
324
|
+
for (const candidate of candidates) {
|
|
325
|
+
if (Object.hasOwn(candidate, "retrieval_status")) {
|
|
326
|
+
pushField("retrieval_status", candidate.retrieval_status);
|
|
327
|
+
}
|
|
328
|
+
const task = asRecordOrNull(candidate.task);
|
|
329
|
+
if (task) {
|
|
330
|
+
for (const [key, value] of Object.entries(task)) {
|
|
331
|
+
pushField(key, value);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!task &&
|
|
335
|
+
(Object.hasOwn(candidate, "task_id") ||
|
|
336
|
+
Object.hasOwn(candidate, "task_type") ||
|
|
337
|
+
Object.hasOwn(candidate, "status"))) {
|
|
338
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
339
|
+
pushField(key, value);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const rawText = extractText(rawContent);
|
|
344
|
+
for (const [key, value] of xmlLeafFields(rawText)) {
|
|
345
|
+
pushField(key, value);
|
|
346
|
+
}
|
|
347
|
+
return lines.join("\n") || rawText.trim() || extractText(rawResult).trim();
|
|
348
|
+
}
|
|
349
|
+
function upsertTask(session, patch) {
|
|
350
|
+
const existing = session.tasksById.get(patch.task_id);
|
|
351
|
+
const task = {
|
|
352
|
+
task_id: patch.task_id,
|
|
353
|
+
subject: patch.subject ?? existing?.subject ?? patch.task_id,
|
|
354
|
+
status: patch.status ?? existing?.status ?? "pending",
|
|
355
|
+
blocks: patch.blocks ?? existing?.blocks ?? [],
|
|
356
|
+
blocked_by: patch.blocked_by ?? existing?.blocked_by ?? [],
|
|
357
|
+
};
|
|
358
|
+
if (patch.description !== undefined) {
|
|
359
|
+
task.description = patch.description;
|
|
360
|
+
}
|
|
361
|
+
else if (existing?.description !== undefined) {
|
|
362
|
+
task.description = existing.description;
|
|
363
|
+
}
|
|
364
|
+
if (patch.active_form !== undefined) {
|
|
365
|
+
task.active_form = patch.active_form;
|
|
366
|
+
}
|
|
367
|
+
else if (existing?.active_form !== undefined) {
|
|
368
|
+
task.active_form = existing.active_form;
|
|
369
|
+
}
|
|
370
|
+
if (patch.owner !== undefined) {
|
|
371
|
+
task.owner = patch.owner;
|
|
372
|
+
}
|
|
373
|
+
else if (existing?.owner !== undefined) {
|
|
374
|
+
task.owner = existing.owner;
|
|
375
|
+
}
|
|
376
|
+
if (patch.metadata !== undefined) {
|
|
377
|
+
task.metadata = patch.metadata;
|
|
378
|
+
}
|
|
379
|
+
else if (existing?.metadata !== undefined) {
|
|
380
|
+
task.metadata = existing.metadata;
|
|
381
|
+
}
|
|
382
|
+
if (patch.source_tool_call_id !== undefined) {
|
|
383
|
+
task.source_tool_call_id = patch.source_tool_call_id;
|
|
384
|
+
}
|
|
385
|
+
else if (existing?.source_tool_call_id !== undefined) {
|
|
386
|
+
task.source_tool_call_id = existing.source_tool_call_id;
|
|
387
|
+
}
|
|
388
|
+
session.tasksById.set(task.task_id, task);
|
|
389
|
+
if (!existing && !session.taskOrder.includes(task.task_id)) {
|
|
390
|
+
session.taskOrder.push(task.task_id);
|
|
391
|
+
}
|
|
392
|
+
return cloneTask(task);
|
|
393
|
+
}
|
|
394
|
+
function removeTasks(session, taskIds) {
|
|
395
|
+
const removed = [];
|
|
396
|
+
for (const taskId of uniqueStrings(taskIds)) {
|
|
397
|
+
if (session.tasksById.delete(taskId)) {
|
|
398
|
+
removed.push(taskId);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
removed.push(taskId);
|
|
402
|
+
}
|
|
403
|
+
unlinkTaskToolUse(session, taskId);
|
|
404
|
+
}
|
|
405
|
+
if (removed.length > 0) {
|
|
406
|
+
const removedSet = new Set(removed);
|
|
407
|
+
session.taskOrder = session.taskOrder.filter((taskId) => !removedSet.has(taskId));
|
|
408
|
+
}
|
|
409
|
+
return removed;
|
|
410
|
+
}
|
|
411
|
+
function orderedTasks(session) {
|
|
412
|
+
return session.taskOrder
|
|
413
|
+
.map((taskId) => session.tasksById.get(taskId))
|
|
414
|
+
.filter((task) => Boolean(task))
|
|
415
|
+
.map(cloneTask);
|
|
416
|
+
}
|
|
417
|
+
function emitTaskStateUpdate(session, source, tasks, removedTaskIds = [], isCompleteSnapshot = false) {
|
|
418
|
+
emitSessionUpdate(session.sessionId, {
|
|
419
|
+
type: "task_state_update",
|
|
420
|
+
source,
|
|
421
|
+
tasks: tasks.map(cloneTask),
|
|
422
|
+
removed_task_ids: uniqueStrings(removedTaskIds),
|
|
423
|
+
is_complete_snapshot: isCompleteSnapshot,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function mergeMetadata(existing, patch) {
|
|
427
|
+
if (!patch) {
|
|
428
|
+
return existing;
|
|
429
|
+
}
|
|
430
|
+
const existingRecord = existing && typeof existing === "object" && !Array.isArray(existing)
|
|
431
|
+
? { ...existing }
|
|
432
|
+
: {};
|
|
433
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
434
|
+
if (value === null) {
|
|
435
|
+
delete existingRecord[key];
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
existingRecord[key] = value;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return Object.keys(existingRecord).length > 0 ? existingRecord : null;
|
|
442
|
+
}
|
|
443
|
+
function taskCreatePatch(taskRecord, input, toolUseId) {
|
|
444
|
+
const taskId = nonEmptyString(taskRecord.id);
|
|
445
|
+
if (!taskId) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
const metadata = jsonValue(input.metadata);
|
|
449
|
+
return {
|
|
450
|
+
task_id: taskId,
|
|
451
|
+
subject: nonEmptyString(taskRecord.subject) ?? nonEmptyString(input.subject) ?? taskId,
|
|
452
|
+
description: nonEmptyString(input.description),
|
|
453
|
+
active_form: nonEmptyString(input.activeForm),
|
|
454
|
+
status: "pending",
|
|
455
|
+
blocks: [],
|
|
456
|
+
blocked_by: [],
|
|
457
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
458
|
+
source_tool_call_id: toolUseId,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function taskUpdatePatch(session, taskId, input, output) {
|
|
462
|
+
const existing = session.tasksById.get(taskId);
|
|
463
|
+
const metadata = mergeMetadata(existing?.metadata, jsonRecord(input.metadata));
|
|
464
|
+
const status = normalizeTaskStatus(input.status) ?? normalizeTaskStatus(asRecordOrNull(output.statusChange)?.to);
|
|
465
|
+
const addBlocks = stringArray(input.addBlocks);
|
|
466
|
+
const addBlockedBy = stringArray(input.addBlockedBy);
|
|
467
|
+
const patch = {
|
|
468
|
+
task_id: taskId,
|
|
469
|
+
subject: nonEmptyString(input.subject),
|
|
470
|
+
description: nonEmptyString(input.description),
|
|
471
|
+
active_form: nonEmptyString(input.activeForm),
|
|
472
|
+
status,
|
|
473
|
+
owner: nonEmptyString(input.owner),
|
|
474
|
+
blocks: addBlocks.length > 0 ? uniqueStrings([...(existing?.blocks ?? []), ...addBlocks]) : undefined,
|
|
475
|
+
blocked_by: addBlockedBy.length > 0
|
|
476
|
+
? uniqueStrings([...(existing?.blocked_by ?? []), ...addBlockedBy])
|
|
477
|
+
: undefined,
|
|
478
|
+
metadata,
|
|
479
|
+
};
|
|
480
|
+
if (Object.hasOwn(input, "metadata") && metadata === undefined) {
|
|
481
|
+
patch.metadata = undefined;
|
|
482
|
+
}
|
|
483
|
+
return patch;
|
|
484
|
+
}
|
|
485
|
+
function taskGetPatch(taskRecord, existing) {
|
|
486
|
+
const taskId = nonEmptyString(taskRecord.id);
|
|
487
|
+
const subject = nonEmptyString(taskRecord.subject);
|
|
488
|
+
const status = normalizeTaskStatus(taskRecord.status);
|
|
489
|
+
if (!taskId || !subject || !status) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
task_id: taskId,
|
|
494
|
+
subject,
|
|
495
|
+
description: nonEmptyString(taskRecord.description),
|
|
496
|
+
active_form: existing?.active_form,
|
|
497
|
+
status,
|
|
498
|
+
owner: existing?.owner,
|
|
499
|
+
blocks: stringArray(taskRecord.blocks),
|
|
500
|
+
blocked_by: stringArray(taskRecord.blockedBy),
|
|
501
|
+
metadata: existing?.metadata,
|
|
502
|
+
source_tool_call_id: existing?.source_tool_call_id,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function taskListPatch(taskRecord, existing) {
|
|
506
|
+
const taskId = nonEmptyString(taskRecord.id);
|
|
507
|
+
const subject = nonEmptyString(taskRecord.subject);
|
|
508
|
+
const status = normalizeTaskStatus(taskRecord.status);
|
|
509
|
+
if (!taskId || !subject || !status) {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
task_id: taskId,
|
|
514
|
+
subject,
|
|
515
|
+
description: existing?.description,
|
|
516
|
+
active_form: existing?.active_form,
|
|
517
|
+
status,
|
|
518
|
+
owner: nonEmptyString(taskRecord.owner) ?? existing?.owner,
|
|
519
|
+
blocks: existing?.blocks ?? [],
|
|
520
|
+
blocked_by: stringArray(taskRecord.blockedBy),
|
|
521
|
+
metadata: existing?.metadata,
|
|
522
|
+
source_tool_call_id: existing?.source_tool_call_id,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function replaceTaskSnapshot(session, patches) {
|
|
526
|
+
const previousIds = session.taskOrder;
|
|
527
|
+
const nextIds = [];
|
|
528
|
+
const nextTasks = new Map();
|
|
529
|
+
const emitted = [];
|
|
530
|
+
for (const patch of patches) {
|
|
531
|
+
if (nextTasks.has(patch.task_id)) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const existing = session.tasksById.get(patch.task_id);
|
|
535
|
+
const task = upsertFromExisting(existing, patch);
|
|
536
|
+
nextTasks.set(task.task_id, task);
|
|
537
|
+
nextIds.push(task.task_id);
|
|
538
|
+
emitted.push(cloneTask(task));
|
|
539
|
+
}
|
|
540
|
+
const retained = new Set(nextIds);
|
|
541
|
+
const removedTaskIds = previousIds.filter((taskId) => !retained.has(taskId));
|
|
542
|
+
for (const taskId of removedTaskIds) {
|
|
543
|
+
unlinkTaskToolUse(session, taskId);
|
|
544
|
+
}
|
|
545
|
+
session.tasksById = nextTasks;
|
|
546
|
+
session.taskOrder = nextIds;
|
|
547
|
+
return { tasks: emitted, removedTaskIds };
|
|
548
|
+
}
|
|
549
|
+
function upsertFromExisting(existing, patch) {
|
|
550
|
+
return {
|
|
551
|
+
task_id: patch.task_id,
|
|
552
|
+
subject: patch.subject ?? existing?.subject ?? patch.task_id,
|
|
553
|
+
description: patch.description ?? existing?.description,
|
|
554
|
+
active_form: patch.active_form ?? existing?.active_form,
|
|
555
|
+
status: patch.status ?? existing?.status ?? "pending",
|
|
556
|
+
owner: patch.owner ?? existing?.owner,
|
|
557
|
+
blocks: patch.blocks ?? existing?.blocks ?? [],
|
|
558
|
+
blocked_by: patch.blocked_by ?? existing?.blocked_by ?? [],
|
|
559
|
+
metadata: patch.metadata ?? existing?.metadata,
|
|
560
|
+
source_tool_call_id: patch.source_tool_call_id ?? existing?.source_tool_call_id,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function taskStatusMarker(status) {
|
|
564
|
+
switch (status) {
|
|
565
|
+
case "completed":
|
|
566
|
+
return "■";
|
|
567
|
+
case "in_progress":
|
|
568
|
+
case "running":
|
|
569
|
+
return "▣";
|
|
570
|
+
default:
|
|
571
|
+
return "□";
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function taskRecordLine(record) {
|
|
575
|
+
const subject = typeof record.subject === "string" && record.subject.trim() ? record.subject : "Task";
|
|
576
|
+
return `${taskStatusMarker(record.status)} ${subject}`;
|
|
577
|
+
}
|
|
578
|
+
function taskListWindow(lines) {
|
|
579
|
+
if (lines.length <= 9) {
|
|
580
|
+
return lines;
|
|
581
|
+
}
|
|
582
|
+
const firstUnfinished = lines.findIndex((line) => !line.startsWith("■ "));
|
|
583
|
+
const anchor = firstUnfinished >= 0 ? firstUnfinished : lines.length - 1;
|
|
584
|
+
const start = Math.min(Math.max(anchor - 4, 0), Math.max(lines.length - 9, 0));
|
|
585
|
+
const end = Math.min(start + 9, lines.length);
|
|
586
|
+
const visible = lines.slice(start, end);
|
|
587
|
+
if (start > 0) {
|
|
588
|
+
visible[0] = "...";
|
|
589
|
+
}
|
|
590
|
+
if (end < lines.length) {
|
|
591
|
+
visible[visible.length - 1] = "...";
|
|
592
|
+
}
|
|
593
|
+
return visible;
|
|
594
|
+
}
|
|
595
|
+
export function taskToolResultText(toolName, rawResult, rawContent, rawInput) {
|
|
596
|
+
const candidates = resultCandidates(rawResult, rawContent);
|
|
597
|
+
if (toolName === "TaskCreate") {
|
|
598
|
+
return "";
|
|
599
|
+
}
|
|
600
|
+
if (toolName === "TaskUpdate") {
|
|
601
|
+
const output = firstTaskUpdateOutput(candidates);
|
|
602
|
+
if (!output) {
|
|
603
|
+
return "";
|
|
604
|
+
}
|
|
605
|
+
if (output.success !== true) {
|
|
606
|
+
const error = typeof output.error === "string" && output.error.trim() ? output.error : "Task update failed";
|
|
607
|
+
return error;
|
|
608
|
+
}
|
|
609
|
+
return "";
|
|
610
|
+
}
|
|
611
|
+
if (toolName === "TaskGet") {
|
|
612
|
+
const output = firstRecordWithTaskProperty(candidates);
|
|
613
|
+
if (!output) {
|
|
614
|
+
return "";
|
|
615
|
+
}
|
|
616
|
+
const task = asRecordOrNull(output.task);
|
|
617
|
+
if (!task) {
|
|
618
|
+
return "Task not found";
|
|
619
|
+
}
|
|
620
|
+
const lines = [taskRecordLine(task)];
|
|
621
|
+
if (typeof task.description === "string" && task.description.trim()) {
|
|
622
|
+
lines.push(task.description);
|
|
623
|
+
}
|
|
624
|
+
const blockedBy = Array.isArray(task.blockedBy)
|
|
625
|
+
? task.blockedBy.filter((entry) => typeof entry === "string")
|
|
626
|
+
: [];
|
|
627
|
+
if (blockedBy.length > 0) {
|
|
628
|
+
lines.push(`Blocked by: ${blockedBy.join(", ")}`);
|
|
629
|
+
}
|
|
630
|
+
return lines.join("\n");
|
|
631
|
+
}
|
|
632
|
+
if (toolName === "TaskList") {
|
|
633
|
+
const output = firstTaskListOutput(candidates);
|
|
634
|
+
if (!output || !Array.isArray(output.tasks)) {
|
|
635
|
+
return "";
|
|
636
|
+
}
|
|
637
|
+
const lines = output.tasks
|
|
638
|
+
.map((entry) => asRecordOrNull(entry))
|
|
639
|
+
.filter((entry) => Boolean(entry))
|
|
640
|
+
.map(taskRecordLine);
|
|
641
|
+
return lines.length > 0 ? taskListWindow(lines).join("\n") : "No tasks";
|
|
642
|
+
}
|
|
643
|
+
if (toolName === "TaskOutput") {
|
|
644
|
+
return taskOutputResultText(rawResult, rawContent, rawInput);
|
|
645
|
+
}
|
|
646
|
+
if (toolName === "TaskStop") {
|
|
647
|
+
const output = firstTaskStopOutput(candidates);
|
|
648
|
+
if (!output) {
|
|
649
|
+
return "";
|
|
650
|
+
}
|
|
651
|
+
const lines = [
|
|
652
|
+
`Message: ${output.message}`,
|
|
653
|
+
`Task ID: ${output.task_id}`,
|
|
654
|
+
`Task type: ${output.task_type}`,
|
|
655
|
+
];
|
|
656
|
+
const command = nonEmptyString(output.command);
|
|
657
|
+
if (command) {
|
|
658
|
+
lines.push(`Command: ${command}`);
|
|
659
|
+
}
|
|
660
|
+
return lines.join("\n");
|
|
661
|
+
}
|
|
662
|
+
return "";
|
|
663
|
+
}
|
|
664
|
+
export function taskUpdateSucceeded(rawResult, rawContent) {
|
|
665
|
+
return firstTaskUpdateOutput(resultCandidates(rawResult, rawContent))?.success;
|
|
666
|
+
}
|
|
667
|
+
export function applyTaskToolResult(session, toolUseId, isError, rawContent, rawResult) {
|
|
668
|
+
if (isError) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const base = session.toolCalls.get(toolUseId);
|
|
672
|
+
const toolName = toolNameFromToolCall(base);
|
|
673
|
+
if (!isTaskToolName(toolName)) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const input = inputRecord(base);
|
|
677
|
+
const candidates = resultCandidates(rawResult, rawContent);
|
|
678
|
+
if (toolName === "TaskCreate") {
|
|
679
|
+
const taskRecord = firstTaskRecord(candidates);
|
|
680
|
+
const patch = taskRecord ? taskCreatePatch(taskRecord, input, toolUseId) : undefined;
|
|
681
|
+
if (!patch) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
linkTaskToolUse(session, patch.task_id, toolUseId);
|
|
685
|
+
emitTaskStateUpdate(session, "task_create", [upsertTask(session, patch)]);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (toolName === "TaskUpdate") {
|
|
689
|
+
const output = firstTaskUpdateOutput(candidates);
|
|
690
|
+
const taskId = nonEmptyString(output?.taskId) ?? nonEmptyString(input.taskId);
|
|
691
|
+
if (!output || !taskId || output.success !== true) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (input.status === "deleted") {
|
|
695
|
+
const removed = removeTasks(session, [taskId]);
|
|
696
|
+
emitTaskStateUpdate(session, "task_update", [], removed);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
emitTaskStateUpdate(session, "task_update", [
|
|
700
|
+
upsertTask(session, taskUpdatePatch(session, taskId, input, output)),
|
|
701
|
+
]);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (toolName === "TaskGet") {
|
|
705
|
+
const output = firstRecordWithTaskProperty(candidates);
|
|
706
|
+
const taskId = nonEmptyString(input.taskId);
|
|
707
|
+
if (!output) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const taskRecord = asRecordOrNull(output.task);
|
|
711
|
+
if (!taskRecord) {
|
|
712
|
+
if (taskId) {
|
|
713
|
+
emitTaskStateUpdate(session, "task_get", [], removeTasks(session, [taskId]));
|
|
714
|
+
}
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const patch = taskGetPatch(taskRecord, session.tasksById.get(nonEmptyString(taskRecord.id) ?? ""));
|
|
718
|
+
if (patch) {
|
|
719
|
+
emitTaskStateUpdate(session, "task_get", [upsertTask(session, patch)]);
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (toolName === "TaskList") {
|
|
724
|
+
const output = firstTaskListOutput(candidates);
|
|
725
|
+
if (!output || !Array.isArray(output.tasks)) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const patches = output.tasks
|
|
729
|
+
.map((entry) => {
|
|
730
|
+
const record = asRecordOrNull(entry);
|
|
731
|
+
const taskId = nonEmptyString(record?.id);
|
|
732
|
+
return record ? taskListPatch(record, taskId ? session.tasksById.get(taskId) : undefined) : undefined;
|
|
733
|
+
})
|
|
734
|
+
.filter((patch) => Boolean(patch));
|
|
735
|
+
const snapshot = replaceTaskSnapshot(session, patches);
|
|
736
|
+
emitTaskStateUpdate(session, "task_list", snapshot.tasks, snapshot.removedTaskIds, true);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (toolName === "TaskOutput") {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (toolName === "TaskStop") {
|
|
743
|
+
const output = firstTaskStopOutput(candidates);
|
|
744
|
+
if (!output) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const taskId = nonEmptyString(output.task_id);
|
|
748
|
+
if (!taskId) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const existing = session.tasksById.get(taskId);
|
|
752
|
+
const sourceToolCallId = existing?.source_tool_call_id ?? session.taskToolUseIds.get(taskId);
|
|
753
|
+
if (!existing && !sourceToolCallId) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const metadata = mergeMetadata(existing?.metadata, {
|
|
757
|
+
terminal_status: "stopped",
|
|
758
|
+
task_type: output.task_type,
|
|
759
|
+
...(typeof output.command === "string" ? { command: output.command } : {}),
|
|
760
|
+
});
|
|
761
|
+
emitTaskStateUpdate(session, "task_lifecycle", [
|
|
762
|
+
upsertTask(session, {
|
|
763
|
+
task_id: taskId,
|
|
764
|
+
subject: existing?.subject ?? nonEmptyString(output.command) ?? nonEmptyString(output.task_type) ?? taskId,
|
|
765
|
+
status: "completed",
|
|
766
|
+
metadata,
|
|
767
|
+
source_tool_call_id: sourceToolCallId,
|
|
768
|
+
}),
|
|
769
|
+
]);
|
|
770
|
+
unlinkTaskToolUse(session, taskId);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function lifecycleTaskStatus(subtype, msg) {
|
|
774
|
+
const patch = asRecordOrNull(msg.patch);
|
|
775
|
+
const explicit = normalizeLifecycleTaskStatus(msg.status) ?? normalizeLifecycleTaskStatus(patch?.status);
|
|
776
|
+
if (explicit) {
|
|
777
|
+
return explicit;
|
|
778
|
+
}
|
|
779
|
+
if (subtype === "task_started" || subtype === "task_progress") {
|
|
780
|
+
return "in_progress";
|
|
781
|
+
}
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
function lifecycleMetadata(msg) {
|
|
785
|
+
const patch = asRecordOrNull(msg.patch) ?? undefined;
|
|
786
|
+
const metadata = {};
|
|
787
|
+
const copyValue = (from, key, outKey = key) => {
|
|
788
|
+
if (!from || !Object.hasOwn(from, key)) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const value = jsonValue(from[key]);
|
|
792
|
+
if (value !== undefined) {
|
|
793
|
+
metadata[outKey] = value;
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
for (const key of [
|
|
797
|
+
"error",
|
|
798
|
+
"is_backgrounded",
|
|
799
|
+
"request_id",
|
|
800
|
+
"subagent_type",
|
|
801
|
+
"task_description",
|
|
802
|
+
"task_type",
|
|
803
|
+
"workflow_name",
|
|
804
|
+
"prompt",
|
|
805
|
+
"output_file",
|
|
806
|
+
"summary",
|
|
807
|
+
"end_time",
|
|
808
|
+
"total_paused_ms",
|
|
809
|
+
]) {
|
|
810
|
+
copyValue(msg, key);
|
|
811
|
+
copyValue(patch, key);
|
|
812
|
+
}
|
|
813
|
+
const terminalStatus = nonEmptyString(msg.status) ?? nonEmptyString(patch?.status);
|
|
814
|
+
if (terminalStatus === "completed" ||
|
|
815
|
+
terminalStatus === "failed" ||
|
|
816
|
+
terminalStatus === "killed" ||
|
|
817
|
+
terminalStatus === "stopped") {
|
|
818
|
+
metadata.terminal_status = terminalStatus;
|
|
819
|
+
}
|
|
820
|
+
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
821
|
+
}
|
|
822
|
+
export function applyTaskLifecycleState(session, subtype, msg) {
|
|
823
|
+
const taskId = nonEmptyString(msg.task_id);
|
|
824
|
+
if (!taskId) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const patch = asRecordOrNull(msg.patch);
|
|
828
|
+
const explicitToolUseId = nonEmptyString(msg.tool_use_id);
|
|
829
|
+
if (explicitToolUseId) {
|
|
830
|
+
linkTaskToolUse(session, taskId, explicitToolUseId);
|
|
831
|
+
}
|
|
832
|
+
const existing = session.tasksById.get(taskId);
|
|
833
|
+
const status = lifecycleTaskStatus(subtype, msg);
|
|
834
|
+
const description = nonEmptyString(patch?.description) ?? nonEmptyString(msg.description) ?? nonEmptyString(msg.summary);
|
|
835
|
+
const activeForm = nonEmptyString(patch?.activeForm);
|
|
836
|
+
const subject = nonEmptyString(patch?.subject) ??
|
|
837
|
+
nonEmptyString(msg.subject) ??
|
|
838
|
+
existing?.subject ??
|
|
839
|
+
nonEmptyString(msg.workflow_name) ??
|
|
840
|
+
nonEmptyString(msg.task_description) ??
|
|
841
|
+
description ??
|
|
842
|
+
taskId;
|
|
843
|
+
const metadata = mergeMetadata(existing?.metadata, lifecycleMetadata(msg));
|
|
844
|
+
const sourceToolCallId = session.taskToolUseIds.get(taskId);
|
|
845
|
+
if (!status && !description && !activeForm && metadata === existing?.metadata && !sourceToolCallId) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
emitTaskStateUpdate(session, "task_lifecycle", [
|
|
849
|
+
upsertTask(session, {
|
|
850
|
+
task_id: taskId,
|
|
851
|
+
subject,
|
|
852
|
+
description,
|
|
853
|
+
active_form: activeForm,
|
|
854
|
+
status,
|
|
855
|
+
metadata,
|
|
856
|
+
source_tool_call_id: sourceToolCallId,
|
|
857
|
+
}),
|
|
858
|
+
]);
|
|
859
|
+
}
|
|
860
|
+
export function currentTaskSnapshot(session) {
|
|
861
|
+
return orderedTasks(session);
|
|
862
|
+
}
|