@evandu/super-pi 0.1.40 → 0.1.42
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/agents/super-pi-task/index.ts +25 -8
- package/dist/client/assets/{index-DuqJLo6R.css → index-C6BogHc-.css} +1 -1
- package/dist/client/assets/{index-Dxm2Vtis.js → index-DlKKZuzf.js} +3 -2
- package/dist/client/index.html +2 -2
- package/dist/server/application/run-service.js +87 -41
- package/dist/server/runtime.js +5 -3
- package/dist/server/store.d.ts +11 -0
- package/dist/server/store.js +35 -1
- package/dist/server/tool-calls.js +9 -0
- package/dist/server/types.d.ts +16 -0
- package/package.json +1 -1
package/dist/client/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="alternate icon" href="/favicon.ico" />
|
|
9
9
|
<link rel="apple-touch-icon" href="/favicon.svg" />
|
|
10
10
|
<title>Super Pi</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-DlKKZuzf.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C6BogHc-.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -110,35 +110,66 @@ export class RunService {
|
|
|
110
110
|
* backfilled with `options[0]`.
|
|
111
111
|
*/
|
|
112
112
|
async askQuestion(conversationId) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
// THE ROW ARRIVES AFTER THE REQUEST, AND THAT IS A RACE, NOT A BUG IN THE CALLER.
|
|
114
|
+
//
|
|
115
|
+
// The extension lives INSIDE the Pi process. When Pi starts running the tool it writes
|
|
116
|
+
// `tool_execution_start` to stdout AND calls the tool - so the tool's HTTP request can reach this
|
|
117
|
+
// server while the transcript drain loop is still reading that very record off the pipe. The endpoint
|
|
118
|
+
// therefore asked for a tool call that had not been stored yet, and answered 「这个会话里没有正在
|
|
119
|
+
// 等待回答的 question 工具调用」 - which is a refusal, so the tool FAILED, and the person was never
|
|
120
|
+
// shown the question at all.
|
|
121
|
+
//
|
|
122
|
+
// So the lookup WAITS. A few hundred milliseconds of polling covers the drain, and a question that
|
|
123
|
+
// genuinely has no pending call still fails - just not instantly.
|
|
124
|
+
const findCall = () => {
|
|
125
|
+
const conversation = this.store.snapshot().conversations.find((item) => item.id === conversationId);
|
|
126
|
+
const message = conversation?.messages.slice().reverse().find((item) => item.role === "assistant");
|
|
127
|
+
if (!message)
|
|
128
|
+
return undefined;
|
|
129
|
+
for (let i = (message.toolCalls?.length ?? 0) - 1; i >= 0; i -= 1) {
|
|
130
|
+
const call = message.toolCalls?.[i];
|
|
131
|
+
if (call?.label === "question" && call.question?.status === "pending")
|
|
132
|
+
return { messageId: message.id, index: i };
|
|
125
133
|
}
|
|
134
|
+
return undefined;
|
|
135
|
+
};
|
|
136
|
+
let found;
|
|
137
|
+
for (let attempt = 0; attempt < 50 && !found; attempt += 1) {
|
|
138
|
+
found = findCall();
|
|
139
|
+
if (found)
|
|
140
|
+
break;
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
126
142
|
}
|
|
127
|
-
|
|
143
|
+
const conversation = this.store.snapshot().conversations.find((item) => item.id === conversationId);
|
|
144
|
+
if (!conversation?.messages.length)
|
|
145
|
+
throw new Error("No active assistant message");
|
|
146
|
+
if (!found)
|
|
128
147
|
throw new Error("这个会话里没有正在等待回答的 question 工具调用");
|
|
129
|
-
const messageId =
|
|
148
|
+
const { messageId, index } = found;
|
|
130
149
|
return new Promise((resolve, reject) => {
|
|
131
150
|
const finish = (settle) => { clearInterval(poll); clearTimeout(deadline); settle(); };
|
|
132
151
|
const poll = setInterval(() => {
|
|
133
152
|
const current = this.store.snapshot().conversations.find((item) => item.id === conversationId)?.messages.find((item) => item.id === messageId)?.toolCalls?.[index]?.question;
|
|
134
153
|
if (current?.status === "answered")
|
|
135
|
-
finish(() => resolve(
|
|
154
|
+
finish(() => resolve(answerPayload(current)));
|
|
136
155
|
// CANCELLED IS NOT AN ANSWER. Rejecting is what makes the tool call fail, which is what stops the
|
|
137
156
|
// run; resolving here would let the model carry on from a question the person refused to answer.
|
|
138
157
|
else if (current?.status === "cancelled")
|
|
139
158
|
finish(() => reject(new Error("用户没有回答这个问题")));
|
|
140
159
|
}, 250);
|
|
141
160
|
const deadline = setTimeout(() => finish(() => reject(new Error("等待回答超时"))), 30 * 60 * 1000);
|
|
161
|
+
// WHAT GOES BACK TO THE MODEL IS WRITTEN FOR THE MODEL. The answers pair to their questions by id,
|
|
162
|
+
// which is what stops a person answering the second of two being recorded against the first - but an
|
|
163
|
+
// id is a UUID, and a model handed `{answers:[{id:"671b0f0b-…", selected:["发"]}]}` has to guess at
|
|
164
|
+
// what that means. It guessed, and reported a real answer as a cancellation. So the answer goes back
|
|
165
|
+
// as its questions and its choices, with a sentence naming the outcome.
|
|
166
|
+
const answerPayload = (current) => {
|
|
167
|
+
const answers = (current.answers ?? []).map((answer) => {
|
|
168
|
+
const asked = current.items.find((item) => item.id === answer.id);
|
|
169
|
+
return { question: asked?.question, selected: answer.selected, ...(answer.note ? { note: answer.note } : {}) };
|
|
170
|
+
});
|
|
171
|
+
return { answers, summary: `用户回答:${answers.map((answer) => answer.selected.join("、")).join(";")}` };
|
|
172
|
+
};
|
|
142
173
|
});
|
|
143
174
|
}
|
|
144
175
|
/**
|
|
@@ -283,6 +314,11 @@ export class RunService {
|
|
|
283
314
|
// an order that never happened. The runtime calls `onToolCall` synchronously between yields, so a
|
|
284
315
|
// counter advanced once per yielded delta is the real position of the call in the answer.
|
|
285
316
|
let spoken = 0;
|
|
317
|
+
// AND HOW MUCH REASONING HAD BEEN STREAMED, for the same reason and from the same place. The store's
|
|
318
|
+
// `message.thinking` is only updated at the end of the turn (in `finally`), so it cannot be read here.
|
|
319
|
+
// The running total is what lets a call made during reasoning anchor inside the thinking block rather than
|
|
320
|
+
// after it, which is the order the events had.
|
|
321
|
+
let thought = 0;
|
|
286
322
|
/**
|
|
287
323
|
* ONE COUNTER FOR EVERY ROW, AND THE ONLY WAY A ROW NUMBER IS EVER MADE.
|
|
288
324
|
*
|
|
@@ -333,6 +369,7 @@ export class RunService {
|
|
|
333
369
|
...(call.detail ? { detail: call.detail } : { detail: "正在生成参数…" }),
|
|
334
370
|
preparing: true,
|
|
335
371
|
at,
|
|
372
|
+
thinkingAt: thought,
|
|
336
373
|
}));
|
|
337
374
|
preparingRows.set(call.contentIndex, { row, name: call.name });
|
|
338
375
|
};
|
|
@@ -364,7 +401,7 @@ export class RunService {
|
|
|
364
401
|
const row = rows++;
|
|
365
402
|
if (call.id)
|
|
366
403
|
rowByToolCallId.set(call.id, row);
|
|
367
|
-
enqueueTools("a tool call", () => this.store.appendToolCall(conversation.id, assistant.id, { label: call.name, detail: call.detail, args: call.args, at, ...(call.question ? { question: call.question } : {}) }));
|
|
404
|
+
enqueueTools("a tool call", () => this.store.appendToolCall(conversation.id, assistant.id, { label: call.name, detail: call.detail, args: call.args, at, thinkingAt: thought, ...(call.question ? { question: call.question } : {}) }));
|
|
368
405
|
};
|
|
369
406
|
const onToolUpdate = (call) => {
|
|
370
407
|
const index = call.id ? rowByToolCallId.get(call.id) : undefined;
|
|
@@ -390,7 +427,14 @@ export class RunService {
|
|
|
390
427
|
onToolPreparing,
|
|
391
428
|
onToolUpdate,
|
|
392
429
|
onToolEnd,
|
|
393
|
-
|
|
430
|
+
// BROADCAST AND KEEP. It used to only broadcast, into an event no one handled - so the reasoning
|
|
431
|
+
// reached nobody, live or afterwards. Both halves now happen: the browser draws the delta as it
|
|
432
|
+
// arrives, and the message keeps it so the finished turn has something to go back to.
|
|
433
|
+
onThinking: (delta) => {
|
|
434
|
+
thought += delta.length;
|
|
435
|
+
this.broadcaster.broadcast({ type: "message.thinking", conversationId: conversation.id, messageId: assistant.id, delta, active: true });
|
|
436
|
+
void this.store.appendThinkingDelta(conversation.id, assistant.id, delta);
|
|
437
|
+
},
|
|
394
438
|
images,
|
|
395
439
|
})) {
|
|
396
440
|
await this.store.appendAssistantDelta(conversation.id, assistant.id, delta);
|
|
@@ -593,36 +637,38 @@ export class RunService {
|
|
|
593
637
|
const result = run.status === "completed" ? run.output ?? "" : `Background task failed: ${run.error ?? run.status}`;
|
|
594
638
|
const tag = run.status === "completed" ? "task_result" : "task_error";
|
|
595
639
|
const message = `<task id="${run.id}" state="${run.status === "completed" ? "completed" : "error"}">\n<summary>Background task ${run.status === "completed" ? "completed" : "failed"}: ${body.description?.trim() || subagentType}</summary>\n<${tag}>\n${result}\n</${tag}>\n</task>`;
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
640
|
+
// A BACKGROUND RESULT IS A PROMPT, AND DELIVERING IT IS NOT TRIGGERING IT.
|
|
641
|
+
//
|
|
642
|
+
// This used to park it in the queue until the reader sent the next prompt, which is the hole the
|
|
643
|
+
// queue exists to avoid: nothing owned it. `takeNextQueuedMessage` is only ever called from
|
|
644
|
+
// inside a turn, so a result arriving at an IDLE conversation sat in the dock forever - and when
|
|
645
|
+
// the reader tried to steer it, the runtime refused with 「会话 X 现在没有正在运行的 Pi 进程」,
|
|
646
|
+
// because of course there was no run: the message had been waiting for one nobody would start.
|
|
647
|
+
//
|
|
648
|
+
// `enqueueAndRun` is the owner this needed. It claims the run slot synchronously, so it starts a
|
|
649
|
+
// turn when the conversation is idle and enqueues when one is running - and the turn it starts
|
|
650
|
+
// drains whatever follows. One call, and no queued message is ever left without a scheduled reader.
|
|
651
|
+
// HANDED OFF, NOT AWAITED. `enqueueAndRun` resolves when the whole turn it started has FINISHED,
|
|
652
|
+
// and this handler owns the `backgroundTasks` row - so awaiting it here kept that row on screen
|
|
653
|
+
// for the entire delivered answer, a finished task wearing its own 「running」 state. The row is
|
|
654
|
+
// removed as soon as the result has a home, which is what its lifecycle means.
|
|
655
|
+
const started = this.enqueueAndRun(conversation.id, message);
|
|
656
|
+
await this.store.removeBackgroundTask(run.id);
|
|
657
|
+
this.broadcaster.broadcastState();
|
|
658
|
+
await started.catch(async (error) => {
|
|
659
|
+
// The turn could not start, so the result has nowhere to be read from. Put it back in the queue,
|
|
660
|
+
// where it is at least visible and cancellable, rather than dropping it on the floor.
|
|
606
661
|
await this.store.enqueueMessage(conversation.id, message);
|
|
607
|
-
console.warn(`Super Pi: background result could not start a turn: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
608
|
-
}
|
|
609
|
-
finally {
|
|
610
|
-
await this.store.removeBackgroundTask(run.id);
|
|
611
662
|
this.broadcaster.broadcastState();
|
|
612
|
-
|
|
663
|
+
console.warn(`Super Pi: background result could not start a turn: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
664
|
+
});
|
|
613
665
|
}).catch(async (error) => {
|
|
614
666
|
const message = `<task id="${backgroundId}" state="error">\n<summary>Background task failed: ${body.description?.trim() || subagentType}</summary>\n<task_error>Background task failed before completion: ${error instanceof Error ? error.message : "unknown error"}</task_error>\n</task>`;
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
await this.enqueueAndRun(conversationId, message);
|
|
618
|
-
else
|
|
619
|
-
await this.store.enqueueMessage(conversationId, message);
|
|
620
|
-
}
|
|
621
|
-
catch {
|
|
622
|
-
await this.store.enqueueMessage(conversationId, message);
|
|
623
|
-
}
|
|
667
|
+
// The same rule as the task result above, and the same shape: hand it off, then drop the row.
|
|
668
|
+
const started = this.enqueueAndRun(conversationId, message);
|
|
624
669
|
await this.store.removeBackgroundTask(backgroundId);
|
|
625
670
|
this.broadcaster.broadcastState();
|
|
671
|
+
await started.catch(async () => { await this.store.enqueueMessage(conversationId, message); this.broadcaster.broadcastState(); });
|
|
626
672
|
console.warn(`Super Pi: background sub-agent dispatch failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
627
673
|
}));
|
|
628
674
|
return {
|
package/dist/server/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
4
3
|
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { resolve, join } from "node:path";
|
|
6
6
|
import { agentSpawnArgs, readAgents } from "./agents.js";
|
|
7
7
|
import { TASK_CAPABILITY_ENV, mintCapability, releaseCapability } from "./capability.js";
|
|
@@ -779,7 +779,6 @@ export class PiRpcRuntime {
|
|
|
779
779
|
} : {}) } } : {}),
|
|
780
780
|
});
|
|
781
781
|
const channel = new RpcChannel(child);
|
|
782
|
-
this.runs.set(conversation.id, { child, channel });
|
|
783
782
|
// Same guard as subagents.ts: a destroyed stdin throws synchronously on write, and an abort
|
|
784
783
|
// listener that throws inside a listener would surface as an unhandled error instead of a cancel.
|
|
785
784
|
const abort = () => {
|
|
@@ -994,6 +993,7 @@ export class PiRpcRuntime {
|
|
|
994
993
|
}
|
|
995
994
|
if (record.type === "message_update") {
|
|
996
995
|
const event = record.assistantMessageEvent;
|
|
996
|
+
console.log(`[runtime] message_update event.type=${JSON.stringify(event?.type)}`);
|
|
997
997
|
// text_delta is what drives the visible answer, and toolcall_start/delta/end are what make a
|
|
998
998
|
// call VISIBLE WHILE IT IS BEING WRITTEN.
|
|
999
999
|
//
|
|
@@ -1027,8 +1027,10 @@ export class PiRpcRuntime {
|
|
|
1027
1027
|
}
|
|
1028
1028
|
else if (event?.type === "thinking_delta")
|
|
1029
1029
|
onThinking?.(typeof event.delta === "string" ? event.delta : "");
|
|
1030
|
-
else
|
|
1030
|
+
else {
|
|
1031
1031
|
noteUnhandled(`message_update:${JSON.stringify(event?.type ?? "(missing)")}`);
|
|
1032
|
+
console.log(`[runtime] message_update not matched: event.type=${JSON.stringify(event?.type)}`);
|
|
1033
|
+
}
|
|
1032
1034
|
continue;
|
|
1033
1035
|
}
|
|
1034
1036
|
if (record.type === "extension_ui_request") {
|
package/dist/server/store.d.ts
CHANGED
|
@@ -211,6 +211,17 @@ export declare class Store {
|
|
|
211
211
|
}): Promise<Conversation>;
|
|
212
212
|
beginAssistantMessage(conversationId: string, agentIdentity?: Message["agentIdentity"]): Promise<Message>;
|
|
213
213
|
appendAssistantDelta(conversationId: string, messageId: string, delta: string): Promise<void>;
|
|
214
|
+
/**
|
|
215
|
+
* THE MODEL'S REASONING, APPENDED TO THE MESSAGE IT BELONGS TO.
|
|
216
|
+
*
|
|
217
|
+
* It used to be broadcast and nothing more: the server emitted `message.thinking` on every delta and no
|
|
218
|
+
* code anywhere read it, so the reasoning reached no one - not the screen while it streamed, and not
|
|
219
|
+
* the transcript afterwards. The field, the event and the stylesheet were all already there; the two
|
|
220
|
+
* halves that would have used them were missing, which is why a finished turn had no visible reasoning
|
|
221
|
+
* to go back to. Persisting here rather than only in the browser is the point of its living on the
|
|
222
|
+
* message: a reasoning stream that vanishes on reload was never a record of anything.
|
|
223
|
+
*/
|
|
224
|
+
appendThinkingDelta(conversationId: string, messageId: string, delta: string): Promise<void>;
|
|
214
225
|
/**
|
|
215
226
|
* Appends one tool call to the message it belongs to, and returns the message's whole `toolCalls`
|
|
216
227
|
* array so the caller can broadcast it as one snapshot.
|
package/dist/server/store.js
CHANGED
|
@@ -864,6 +864,25 @@ export class Store {
|
|
|
864
864
|
conversation.updatedAt = now();
|
|
865
865
|
await this.persistTransaction({ kind: "message.replace", conversationId, message: structuredClone(message), updatedAt: conversation.updatedAt });
|
|
866
866
|
}
|
|
867
|
+
/**
|
|
868
|
+
* THE MODEL'S REASONING, APPENDED TO THE MESSAGE IT BELONGS TO.
|
|
869
|
+
*
|
|
870
|
+
* It used to be broadcast and nothing more: the server emitted `message.thinking` on every delta and no
|
|
871
|
+
* code anywhere read it, so the reasoning reached no one - not the screen while it streamed, and not
|
|
872
|
+
* the transcript afterwards. The field, the event and the stylesheet were all already there; the two
|
|
873
|
+
* halves that would have used them were missing, which is why a finished turn had no visible reasoning
|
|
874
|
+
* to go back to. Persisting here rather than only in the browser is the point of its living on the
|
|
875
|
+
* message: a reasoning stream that vanishes on reload was never a record of anything.
|
|
876
|
+
*/
|
|
877
|
+
async appendThinkingDelta(conversationId, messageId, delta) {
|
|
878
|
+
const conversation = this.requireConversation(conversationId);
|
|
879
|
+
const message = conversation.messages.find((item) => item.id === messageId);
|
|
880
|
+
if (!message)
|
|
881
|
+
throw new Error("Assistant message not found");
|
|
882
|
+
message.thinking = `${message.thinking ?? ""}${delta}`;
|
|
883
|
+
conversation.updatedAt = now();
|
|
884
|
+
await this.persistTransaction({ kind: "message.replace", conversationId, message: structuredClone(message), updatedAt: conversation.updatedAt });
|
|
885
|
+
}
|
|
867
886
|
/**
|
|
868
887
|
* Appends one tool call to the message it belongs to, and returns the message's whole `toolCalls`
|
|
869
888
|
* array so the caller can broadcast it as one snapshot.
|
|
@@ -894,7 +913,22 @@ export class Store {
|
|
|
894
913
|
// halves: the call arrived, and the message already holds every character streamed before it. Without
|
|
895
914
|
// it the browser can only render the blocks as one list beside the text, so a turn that read a file,
|
|
896
915
|
// said something, then ran a test would show all three in the wrong order.
|
|
897
|
-
|
|
916
|
+
//
|
|
917
|
+
// AND WHERE IN THE REASONING, for the same reason and from the same place. `thinking` is the other
|
|
918
|
+
// stream a turn has, and a call made while the model was still thinking has to be placeable inside it -
|
|
919
|
+
// otherwise the transcript can only say "it reasoned, and then it called a tool", which is the one order
|
|
920
|
+
// a `hmm → read → hmm → grep → answer` turn never had. Stamped as the thinking length BEFORE this call,
|
|
921
|
+
// so a call that started before any reasoning anchors at 0 and renders first.
|
|
922
|
+
//
|
|
923
|
+
// The thinking read here is the message's own field, written synchronously as each delta arrives - not
|
|
924
|
+
// a count of how much a queued disk write had managed to flush. That distinction is the same one `at`
|
|
925
|
+
// was stamped for.
|
|
926
|
+
const placed = {
|
|
927
|
+
...call,
|
|
928
|
+
...(call.at === undefined ? { at: message.content.length } : {}),
|
|
929
|
+
...(call.thinkingAt === undefined ? { thinkingAt: (message.thinking ?? "").length } : {}),
|
|
930
|
+
};
|
|
931
|
+
calls.push(structuredClone(placed));
|
|
898
932
|
conversation.updatedAt = now();
|
|
899
933
|
await this.persist();
|
|
900
934
|
return structuredClone(calls);
|
|
@@ -140,6 +140,15 @@ const oneLine = (value) => {
|
|
|
140
140
|
* shape can be regression-tested at all.
|
|
141
141
|
*/
|
|
142
142
|
export function describeToolCall(name, args) {
|
|
143
|
+
// A QUESTION NAMES ITSELF. The generic path reads a target-ish string out of the arguments and a
|
|
144
|
+
// question has none, so it fell through to the preparing placeholder - which left every settled
|
|
145
|
+
// question row headed 「正在生成参数…」 above the question itself. The row is the decision; the header
|
|
146
|
+
// should say which one.
|
|
147
|
+
if (name.trim().toLowerCase() === "question") {
|
|
148
|
+
const asked = readQuestionCall(name, args);
|
|
149
|
+
if (asked?.items.length)
|
|
150
|
+
return oneLine(asked.items.length === 1 ? asked.items[0].question : `${asked.items.length} 个问题`);
|
|
151
|
+
}
|
|
143
152
|
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
144
153
|
return undefined;
|
|
145
154
|
const record = args;
|
package/dist/server/types.d.ts
CHANGED
|
@@ -167,7 +167,23 @@ export type ToolCallBlock = {
|
|
|
167
167
|
* the only thing on screen during a long parameter stream - an 'edit' block carries a whole file's
|
|
168
168
|
* replacement text, and without this the screen is blank until the last token arrives. */
|
|
169
169
|
preparing?: boolean;
|
|
170
|
+
/**
|
|
171
|
+
* WHERE IN THE ANSWER the call happened, as a character offset into `content` - and WHERE IN THE
|
|
172
|
+
* REASONING, as an offset into `thinking`.
|
|
173
|
+
*
|
|
174
|
+
* A TURN HAS TWO STREAMS, and a call belongs to a position in each. `at` places it among the words the
|
|
175
|
+
* agent spoke; `thinkingAt` places it inside the reasoning it did before speaking, which is what lets a
|
|
176
|
+
* call that happened mid-thought render INSIDE the thinking block rather than beside it. Without the
|
|
177
|
+
* second offset a tool call can only be stacked after the reasoning, which reads as "the model thought,
|
|
178
|
+
* and then, separately, called a tool" - the order the events did not have.
|
|
179
|
+
*
|
|
180
|
+
* OPTIONAL, and absent means "this call's place in the reasoning was never recorded" - a message written
|
|
181
|
+
* before the field existed. A reader that guesses a position for such a call would be re-ordering a turn
|
|
182
|
+
* on no evidence, so it must fall back to `at` alone, which is exactly what those blocks were written to
|
|
183
|
+
* be read by.
|
|
184
|
+
*/
|
|
170
185
|
at?: number;
|
|
186
|
+
thinkingAt?: number;
|
|
171
187
|
question?: ToolCallQuestion;
|
|
172
188
|
};
|
|
173
189
|
/**
|