@shipers-dev/multi 0.5.0 → 0.5.1
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/dist/index.js +76 -35
- package/package.json +1 -1
- package/src/acp-runner.ts +15 -3
- package/src/index.ts +54 -31
package/dist/index.js
CHANGED
|
@@ -5198,6 +5198,7 @@ async function runAcp(opts) {
|
|
|
5198
5198
|
const stream2 = ndJsonStream(output, input);
|
|
5199
5199
|
let activeSessionId = opts.sessionId || null;
|
|
5200
5200
|
let recording = false;
|
|
5201
|
+
const alwaysAllow = new Set;
|
|
5201
5202
|
const client = {
|
|
5202
5203
|
async sessionUpdate(params) {
|
|
5203
5204
|
if (!recording)
|
|
@@ -5205,7 +5206,7 @@ async function runAcp(opts) {
|
|
|
5205
5206
|
await handleSessionUpdate(params, opts);
|
|
5206
5207
|
},
|
|
5207
5208
|
async requestPermission(params) {
|
|
5208
|
-
return await handleRequestPermission(params, opts);
|
|
5209
|
+
return await handleRequestPermission(params, opts, alwaysAllow);
|
|
5209
5210
|
},
|
|
5210
5211
|
async readTextFile(params) {
|
|
5211
5212
|
try {
|
|
@@ -5325,8 +5326,14 @@ ${entries}` } });
|
|
|
5325
5326
|
break;
|
|
5326
5327
|
}
|
|
5327
5328
|
}
|
|
5328
|
-
async function handleRequestPermission(params, o) {
|
|
5329
|
+
async function handleRequestPermission(params, o, allowCache) {
|
|
5329
5330
|
const tc = params.toolCall || {};
|
|
5331
|
+
const toolKey = `${tc.toolName || tc.title || ""}|${tc.kind || ""}`.toLowerCase();
|
|
5332
|
+
if (toolKey && allowCache.has(toolKey)) {
|
|
5333
|
+
const allowOpt = params.options.find((op) => /allow/i.test(op.kind || "") || /allow/i.test(op.name || ""));
|
|
5334
|
+
if (allowOpt)
|
|
5335
|
+
return { outcome: { outcome: "selected", optionId: allowOpt.optionId } };
|
|
5336
|
+
}
|
|
5330
5337
|
const created = await apiClient.post(`${o.apiUrl}/api/permissions`, {
|
|
5331
5338
|
issue_id: o.issueId,
|
|
5332
5339
|
device_id: o.deviceId,
|
|
@@ -5346,6 +5353,10 @@ ${entries}` } });
|
|
|
5346
5353
|
if (!row)
|
|
5347
5354
|
break;
|
|
5348
5355
|
if (row.status === "resolved" && row.chosen) {
|
|
5356
|
+
const chosenOpt = params.options.find((op) => op.optionId === row.chosen);
|
|
5357
|
+
const isAlways = chosenOpt && (/always/i.test(chosenOpt.name || "") || /allow_always/i.test(chosenOpt.kind || ""));
|
|
5358
|
+
if (isAlways && toolKey)
|
|
5359
|
+
allowCache.add(toolKey);
|
|
5349
5360
|
return { outcome: { outcome: "selected", optionId: row.chosen } };
|
|
5350
5361
|
}
|
|
5351
5362
|
if (row.status === "cancelled") {
|
|
@@ -5381,7 +5392,7 @@ var LOG_PATH = join(MULTI_DIR, "logs", "agent.log");
|
|
|
5381
5392
|
var SKILLS_DIR = join(MULTI_DIR, "skills");
|
|
5382
5393
|
var STOP_PATH = join(MULTI_DIR, "stop.flag");
|
|
5383
5394
|
var TASKS_DB_PATH = join(MULTI_DIR, "tasks.db");
|
|
5384
|
-
var VERSION = "0.5.
|
|
5395
|
+
var VERSION = "0.5.1";
|
|
5385
5396
|
var COMMANDS = {
|
|
5386
5397
|
setup: "Register this device with a workspace",
|
|
5387
5398
|
connect: "Connect device to realtime hub and execute assigned tasks",
|
|
@@ -5833,29 +5844,71 @@ async function handleRunTask(apiUrl, deviceId, task, detected) {
|
|
|
5833
5844
|
} catch {}
|
|
5834
5845
|
};
|
|
5835
5846
|
const turn = {
|
|
5836
|
-
|
|
5837
|
-
toolOrder: [],
|
|
5847
|
+
blocks: [],
|
|
5838
5848
|
tools: new Map,
|
|
5839
5849
|
plans: [],
|
|
5840
5850
|
progress: [],
|
|
5841
5851
|
result: null,
|
|
5842
5852
|
error: null
|
|
5843
5853
|
};
|
|
5854
|
+
const appendText = (text) => {
|
|
5855
|
+
const last = turn.blocks[turn.blocks.length - 1];
|
|
5856
|
+
if (last && last.kind === "text")
|
|
5857
|
+
last.text += text;
|
|
5858
|
+
else
|
|
5859
|
+
turn.blocks.push({ kind: "text", text });
|
|
5860
|
+
};
|
|
5861
|
+
const upsertTool = (id, patch) => {
|
|
5862
|
+
const existing = turn.tools.get(id);
|
|
5863
|
+
if (existing) {
|
|
5864
|
+
if (patch.tool && patch.tool !== "tool")
|
|
5865
|
+
existing.tool = patch.tool;
|
|
5866
|
+
if (patch.kind)
|
|
5867
|
+
existing.kind = patch.kind;
|
|
5868
|
+
if (patch.status)
|
|
5869
|
+
existing.status = patch.status;
|
|
5870
|
+
if (patch.input !== undefined)
|
|
5871
|
+
existing.input = patch.input;
|
|
5872
|
+
} else {
|
|
5873
|
+
turn.tools.set(id, { id, tool: patch.tool || "tool", kind: patch.kind, status: patch.status, input: patch.input, results: [] });
|
|
5874
|
+
turn.blocks.push({ kind: "tool", id });
|
|
5875
|
+
}
|
|
5876
|
+
};
|
|
5877
|
+
const toolLabel = (t) => {
|
|
5878
|
+
const clean = stripMd(t.tool);
|
|
5879
|
+
if (clean && clean !== "tool")
|
|
5880
|
+
return clean;
|
|
5881
|
+
if (t.input && typeof t.input === "object") {
|
|
5882
|
+
if (t.input.command)
|
|
5883
|
+
return `Bash: ${String(t.input.command).split(`
|
|
5884
|
+
`)[0].slice(0, 60)}`;
|
|
5885
|
+
if (t.input.file_path)
|
|
5886
|
+
return `${t.kind === "edit" ? "Edit" : "Read"}: ${t.input.file_path}`;
|
|
5887
|
+
if (t.input.pattern)
|
|
5888
|
+
return `Grep: ${t.input.pattern}`;
|
|
5889
|
+
if (t.input.url)
|
|
5890
|
+
return `Fetch: ${t.input.url}`;
|
|
5891
|
+
}
|
|
5892
|
+
return t.kind ? `${t.kind}` : "tool";
|
|
5893
|
+
};
|
|
5844
5894
|
const render = () => {
|
|
5845
5895
|
const parts = [];
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5896
|
+
for (const b of turn.blocks) {
|
|
5897
|
+
if (b.kind === "text") {
|
|
5898
|
+
if (b.text)
|
|
5899
|
+
parts.push(b.text);
|
|
5900
|
+
continue;
|
|
5901
|
+
}
|
|
5902
|
+
const t = turn.tools.get(b.id);
|
|
5850
5903
|
if (!t)
|
|
5851
5904
|
continue;
|
|
5852
5905
|
const icon = statusIcon(t.status);
|
|
5853
|
-
const
|
|
5854
|
-
const head = `${icon} ${
|
|
5906
|
+
const label = toolLabel(t);
|
|
5907
|
+
const head = `${icon} ${label}${t.status && t.status !== "completed" ? ` \xB7 ${t.status}` : ""}`;
|
|
5855
5908
|
const body = [];
|
|
5856
5909
|
if (t.input !== undefined && t.input !== null) {
|
|
5857
5910
|
const inputStr = typeof t.input === "object" ? JSON.stringify(t.input, null, 2) : String(t.input);
|
|
5858
|
-
body.push("```json\n" + inputStr + "\n```");
|
|
5911
|
+
body.push("```json\n" + inputStr.slice(0, 2000) + "\n```");
|
|
5859
5912
|
}
|
|
5860
5913
|
if (t.results.length) {
|
|
5861
5914
|
const joined = t.results.join(`
|
|
@@ -5917,7 +5970,7 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5917
5970
|
}
|
|
5918
5971
|
case "assistant_text": {
|
|
5919
5972
|
await ensureLiveComment();
|
|
5920
|
-
|
|
5973
|
+
appendText(p.text);
|
|
5921
5974
|
hasAssistantText = true;
|
|
5922
5975
|
schedulePatch();
|
|
5923
5976
|
break;
|
|
@@ -5925,8 +5978,8 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5925
5978
|
case "stdout": {
|
|
5926
5979
|
if (p.line) {
|
|
5927
5980
|
await ensureLiveComment();
|
|
5928
|
-
|
|
5929
|
-
` : "") + p.line;
|
|
5981
|
+
appendText((turn.blocks.length ? `
|
|
5982
|
+
` : "") + p.line);
|
|
5930
5983
|
hasAssistantText = true;
|
|
5931
5984
|
schedulePatch();
|
|
5932
5985
|
}
|
|
@@ -5934,36 +5987,24 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5934
5987
|
}
|
|
5935
5988
|
case "tool_call": {
|
|
5936
5989
|
await ensureLiveComment();
|
|
5937
|
-
const id = p.id || `anon-${turn.
|
|
5938
|
-
|
|
5939
|
-
if (existing) {
|
|
5940
|
-
if (p.tool)
|
|
5941
|
-
existing.tool = p.tool;
|
|
5942
|
-
if (p.kind)
|
|
5943
|
-
existing.kind = p.kind;
|
|
5944
|
-
if (p.status)
|
|
5945
|
-
existing.status = p.status;
|
|
5946
|
-
if (p.input !== undefined)
|
|
5947
|
-
existing.input = p.input;
|
|
5948
|
-
} else {
|
|
5949
|
-
turn.toolOrder.push(id);
|
|
5950
|
-
turn.tools.set(id, { id, tool: p.tool || "tool", kind: p.kind, status: p.status, input: p.input, results: [] });
|
|
5951
|
-
}
|
|
5990
|
+
const id = p.id || `anon-${turn.tools.size}`;
|
|
5991
|
+
upsertTool(id, { tool: p.tool, kind: p.kind, status: p.status, input: p.input });
|
|
5952
5992
|
schedulePatch();
|
|
5953
5993
|
break;
|
|
5954
5994
|
}
|
|
5955
5995
|
case "tool_result": {
|
|
5956
5996
|
await ensureLiveComment();
|
|
5957
|
-
const
|
|
5997
|
+
const lastToolId = [...turn.blocks].reverse().find((b) => b.kind === "tool")?.id;
|
|
5998
|
+
const id = p.tool_use_id || lastToolId;
|
|
5958
5999
|
const entry = id ? turn.tools.get(id) : undefined;
|
|
5959
6000
|
const content = String(p.content ?? "").trim();
|
|
5960
6001
|
if (entry && content) {
|
|
5961
6002
|
entry.results.push(content);
|
|
5962
6003
|
schedulePatch();
|
|
5963
6004
|
} else if (content) {
|
|
5964
|
-
const pid = `result-${turn.
|
|
5965
|
-
|
|
5966
|
-
turn.tools.
|
|
6005
|
+
const pid = `result-${turn.tools.size}`;
|
|
6006
|
+
upsertTool(pid, { tool: "tool result" });
|
|
6007
|
+
turn.tools.get(pid).results.push(content);
|
|
5967
6008
|
schedulePatch();
|
|
5968
6009
|
}
|
|
5969
6010
|
break;
|
|
@@ -5973,7 +6014,7 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5973
6014
|
if (p.is_error)
|
|
5974
6015
|
hadError = true;
|
|
5975
6016
|
if (!hasAssistantText && p.result) {
|
|
5976
|
-
|
|
6017
|
+
appendText(p.result);
|
|
5977
6018
|
hasAssistantText = true;
|
|
5978
6019
|
}
|
|
5979
6020
|
turn.result = { duration_ms: p.duration_ms, total_cost_usd: p.total_cost_usd, stopReason: p.stopReason, is_error: p.is_error };
|
package/package.json
CHANGED
package/src/acp-runner.ts
CHANGED
|
@@ -54,6 +54,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
54
54
|
|
|
55
55
|
let activeSessionId: string | null = opts.sessionId || null;
|
|
56
56
|
let recording = false; // only forward events after prompt() starts
|
|
57
|
+
const alwaysAllow = new Set<string>(); // keys: toolName|kind that user said "always allow"
|
|
57
58
|
|
|
58
59
|
const client: Client = {
|
|
59
60
|
async sessionUpdate(params: SessionNotification): Promise<void> {
|
|
@@ -61,7 +62,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
61
62
|
await handleSessionUpdate(params, opts);
|
|
62
63
|
},
|
|
63
64
|
async requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
|
64
|
-
return await handleRequestPermission(params, opts);
|
|
65
|
+
return await handleRequestPermission(params, opts, alwaysAllow);
|
|
65
66
|
},
|
|
66
67
|
async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
|
67
68
|
try {
|
|
@@ -170,8 +171,16 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
async function handleRequestPermission(params: RequestPermissionRequest, o: AcpRunOpts): Promise<RequestPermissionResponse> {
|
|
174
|
+
async function handleRequestPermission(params: RequestPermissionRequest, o: AcpRunOpts, allowCache: Set<string>): Promise<RequestPermissionResponse> {
|
|
174
175
|
const tc: any = params.toolCall || {};
|
|
176
|
+
const toolKey = `${tc.toolName || tc.title || ''}|${tc.kind || ''}`.toLowerCase();
|
|
177
|
+
|
|
178
|
+
// Auto-approve if user previously chose "always allow" for same tool/kind
|
|
179
|
+
if (toolKey && allowCache.has(toolKey)) {
|
|
180
|
+
const allowOpt = (params.options as any[]).find(op => /allow/i.test(op.kind || '') || /allow/i.test(op.name || ''));
|
|
181
|
+
if (allowOpt) return { outcome: { outcome: 'selected', optionId: allowOpt.optionId } as any };
|
|
182
|
+
}
|
|
183
|
+
|
|
175
184
|
const created = await apiClient.post<any>(`${o.apiUrl}/api/permissions`, {
|
|
176
185
|
issue_id: o.issueId,
|
|
177
186
|
device_id: o.deviceId,
|
|
@@ -183,7 +192,6 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
183
192
|
const permId = created.data?.id;
|
|
184
193
|
if (!permId) return { outcome: { outcome: 'cancelled' } as any };
|
|
185
194
|
|
|
186
|
-
// Poll every 500ms up to 5 min
|
|
187
195
|
const deadline = Date.now() + 5 * 60 * 1000;
|
|
188
196
|
while (Date.now() < deadline) {
|
|
189
197
|
await new Promise(r => setTimeout(r, 500));
|
|
@@ -191,6 +199,10 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
191
199
|
const row = got.data;
|
|
192
200
|
if (!row) break;
|
|
193
201
|
if (row.status === 'resolved' && row.chosen) {
|
|
202
|
+
// If the chosen option is an "always" variant, cache it
|
|
203
|
+
const chosenOpt = (params.options as any[]).find(op => op.optionId === row.chosen);
|
|
204
|
+
const isAlways = chosenOpt && (/always/i.test(chosenOpt.name || '') || /allow_always/i.test(chosenOpt.kind || ''));
|
|
205
|
+
if (isAlways && toolKey) allowCache.add(toolKey);
|
|
194
206
|
return { outcome: { outcome: 'selected', optionId: row.chosen } as any };
|
|
195
207
|
}
|
|
196
208
|
if (row.status === 'cancelled') {
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ const LOG_PATH = join(MULTI_DIR, 'logs', 'agent.log');
|
|
|
16
16
|
const SKILLS_DIR = join(MULTI_DIR, 'skills');
|
|
17
17
|
const STOP_PATH = join(MULTI_DIR, 'stop.flag');
|
|
18
18
|
const TASKS_DB_PATH = join(MULTI_DIR, 'tasks.db');
|
|
19
|
-
const VERSION = '0.5.
|
|
19
|
+
const VERSION = '0.5.1';
|
|
20
20
|
|
|
21
21
|
const COMMANDS = {
|
|
22
22
|
setup: 'Register this device with a workspace',
|
|
@@ -448,9 +448,9 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
448
448
|
};
|
|
449
449
|
|
|
450
450
|
type ToolEntry = { id: string; tool: string; kind?: string; status?: string; input?: any; results: string[] };
|
|
451
|
+
type Block = { kind: 'text'; text: string } | { kind: 'tool'; id: string };
|
|
451
452
|
const turn = {
|
|
452
|
-
|
|
453
|
-
toolOrder: [] as string[],
|
|
453
|
+
blocks: [] as Block[],
|
|
454
454
|
tools: new Map<string, ToolEntry>(),
|
|
455
455
|
plans: [] as string[],
|
|
456
456
|
progress: [] as string[],
|
|
@@ -458,20 +458,54 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
458
458
|
error: null as null | string,
|
|
459
459
|
};
|
|
460
460
|
|
|
461
|
+
const appendText = (text: string) => {
|
|
462
|
+
const last = turn.blocks[turn.blocks.length - 1];
|
|
463
|
+
if (last && last.kind === 'text') last.text += text;
|
|
464
|
+
else turn.blocks.push({ kind: 'text', text });
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const upsertTool = (id: string, patch: Partial<ToolEntry>) => {
|
|
468
|
+
const existing = turn.tools.get(id);
|
|
469
|
+
if (existing) {
|
|
470
|
+
if (patch.tool && patch.tool !== 'tool') existing.tool = patch.tool;
|
|
471
|
+
if (patch.kind) existing.kind = patch.kind;
|
|
472
|
+
if (patch.status) existing.status = patch.status;
|
|
473
|
+
if (patch.input !== undefined) existing.input = patch.input;
|
|
474
|
+
} else {
|
|
475
|
+
turn.tools.set(id, { id, tool: patch.tool || 'tool', kind: patch.kind, status: patch.status, input: patch.input, results: [] });
|
|
476
|
+
turn.blocks.push({ kind: 'tool', id });
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const toolLabel = (t: ToolEntry): string => {
|
|
481
|
+
const clean = stripMd(t.tool);
|
|
482
|
+
if (clean && clean !== 'tool') return clean;
|
|
483
|
+
// Fallback from input shape
|
|
484
|
+
if (t.input && typeof t.input === 'object') {
|
|
485
|
+
if (t.input.command) return `Bash: ${String(t.input.command).split('\n')[0].slice(0, 60)}`;
|
|
486
|
+
if (t.input.file_path) return `${t.kind === 'edit' ? 'Edit' : 'Read'}: ${t.input.file_path}`;
|
|
487
|
+
if (t.input.pattern) return `Grep: ${t.input.pattern}`;
|
|
488
|
+
if (t.input.url) return `Fetch: ${t.input.url}`;
|
|
489
|
+
}
|
|
490
|
+
return (t.kind ? `${t.kind}` : 'tool');
|
|
491
|
+
};
|
|
492
|
+
|
|
461
493
|
const render = (): string => {
|
|
462
494
|
const parts: string[] = [];
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
495
|
+
for (const b of turn.blocks) {
|
|
496
|
+
if (b.kind === 'text') {
|
|
497
|
+
if (b.text) parts.push(b.text);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const t = turn.tools.get(b.id);
|
|
467
501
|
if (!t) continue;
|
|
468
502
|
const icon = statusIcon(t.status);
|
|
469
|
-
const
|
|
470
|
-
const head = `${icon} ${
|
|
503
|
+
const label = toolLabel(t);
|
|
504
|
+
const head = `${icon} ${label}${t.status && t.status !== 'completed' ? ` · ${t.status}` : ''}`;
|
|
471
505
|
const body: string[] = [];
|
|
472
506
|
if (t.input !== undefined && t.input !== null) {
|
|
473
507
|
const inputStr = typeof t.input === 'object' ? JSON.stringify(t.input, null, 2) : String(t.input);
|
|
474
|
-
body.push('```json\n' + inputStr + '\n```');
|
|
508
|
+
body.push('```json\n' + inputStr.slice(0, 2000) + '\n```');
|
|
475
509
|
}
|
|
476
510
|
if (t.results.length) {
|
|
477
511
|
const joined = t.results.join('\n').slice(-2000);
|
|
@@ -481,7 +515,6 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
481
515
|
}
|
|
482
516
|
|
|
483
517
|
if (turn.plans.length) parts.push('**Plan**\n\n' + turn.plans[turn.plans.length - 1]);
|
|
484
|
-
|
|
485
518
|
if (turn.error) parts.push(`> ❌ ${turn.error}`);
|
|
486
519
|
|
|
487
520
|
if (turn.result) {
|
|
@@ -517,16 +550,15 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
517
550
|
}
|
|
518
551
|
case 'assistant_text': {
|
|
519
552
|
await ensureLiveComment();
|
|
520
|
-
|
|
553
|
+
appendText(p.text);
|
|
521
554
|
hasAssistantText = true;
|
|
522
555
|
schedulePatch();
|
|
523
556
|
break;
|
|
524
557
|
}
|
|
525
558
|
case 'stdout': {
|
|
526
|
-
// Legacy (non-ACP) runners emit stdout — route to live comment too.
|
|
527
559
|
if (p.line) {
|
|
528
560
|
await ensureLiveComment();
|
|
529
|
-
|
|
561
|
+
appendText((turn.blocks.length ? '\n' : '') + p.line);
|
|
530
562
|
hasAssistantText = true;
|
|
531
563
|
schedulePatch();
|
|
532
564
|
}
|
|
@@ -534,33 +566,24 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
534
566
|
}
|
|
535
567
|
case 'tool_call': {
|
|
536
568
|
await ensureLiveComment();
|
|
537
|
-
const id = p.id || `anon-${turn.
|
|
538
|
-
|
|
539
|
-
if (existing) {
|
|
540
|
-
if (p.tool) existing.tool = p.tool;
|
|
541
|
-
if (p.kind) existing.kind = p.kind;
|
|
542
|
-
if (p.status) existing.status = p.status;
|
|
543
|
-
if (p.input !== undefined) existing.input = p.input;
|
|
544
|
-
} else {
|
|
545
|
-
turn.toolOrder.push(id);
|
|
546
|
-
turn.tools.set(id, { id, tool: p.tool || 'tool', kind: p.kind, status: p.status, input: p.input, results: [] });
|
|
547
|
-
}
|
|
569
|
+
const id = p.id || `anon-${turn.tools.size}`;
|
|
570
|
+
upsertTool(id, { tool: p.tool, kind: p.kind, status: p.status, input: p.input });
|
|
548
571
|
schedulePatch();
|
|
549
572
|
break;
|
|
550
573
|
}
|
|
551
574
|
case 'tool_result': {
|
|
552
575
|
await ensureLiveComment();
|
|
553
|
-
const
|
|
576
|
+
const lastToolId = [...turn.blocks].reverse().find(b => b.kind === 'tool')?.id;
|
|
577
|
+
const id = p.tool_use_id || lastToolId;
|
|
554
578
|
const entry = id ? turn.tools.get(id) : undefined;
|
|
555
579
|
const content = String(p.content ?? '').trim();
|
|
556
580
|
if (entry && content) {
|
|
557
581
|
entry.results.push(content);
|
|
558
582
|
schedulePatch();
|
|
559
583
|
} else if (content) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
turn.
|
|
563
|
-
turn.tools.set(pid, { id: pid, tool: 'tool result', results: [content] });
|
|
584
|
+
const pid = `result-${turn.tools.size}`;
|
|
585
|
+
upsertTool(pid, { tool: 'tool result' });
|
|
586
|
+
turn.tools.get(pid)!.results.push(content);
|
|
564
587
|
schedulePatch();
|
|
565
588
|
}
|
|
566
589
|
break;
|
|
@@ -569,7 +592,7 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
569
592
|
await ensureLiveComment();
|
|
570
593
|
if (p.is_error) hadError = true;
|
|
571
594
|
if (!hasAssistantText && p.result) {
|
|
572
|
-
|
|
595
|
+
appendText(p.result);
|
|
573
596
|
hasAssistantText = true;
|
|
574
597
|
}
|
|
575
598
|
turn.result = { duration_ms: p.duration_ms, total_cost_usd: p.total_cost_usd, stopReason: p.stopReason, is_error: p.is_error };
|