@immediately-run/sdk 0.57.1 → 0.58.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/dist/agentChatClient.cjs +112 -0
- package/dist/agentChatClient.cjs.map +1 -0
- package/dist/agentChatClient.d.cts +9 -0
- package/dist/agentChatClient.d.ts +9 -0
- package/dist/agentChatClient.js +89 -0
- package/dist/agentChatClient.js.map +1 -0
- package/dist/agentContext.cjs +69 -0
- package/dist/agentContext.cjs.map +1 -0
- package/dist/agentContext.d.cts +45 -0
- package/dist/agentContext.d.ts +45 -0
- package/dist/agentContext.js +45 -0
- package/dist/agentContext.js.map +1 -0
- package/dist/agentLoop.cjs +301 -0
- package/dist/agentLoop.cjs.map +1 -0
- package/dist/agentLoop.d.cts +267 -0
- package/dist/agentLoop.d.ts +267 -0
- package/dist/agentLoop.js +271 -0
- package/dist/agentLoop.js.map +1 -0
- package/dist/agentSteering.cjs +127 -0
- package/dist/agentSteering.cjs.map +1 -0
- package/dist/agentSteering.d.cts +80 -0
- package/dist/agentSteering.d.ts +80 -0
- package/dist/agentSteering.js +98 -0
- package/dist/agentSteering.js.map +1 -0
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +33 -2
- package/dist/auth.d.ts +33 -2
- package/dist/auth.js.map +1 -1
- package/dist/bundle.cjs +91 -0
- package/dist/bundle.cjs.map +1 -0
- package/dist/bundle.d.cts +63 -0
- package/dist/bundle.d.ts +63 -0
- package/dist/bundle.js +62 -0
- package/dist/bundle.js.map +1 -0
- package/dist/collectHeadings.cjs +59 -0
- package/dist/collectHeadings.cjs.map +1 -0
- package/dist/collectHeadings.d.cts +16 -0
- package/dist/collectHeadings.d.ts +16 -0
- package/dist/collectHeadings.js +36 -0
- package/dist/collectHeadings.js.map +1 -0
- package/dist/corpus.cjs +9 -58
- package/dist/corpus.cjs.map +1 -1
- package/dist/corpus.d.cts +3 -63
- package/dist/corpus.d.ts +3 -63
- package/dist/corpus.js +15 -57
- package/dist/corpus.js.map +1 -1
- package/dist/debug.cjs +115 -0
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.js +115 -0
- package/dist/debug.js.map +1 -1
- package/dist/fence.cjs +49 -0
- package/dist/fence.cjs.map +1 -0
- package/dist/fence.d.cts +4 -0
- package/dist/fence.d.ts +4 -0
- package/dist/fence.js +26 -0
- package/dist/fence.js.map +1 -0
- package/dist/formFactor.cjs.map +1 -1
- package/dist/formFactor.d.cts +11 -2
- package/dist/formFactor.d.ts +11 -2
- package/dist/formFactor.js.map +1 -1
- package/dist/fs.cjs.map +1 -1
- package/dist/fs.d.cts +16 -6
- package/dist/fs.d.ts +16 -6
- package/dist/fs.js.map +1 -1
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +7 -0
- package/dist/hooks.d.ts +7 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +21 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/launch.cjs +45 -6
- package/dist/launch.cjs.map +1 -1
- package/dist/launch.d.cts +3 -0
- package/dist/launch.d.ts +3 -0
- package/dist/launch.js +45 -6
- package/dist/launch.js.map +1 -1
- package/dist/llm.cjs.map +1 -1
- package/dist/llm.d.cts +20 -3
- package/dist/llm.d.ts +20 -3
- package/dist/llm.js.map +1 -1
- package/dist/metadataQueryTool.cjs +271 -0
- package/dist/metadataQueryTool.cjs.map +1 -0
- package/dist/metadataQueryTool.d.cts +159 -0
- package/dist/metadataQueryTool.d.ts +159 -0
- package/dist/metadataQueryTool.js +241 -0
- package/dist/metadataQueryTool.js.map +1 -0
- package/dist/mounts.cjs.map +1 -1
- package/dist/mounts.d.cts +22 -3
- package/dist/mounts.d.ts +22 -3
- package/dist/mounts.js.map +1 -1
- package/dist/onFsChange.cjs +13 -1
- package/dist/onFsChange.cjs.map +1 -1
- package/dist/onFsChange.d.cts +21 -1
- package/dist/onFsChange.d.ts +21 -1
- package/dist/onFsChange.js +13 -1
- package/dist/onFsChange.js.map +1 -1
- package/dist/protocolSchemes.cjs +1 -0
- package/dist/protocolSchemes.cjs.map +1 -1
- package/dist/protocolSchemes.d.cts +1 -0
- package/dist/protocolSchemes.d.ts +1 -0
- package/dist/protocolSchemes.js +2 -0
- package/dist/protocolSchemes.js.map +1 -1
- package/dist/recents.cjs +41 -0
- package/dist/recents.cjs.map +1 -0
- package/dist/recents.d.cts +23 -0
- package/dist/recents.d.ts +23 -0
- package/dist/recents.js +17 -0
- package/dist/recents.js.map +1 -0
- package/dist/tasks.cjs +41 -7
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.d.cts +136 -7
- package/dist/tasks.d.ts +136 -7
- package/dist/tasks.js +39 -7
- package/dist/tasks.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/workspace.cjs +53 -0
- package/dist/workspace.cjs.map +1 -0
- package/dist/workspace.d.cts +58 -0
- package/dist/workspace.d.ts +58 -0
- package/dist/workspace.js +28 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +6 -5
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import "./chunk-VHAA22YE.js";
|
|
2
|
+
import { anySignal, steerWireText, INTERRUPTED_TURN_TEXT } from "./agentSteering";
|
|
3
|
+
const textOf = (blocks) => blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
4
|
+
const TERMINAL_STOPS = /* @__PURE__ */ new Set(["max_tokens", "refusal"]);
|
|
5
|
+
const INTENT_RE = /\b(i'?ll|i will|i'?m going to|going to|let me|let's|now,? i(?:'?ll| will)?|next,? i(?:'?ll| will)?)\b[\s\S]{0,80}?\b(read|write|edit|creat|add|updat|modif|regist|check|look|call|run|search|grep|list|open|fetch|inspect|review|explor|implement|fix|appl)/i;
|
|
6
|
+
const DONE_RE = /\b(done|complete|finished|all set|no (?:further|more) (?:changes|steps)|i(?:'| ha)ve (?:creat|add|updat|made|written|regist|edit|implement|fix|appli)|here'?s (?:a |the )?summ|to summ|in summ)/i;
|
|
7
|
+
function detectStall(text) {
|
|
8
|
+
const t = text.trim();
|
|
9
|
+
if (!t) return "empty";
|
|
10
|
+
if (DONE_RE.test(t)) return null;
|
|
11
|
+
if (INTENT_RE.test(t)) return "announced-no-call";
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const NUDGE_TEXT = "You ended your turn without calling a tool. If the task is already complete, say so plainly in one line and stop. Otherwise don't just describe the next step \u2014 emit the tool call now.";
|
|
15
|
+
function estimateTokens(messages) {
|
|
16
|
+
let chars = 0;
|
|
17
|
+
for (const m of messages) {
|
|
18
|
+
for (const b of m.content) {
|
|
19
|
+
if (b.type === "text") chars += b.text.length;
|
|
20
|
+
else if (b.type === "tool_use") chars += JSON.stringify(b.input).length + b.name.length;
|
|
21
|
+
else if (b.type === "tool_result") chars += b.content.length;
|
|
22
|
+
else if (b.type === "image") chars += b.data.length;
|
|
23
|
+
else if (b.type === "reasoning") chars += b.text.length + (b.redactedData?.length ?? 0);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return Math.ceil(chars / 4);
|
|
27
|
+
}
|
|
28
|
+
function shouldCompact(contextTokens, window, reserveTokens) {
|
|
29
|
+
if (!window || window <= 0) return false;
|
|
30
|
+
return contextTokens > window - reserveTokens;
|
|
31
|
+
}
|
|
32
|
+
const COMPACTION_MARKER = "\u241F[compacted-context]\n";
|
|
33
|
+
const SUMMARY_SYSTEM = "You are compacting a coding-agent transcript to fit the context window. Produce a DENSE structured summary under these exact headings: Goal / Constraints / Progress / Decisions / Next Steps / Critical Context. PRESERVE VERBATIM every file path, symbol/identifier, and error string that later steps will need \u2014 do not paraphrase them. Be terse everywhere else. Output only the summary.";
|
|
34
|
+
const SUMMARY_INSTRUCTION = "Summarize everything above into the structured block. Keep exact paths, symbols, and error strings verbatim so work can continue from the summary alone.";
|
|
35
|
+
async function compactTranscript(messages, client, keepRecentTurns) {
|
|
36
|
+
if (messages.length <= keepRecentTurns + 1) return { messages, summarizedCount: 0 };
|
|
37
|
+
const boundary = Math.max(1, messages.length - keepRecentTurns);
|
|
38
|
+
let tailStart = -1;
|
|
39
|
+
for (let i = boundary; i < messages.length; i++) {
|
|
40
|
+
if (messages[i].role === "assistant") {
|
|
41
|
+
tailStart = i;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (tailStart === -1) {
|
|
46
|
+
for (let i = messages.length - 1; i >= 1; i--) {
|
|
47
|
+
if (messages[i].role === "assistant") {
|
|
48
|
+
tailStart = i;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (tailStart <= 0) return { messages, summarizedCount: 0 };
|
|
54
|
+
const head = messages.slice(0, tailStart);
|
|
55
|
+
const tail = messages.map(dropImages).map(dropReasoning).slice(tailStart);
|
|
56
|
+
const reqMessages = head.map((m) => ({ role: m.role, content: [...m.content] }));
|
|
57
|
+
const lastMsg = reqMessages[reqMessages.length - 1];
|
|
58
|
+
if (lastMsg && lastMsg.role === "user") {
|
|
59
|
+
lastMsg.content = [...lastMsg.content, { type: "text", text: SUMMARY_INSTRUCTION }];
|
|
60
|
+
} else {
|
|
61
|
+
reqMessages.push({ role: "user", content: [{ type: "text", text: SUMMARY_INSTRUCTION }] });
|
|
62
|
+
}
|
|
63
|
+
let summaryText = "(summary unavailable)";
|
|
64
|
+
try {
|
|
65
|
+
const res = await client.createMessage({ system: SUMMARY_SYSTEM, messages: reqMessages, tools: [] });
|
|
66
|
+
summaryText = textOf(res.content).trim() || summaryText;
|
|
67
|
+
} catch {
|
|
68
|
+
return { messages, summarizedCount: 0 };
|
|
69
|
+
}
|
|
70
|
+
const summaryMsg = {
|
|
71
|
+
role: "user",
|
|
72
|
+
content: [{ type: "text", text: COMPACTION_MARKER + summaryText }]
|
|
73
|
+
};
|
|
74
|
+
return { messages: [summaryMsg, ...tail], summarizedCount: head.length };
|
|
75
|
+
}
|
|
76
|
+
function dropBlocks(m, kind) {
|
|
77
|
+
if (!m.content.some((b) => b.type === kind)) return m;
|
|
78
|
+
const kept = m.content.filter((b) => b.type !== kind);
|
|
79
|
+
return { role: m.role, content: kept.length ? kept : [{ type: "text", text: "" }] };
|
|
80
|
+
}
|
|
81
|
+
const dropImages = (m) => dropBlocks(m, "image");
|
|
82
|
+
const dropReasoning = (m) => dropBlocks(m, "reasoning");
|
|
83
|
+
function isContextOverflow(e) {
|
|
84
|
+
const msg = (e?.message ?? String(e)).toLowerCase();
|
|
85
|
+
const code = String(e?.code ?? "").toLowerCase();
|
|
86
|
+
return code.includes("context_length") || code.includes("context-length") || /context (?:length|window)|maximum context|too many tokens|prompt is too long|reduce the length/.test(msg);
|
|
87
|
+
}
|
|
88
|
+
const TRUNCATED_RETRY_TEXT = "That turn was cut off at the token limit mid tool-call, so the call was NOT executed. Emit a smaller step: fewer/shorter tool calls, or a smaller file write.";
|
|
89
|
+
async function runAgent(opts) {
|
|
90
|
+
const { client, tools, execute, system, prompt, events, signal, steering } = opts;
|
|
91
|
+
const maxTurns = opts.maxTurns ?? 100;
|
|
92
|
+
const maxNudges = opts.maxNudges ?? 1;
|
|
93
|
+
const maxTruncationRetries = opts.maxTruncationRetries ?? 2;
|
|
94
|
+
const window = opts.contextWindow;
|
|
95
|
+
const reserveTokens = opts.reserveTokens ?? (window ? Math.floor(window * 0.25) : 0);
|
|
96
|
+
const keepRecentTurns = opts.keepRecentTurns ?? 8;
|
|
97
|
+
let messages = [...opts.history ?? [], { role: "user", content: [{ type: "text", text: prompt }] }];
|
|
98
|
+
let nudges = 0;
|
|
99
|
+
let truncationRetries = 0;
|
|
100
|
+
let interruptedLastTurn = false;
|
|
101
|
+
let contextTokens = 0;
|
|
102
|
+
let spentTokens = 0;
|
|
103
|
+
let cacheReadTokens;
|
|
104
|
+
let cacheWriteTokens;
|
|
105
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
106
|
+
if (signal?.aborted) break;
|
|
107
|
+
if (steering) {
|
|
108
|
+
const steers = steering.drain();
|
|
109
|
+
if (steers.length) {
|
|
110
|
+
messages.push({
|
|
111
|
+
role: "user",
|
|
112
|
+
content: steers.map((m) => ({ type: "text", text: steerWireText(m) }))
|
|
113
|
+
});
|
|
114
|
+
events?.onSteer?.({ messages: steers, interrupted: interruptedLastTurn });
|
|
115
|
+
}
|
|
116
|
+
interruptedLastTurn = false;
|
|
117
|
+
steering.rearm();
|
|
118
|
+
}
|
|
119
|
+
if (shouldCompact(contextTokens, window, reserveTokens)) {
|
|
120
|
+
const { messages: compacted, summarizedCount } = await compactTranscript(messages, client, keepRecentTurns);
|
|
121
|
+
if (summarizedCount > 0) {
|
|
122
|
+
messages = compacted;
|
|
123
|
+
contextTokens = estimateTokens(messages);
|
|
124
|
+
events?.onCompact?.({
|
|
125
|
+
summarizedCount,
|
|
126
|
+
...cacheReadTokens !== void 0 ? { cacheReadTokens } : {},
|
|
127
|
+
...cacheWriteTokens !== void 0 ? { cacheWriteTokens } : {}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const turnAbort = anySignal([signal, steering?.interrupt]);
|
|
132
|
+
let partialText = "";
|
|
133
|
+
const onTextDelta = (text) => {
|
|
134
|
+
partialText += text;
|
|
135
|
+
events?.onAssistantDelta?.(text);
|
|
136
|
+
};
|
|
137
|
+
const sendTurn = () => client.createMessage({
|
|
138
|
+
system,
|
|
139
|
+
messages,
|
|
140
|
+
tools,
|
|
141
|
+
// R3-333's local `onTextDelta` (it captures the partial text a steer may cut
|
|
142
|
+
// short) — NOT `events.onAssistantDelta` directly.
|
|
143
|
+
onTextDelta,
|
|
144
|
+
// R3-335's reasoning stream rides alongside it.
|
|
145
|
+
onReasoningDelta: events?.onReasoningDelta,
|
|
146
|
+
// R3-333: STOP composed with the steer INTERRUPT, so either verb ends the turn.
|
|
147
|
+
signal: turnAbort.signal
|
|
148
|
+
});
|
|
149
|
+
let res;
|
|
150
|
+
try {
|
|
151
|
+
try {
|
|
152
|
+
res = await sendTurn();
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (turnAbort.signal.aborted || !isContextOverflow(e)) throw e;
|
|
155
|
+
const { messages: compacted, summarizedCount } = await compactTranscript(messages, client, keepRecentTurns);
|
|
156
|
+
if (summarizedCount === 0) throw e;
|
|
157
|
+
messages = compacted;
|
|
158
|
+
contextTokens = estimateTokens(messages);
|
|
159
|
+
events?.onCompact?.({
|
|
160
|
+
summarizedCount,
|
|
161
|
+
...cacheReadTokens !== void 0 ? { cacheReadTokens } : {},
|
|
162
|
+
...cacheWriteTokens !== void 0 ? { cacheWriteTokens } : {}
|
|
163
|
+
});
|
|
164
|
+
res = await sendTurn();
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
if (signal?.aborted) {
|
|
168
|
+
turnAbort.dispose();
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (steering?.interrupt.aborted) {
|
|
172
|
+
turnAbort.dispose();
|
|
173
|
+
messages.push({
|
|
174
|
+
role: "assistant",
|
|
175
|
+
content: [{ type: "text", text: partialText.trim() || INTERRUPTED_TURN_TEXT }]
|
|
176
|
+
});
|
|
177
|
+
events?.onAssistantText?.(partialText.trim() || INTERRUPTED_TURN_TEXT);
|
|
178
|
+
interruptedLastTurn = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
turnAbort.dispose();
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
turnAbort.dispose();
|
|
185
|
+
const turnCost = res.usage ? res.usage.inputTokens + res.usage.outputTokens : estimateTokens(messages) + Math.ceil(textOf(res.content).length / 4);
|
|
186
|
+
contextTokens = turnCost;
|
|
187
|
+
spentTokens += turnCost;
|
|
188
|
+
if (res.usage?.cacheReadTokens !== void 0) {
|
|
189
|
+
cacheReadTokens = (cacheReadTokens ?? 0) + res.usage.cacheReadTokens;
|
|
190
|
+
}
|
|
191
|
+
if (res.usage?.cacheWriteTokens !== void 0) {
|
|
192
|
+
cacheWriteTokens = (cacheWriteTokens ?? 0) + res.usage.cacheWriteTokens;
|
|
193
|
+
}
|
|
194
|
+
events?.onUsage?.({
|
|
195
|
+
contextTokens,
|
|
196
|
+
window,
|
|
197
|
+
spentTokens,
|
|
198
|
+
...cacheReadTokens !== void 0 ? { cacheReadTokens } : {},
|
|
199
|
+
...cacheWriteTokens !== void 0 ? { cacheWriteTokens } : {}
|
|
200
|
+
});
|
|
201
|
+
const assistantText = textOf(res.content);
|
|
202
|
+
if (assistantText) events?.onAssistantText?.(assistantText);
|
|
203
|
+
for (const b of res.content) if (b.type === "reasoning") events?.onReasoning?.(b);
|
|
204
|
+
messages.push({ role: "assistant", content: res.content });
|
|
205
|
+
const toolUses = res.content.filter((b) => b.type === "tool_use");
|
|
206
|
+
if (res.stopReason === "max_tokens" && toolUses.length > 0) {
|
|
207
|
+
events?.onTruncatedToolCall?.();
|
|
208
|
+
const failed = toolUses.map((c) => ({
|
|
209
|
+
type: "tool_result",
|
|
210
|
+
tool_use_id: c.id,
|
|
211
|
+
content: "tool call truncated by the token limit \u2014 not executed",
|
|
212
|
+
is_error: true
|
|
213
|
+
}));
|
|
214
|
+
failed.push({ type: "text", text: TRUNCATED_RETRY_TEXT });
|
|
215
|
+
messages.push({ role: "user", content: failed });
|
|
216
|
+
if (++truncationRetries > maxTruncationRetries) break;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
truncationRetries = 0;
|
|
220
|
+
if (toolUses.length === 0) {
|
|
221
|
+
const stall = TERMINAL_STOPS.has(res.stopReason) ? null : detectStall(assistantText);
|
|
222
|
+
if (stall && nudges < maxNudges) {
|
|
223
|
+
nudges++;
|
|
224
|
+
events?.onNudge?.(stall);
|
|
225
|
+
messages.push({ role: "user", content: [{ type: "text", text: NUDGE_TEXT }] });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (steering?.hasPending()) continue;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
nudges = 0;
|
|
232
|
+
const results = [];
|
|
233
|
+
const images = [];
|
|
234
|
+
for (const call of toolUses) {
|
|
235
|
+
events?.onToolUse?.(call.name, call.input);
|
|
236
|
+
let outcome;
|
|
237
|
+
try {
|
|
238
|
+
outcome = await execute(call.name, call.input);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
const code = e?.code;
|
|
241
|
+
const msg = e?.message ?? String(e);
|
|
242
|
+
outcome = { content: code ? `${code}: ${msg}` : msg, isError: true };
|
|
243
|
+
}
|
|
244
|
+
events?.onToolResult?.(call.name, outcome);
|
|
245
|
+
results.push({
|
|
246
|
+
type: "tool_result",
|
|
247
|
+
tool_use_id: call.id,
|
|
248
|
+
content: outcome.content,
|
|
249
|
+
is_error: outcome.isError
|
|
250
|
+
});
|
|
251
|
+
if (outcome.images?.length) images.push(...outcome.images);
|
|
252
|
+
}
|
|
253
|
+
messages.push({ role: "user", content: [...results, ...images] });
|
|
254
|
+
if (opts.tokenBudget && spentTokens >= opts.tokenBudget) {
|
|
255
|
+
events?.onBudgetStop?.({ spentTokens, tokenBudget: opts.tokenBudget });
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return messages;
|
|
260
|
+
}
|
|
261
|
+
export {
|
|
262
|
+
COMPACTION_MARKER,
|
|
263
|
+
NUDGE_TEXT,
|
|
264
|
+
compactTranscript,
|
|
265
|
+
detectStall,
|
|
266
|
+
estimateTokens,
|
|
267
|
+
isContextOverflow,
|
|
268
|
+
runAgent,
|
|
269
|
+
shouldCompact
|
|
270
|
+
};
|
|
271
|
+
//# sourceMappingURL=agentLoop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/agentLoop.ts"],"sourcesContent":["// Provider-agnostic agentic tool-use loop — PORTED from agent-demo `src/lib/agentLoop.ts`\n// (GROVE_AGENT_SPEC §7: embedded agents REUSE this seam rather than reimplementing it;\n// the SDK is its shared home so every app's agent loop is the same exercised machinery).\n// Renames for the SDK's flat export surface: `ChatMessage`→`AgentMessage`, `Role`→`AgentRole`.\n// Provider-agnostic agentic tool-use loop (LLM_AND_AGENTS_SPEC §3.3). The loop is\n// the heart of the in-browser coding agent: send the conversation + tool list to a\n// ModelClient, execute any tool calls the model emits, append the results, and\n// repeat until the model stops, a spend budget is hit, or a large safety-stop is\n// reached. The ModelClient seam keeps the loop independent of any one provider\n// (host `chat()` impl: chatModelClient.ts).\n//\n// Confinement (G12/T24) is NOT enforced here — it falls out of the capability\n// model: the `tools` handed to the model ARE the app's grant-filtered §5.5\n// catalog (agentTools.ts), and `execute` routes through the host's gated\n// `invoke()`, so an off-catalog/hallucinated tool returns `forbidden` at the host.\n//\n// R3-220 (AHG-1) adds the machinery that lets the loop run LONG enough to build a\n// real app: token accounting (from the provider `usage` delta), automatic context\n// COMPACTION when the window fills, a truncated-tool-call guard, and a spend budget\n// replacing the old fixed 12-turn cap. All of it is inert unless a `contextWindow`\n// is supplied, so a caller that passes none behaves exactly as before.\n//\n// PREFIX STABILITY IS LOAD-BEARING (R3-336). The loop's contribution to prompt caching\n// is structural, not a parameter: `system` and `tools` are fixed for a run and are sent\n// BYTE-IDENTICALLY on every turn, while everything that changes is appended to\n// `messages`. That is what the host's cache breakpoints key on. Rebuilding the system\n// prompt per turn — re-stamping a date, re-ordering the tool list — would cost nothing\n// visible and silently turn every cache read into a cache write, so it is asserted in\n// the tests rather than left as a convention.\n\nimport { anySignal, steerWireText, INTERRUPTED_TURN_TEXT, type SteerMessage, type SteerSource } from './agentSteering';\n\nexport type TextBlock = { type: 'text'; text: string };\n/**\n * An image the model can look at (R3-339). `data` is base64 with no `data:` prefix,\n * matching the SDK `ContentPart` the transport already accepts.\n *\n * Carried as its OWN block rather than stuffed inside a `tool_result`, because a tool\n * result's content is a string on the wire — the loop appends the image to the same\n * user message that carries the results, which is the shape both host adapters map.\n */\nexport type ImageBlock = { type: 'image'; mimeType: string; data: string };\n/**\n * A block of the model's own reasoning (R3-335).\n *\n * Kept in the message sequence rather than rendered and thrown away, for two reasons:\n * the user needs to see what the model is doing during the long stretches compaction\n * now makes possible, and some providers REQUIRE the block echoed back — with its\n * `signature` — for the following turn of a tool-use chain to stay valid. A loop that\n * drops them is quietly lossy in a way that shows up as degraded output, not an error.\n *\n * `redactedData` carries provider-redacted reasoning: opaque bytes with no readable\n * text, which still have to be replayed in place. Never render it.\n */\nexport type ReasoningBlock = {\n type: 'reasoning';\n text: string;\n signature?: string;\n redactedData?: string;\n};\nexport type ToolUseBlock = { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> };\nexport type ToolResultBlock = { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean };\nexport type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ImageBlock | ReasoningBlock;\n\n/** A tool the model may call: name, description, and a JSON Schema for its input\n * (`input_schema`, the Anthropic wire name — {@link createChatModelClient} maps it to the\n * chat slot's `ToolDef`). */\nexport interface AgentTool {\n name: string;\n description: string;\n input_schema: Record<string, unknown>;\n}\n\nexport type AgentRole = 'user' | 'assistant';\nexport interface AgentMessage {\n role: AgentRole;\n content: ContentBlock[];\n}\n\n/** Provider-reported token counts for one turn (R3-220). `inputTokens` is the size\n * of everything the provider processed this turn; `outputTokens` is what it\n * generated. Absent when the provider emits no `usage` delta. */\nexport interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n /** R3-336 — prompt-cache counters, present only where the provider reports them.\n * ABSENT is not zero: it means this provider says nothing about caching, which is a\n * different fact from \"nothing was cached\", and conflating them would turn a\n * measurement into a guess. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n}\n\n/** One model turn: the assistant's emitted blocks + why it stopped (+ usage). */\nexport interface ModelResponse {\n content: (TextBlock | ToolUseBlock | ReasoningBlock)[];\n /** Anthropic stop_reason: 'end_turn' | 'tool_use' | 'max_tokens' | 'refusal' | … */\n stopReason: string;\n /** Provider token counts for this turn, when reported (R3-220 accounting). */\n usage?: TokenUsage;\n}\n\n/** The provider seam — one model turn. Implemented by `chatModelClient.ts` over\n * the host `chat()` slot; faked in tests. When the client streams, it calls\n * `onTextDelta` with each token slice as it arrives (the assembled turn is still\n * returned whole); a non-streaming client simply never calls it. */\nexport interface ModelClient {\n createMessage(req: {\n system?: string;\n messages: AgentMessage[];\n tools: AgentTool[];\n /** Called with incremental assistant-text slices during a streamed turn. */\n onTextDelta?: (text: string) => void;\n /** R3-335: incremental REASONING slices, for a live thinking surface. Never called\n * by a provider that does not emit reasoning. */\n onReasoningDelta?: (text: string) => void;\n /** R3-224: aborts the in-flight turn — the host stops the upstream provider\n * request and stops billing, not just the app-side stream (§3.3). */\n signal?: AbortSignal;\n }): Promise<ModelResponse>;\n}\n\n/** Executes one tool call, returning a string result (and whether it errored —\n * a `forbidden`/failed call comes back as `is_error` so the model can adapt). */\nexport type ToolExecutor = (name: string, input: Record<string, unknown>) => Promise<ToolOutcome>;\n\n/** What one tool call produced. `images` (R3-339) is how a tool hands the model\n * something to LOOK at; `content` still carries the text the model reads. */\nexport interface ToolOutcome {\n content: string;\n isError?: boolean;\n images?: ImageBlock[];\n}\n\n/** Why a no-tool-call turn looked like a stall rather than a genuine finish. */\nexport type StallReason = 'empty' | 'announced-no-call';\n\n/** Optional UI hooks so a panel can render the loop as it runs. */\nexport interface AgentEvents {\n /** A streamed token slice of the in-flight assistant turn (live preview). */\n onAssistantDelta?(text: string): void;\n /** The complete assistant text for a turn, once the turn is in. */\n onAssistantText?(text: string): void;\n onToolUse?(name: string, input: Record<string, unknown>): void;\n onToolResult?(name: string, result: ToolOutcome): void;\n /** Fired when the loop nudges a STALLED turn (the model ended without a tool\n * call despite empty or \"I'll do X\" intent text) back into action, so a panel\n * can show \"nudging the model to continue\" rather than a silent stall. */\n onNudge?(reason: StallReason): void;\n /** Fired after every turn with the running context size + window (R3-220\n * loop-observability). `contextTokens` is provider-reported when available, else\n * a char/4 estimate. */\n onUsage?(usage: {\n contextTokens: number;\n window?: number;\n spentTokens: number;\n /** R3-336 — cumulative cache reads/writes across the run, on providers that report\n * them. Surfacing this is what makes the caching claim verifiable rather than\n * believed; `undefined` means the provider reported nothing. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n }): void;\n /** Fired when the loop compacts the transcript to stay under the context window;\n * `summarizedCount` is how many older messages were folded into the summary.\n *\n * R3-336: a compaction invalidates the conversation-prefix cache it rewrote — the\n * durable system+tools prefix survives it — so the next turn pays one prefix\n * re-write. `cacheReadTokens`/`cacheWriteTokens` are the run totals AT the\n * compaction, which is what lets the cost curve across it be read off rather than\n * assumed (exit 2). */\n onCompact?(info: { summarizedCount: number; cacheReadTokens?: number; cacheWriteTokens?: number }): void;\n /** Fired when the loop stops because the token/spend budget was exhausted. */\n onBudgetStop?(info: { spentTokens: number; tokenBudget: number }): void;\n /** Fired when a turn was truncated (`max_tokens`) while emitting tool calls, so\n * the partial calls were failed-and-re-prompted rather than executed (R3-220 F3). */\n onTruncatedToolCall?(): void;\n /** R3-335: a streamed slice of the model's reasoning, for a live thinking surface. */\n onReasoningDelta?(text: string): void;\n /** R3-335: the complete reasoning block for a turn, once the turn is in. */\n onReasoning?(block: ReasoningBlock): void;\n /** R3-333: the loop applied the user's mid-run correction(s). `interrupted` is\n * true when an `interrupt`-mode steer cut an in-flight model turn short (as\n * opposed to being applied at an ordinary turn boundary). */\n onSteer?(info: { messages: SteerMessage[]; interrupted: boolean }): void;\n}\n\nexport interface RunAgentOptions {\n client: ModelClient;\n tools: AgentTool[];\n execute: ToolExecutor;\n system?: string;\n /** Prior turns of this conversation, replayed before the new prompt so a\n * follow-up has context (the conversation stage seeds this from the store). */\n history?: AgentMessage[];\n /** The user's instruction that kicks off the loop. */\n prompt: string;\n /** Large safety-stop on model turns (default 100). No longer the primary bound —\n * a long task is bounded by `tokenBudget` + compaction; this just backstops a\n * pathological loop the budget/compaction somehow miss. */\n maxTurns?: number;\n /** Max consecutive \"you announced work but emitted no tool call\" nudges before\n * the loop gives up (default 1). GLM-over-OpenRouter intermittently ends a turn\n * with future-tense intent (\"I'll read the files…\") or an EMPTY turn right after\n * a tool error — no tool call, a silent stall (tutorial findings §2). One nudge\n * recovers most of these; the cap keeps a genuinely-finished model (which answers\n * the nudge with another call-free turn) from looping, and the budget resets on\n * any turn that DID call a tool, so a long task's later stall is still covered.\n * Set 0 to disable the backstop. */\n maxNudges?: number;\n // ---- R3-220 accounting / compaction (all inert unless `contextWindow` is set) ----\n /** The resolved provider's context window (`describeChat().features.maxContextTokens`).\n * Compaction is disabled when this is absent/0 — the loop then behaves as before. */\n contextWindow?: number;\n /** Headroom left below the window before compacting (default: 25% of the window). */\n reserveTokens?: number;\n /** Recent messages kept verbatim across a compaction (default 8). */\n keepRecentTurns?: number;\n /** Cumulative token budget (input+output across turns). When exceeded the loop\n * stops — the runaway-cost guard that replaces the raw 12-turn cap. Off when unset. */\n tokenBudget?: number;\n /** Max consecutive truncated-tool-call re-prompts before giving up (default 2). */\n maxTruncationRetries?: number;\n /** R3-224 (§3.3): the stop button. When it fires the loop stops between turns AND\n * aborts the in-flight model turn (the host tears down the upstream provider\n * request and stops billing) — not merely the between-turn loop. The transcript so\n * far is returned; an abort is a clean stop, never a thrown error. */\n signal?: AbortSignal;\n /** R3-333: the mid-run steering queue. The loop drains it at every turn boundary\n * and folds each correction in as a `user` message, so the human can redirect a\n * run without restarting it and paying for the transcript again. Its `interrupt`\n * signal aborts the in-flight MODEL turn only — never a tool batch, which must\n * keep every `tool_use` paired with a `tool_result`. Absent ⇒ the loop behaves\n * exactly as before. */\n steering?: SteerSource;\n events?: AgentEvents;\n}\n\nconst textOf = (blocks: { type: string; text?: string }[]): string =>\n blocks\n .filter((b): b is TextBlock => b.type === 'text')\n .map((b) => b.text)\n .join('');\n\n// Terminal stops we must NOT nudge past. Only `max_tokens` survives the SDK→loop\n// mapping distinctly (chatModelClient `mapStop`: 'length'→'max_tokens', while\n// 'end'/'filtered'→'end_turn' and 'tool'→'tool_use'); a truncated turn is a\n// token-budget problem a nudge can't fix. An empty give-up after a tool error\n// arrives as 'end_turn', so it stays nudgeable.\nconst TERMINAL_STOPS = new Set(['max_tokens', 'refusal']);\n\n// Future-tense intent to ACT (\"I'll read…\", \"let me create…\", \"next I'll edit…\").\nconst INTENT_RE =\n /\\b(i'?ll|i will|i'?m going to|going to|let me|let's|now,? i(?:'?ll| will)?|next,? i(?:'?ll| will)?)\\b[\\s\\S]{0,80}?\\b(read|write|edit|creat|add|updat|modif|regist|check|look|call|run|search|grep|list|open|fetch|inspect|review|explor|implement|fix|appl)/i;\n// A wrap-up marker → treat the turn as a genuine finish, never nudge.\nconst DONE_RE =\n /\\b(done|complete|finished|all set|no (?:further|more) (?:changes|steps)|i(?:'| ha)ve (?:creat|add|updat|made|written|regist|edit|implement|fix|appli)|here'?s (?:a |the )?summ|to summ|in summ)/i;\n\n/**\n * Classify a NO-tool-call turn as a stall (nudge-worthy) vs a genuine finish.\n * GLM-over-OpenRouter intermittently (a) writes \"I'll read the files…\" then ends\n * with no call, or (b) returns an EMPTY turn after a tool error — both silent\n * give-ups (tutorial findings §2). Conservative on purpose: a real wrap-up (a\n * summary, \"Done\", \"I've created…\") returns null so the loop never nudges a\n * finished agent. Empty text is always a stall (there is nothing a finished agent\n * would say with zero words).\n */\nexport function detectStall(text: string): StallReason | null {\n const t = text.trim();\n if (!t) return 'empty';\n if (DONE_RE.test(t)) return null;\n if (INTENT_RE.test(t)) return 'announced-no-call';\n return null;\n}\n\n// The single follow-up we inject to break a stall. Directive, short, and honest\n// about the two outcomes so a genuinely-finished model just confirms and stops\n// (→ another call-free turn, which the nudge cap then lets terminate). Exported so\n// the transcript renderer can recognise the injected turn and show it as a \"nudge\"\n// row (not a user message) when a persisted conversation is replayed.\nexport const NUDGE_TEXT =\n \"You ended your turn without calling a tool. If the task is already complete, say so plainly in one line and stop. Otherwise don't just describe the next step — emit the tool call now.\";\n\n// ---- R3-220 token accounting + compaction ----------------------------------------\n\n/** Rough token estimate (~4 chars/token) over a message array, used only when the\n * provider reports no `usage` delta. Conservative by design (over- not under-counts\n * by treating structured blocks as their JSON length). */\nexport function estimateTokens(messages: AgentMessage[]): number {\n let chars = 0;\n for (const m of messages) {\n for (const b of m.content) {\n if (b.type === 'text') chars += b.text.length;\n else if (b.type === 'tool_use') chars += JSON.stringify(b.input).length + b.name.length;\n else if (b.type === 'tool_result') chars += b.content.length;\n // R3-339: an image is large and MUST be accounted for, or it escapes exactly the\n // budget the accounting exists to enforce. base64 is ~4/3 of the bytes, and the\n // provider bills tokens per pixel area — the base64 length is the honest local\n // proxy for \"this is big\", and over-counting is the safe direction.\n else if (b.type === 'image') chars += b.data.length;\n // R3-335: reasoning occupies the window like anything else. Not counting it would\n // let a thinking model overrun the context the accounting exists to protect.\n else if (b.type === 'reasoning') chars += b.text.length + (b.redactedData?.length ?? 0);\n }\n }\n return Math.ceil(chars / 4);\n}\n\n/** Should the loop compact now? True once the running context passes\n * `window − reserveTokens`. Disabled (false) when there is no window. */\nexport function shouldCompact(contextTokens: number, window: number | undefined, reserveTokens: number): boolean {\n if (!window || window <= 0) return false;\n return contextTokens > window - reserveTokens;\n}\n\n/** Prefix marking a `user` message as a compaction summary (not a real user turn),\n * so the transcript renderer shows a \"compacted N turns\" affordance on replay. */\nexport const COMPACTION_MARKER = '␟[compacted-context]\\n';\n\nconst SUMMARY_SYSTEM =\n 'You are compacting a coding-agent transcript to fit the context window. Produce a ' +\n 'DENSE structured summary under these exact headings: Goal / Constraints / Progress / ' +\n 'Decisions / Next Steps / Critical Context. PRESERVE VERBATIM every file path, symbol/' +\n 'identifier, and error string that later steps will need — do not paraphrase them. Be ' +\n 'terse everywhere else. Output only the summary.';\nconst SUMMARY_INSTRUCTION =\n 'Summarize everything above into the structured block. Keep exact paths, symbols, and ' +\n 'error strings verbatim so work can continue from the summary alone.';\n\n/** Compact `messages` by folding the older head into a structured summary and keeping\n * a verbatim recent tail. The tail is snapped to start at an `assistant` message so a\n * `tool_use`/`tool_result` pair is never split (which would malform the next request).\n * The taint tier is NOT modelled on messages (it is run-scoped host state, R-ASG-2):\n * this is a pure content transform over the SAME session — it starts no new external\n * read — so it cannot launder taint (F6). Returns the original array unchanged when\n * there is nothing safe to summarize. */\nexport async function compactTranscript(\n messages: AgentMessage[],\n client: ModelClient,\n keepRecentTurns: number,\n): Promise<{ messages: AgentMessage[]; summarizedCount: number }> {\n if (messages.length <= keepRecentTurns + 1) return { messages, summarizedCount: 0 };\n\n // Snap the tail boundary to an assistant message so tool_use/tool_result pairs stay\n // together and the summary (a `user` turn) is followed by an `assistant` turn.\n // Prefer the first assistant at/after the keep-recent boundary; fall back to the\n // last assistant in the transcript so the tail is always well-formed.\n const boundary = Math.max(1, messages.length - keepRecentTurns);\n let tailStart = -1;\n for (let i = boundary; i < messages.length; i++) {\n if (messages[i].role === 'assistant') {\n tailStart = i;\n break;\n }\n }\n if (tailStart === -1) {\n for (let i = messages.length - 1; i >= 1; i--) {\n if (messages[i].role === 'assistant') {\n tailStart = i;\n break;\n }\n }\n }\n if (tailStart <= 0) return { messages, summarizedCount: 0 };\n\n const head = messages.slice(0, tailStart);\n // Compaction DROPS both image parts (R3-339) and reasoning (R3-335) from the kept\n // tail, each by an explicit rule — an implicit answer here is what corrupts a\n // transcript quietly.\n //\n // IMAGES: the largest and least summarisable thing in a transcript, and the summary\n // the head folds into is TEXT. The `tool_result` that named the image stays, so the\n // model still knows it looked at `assets/mock.png` and what it concluded; it simply\n // cannot look again without re-reading the file, which it can do.\n //\n // REASONING: only ever required by the turn that FOLLOWS it, and compaction rewrites\n // at a turn boundary — so nothing after it is mid-chain and nothing needs the block\n // replayed. Keeping them would spend the window on its most disposable content.\n const tail = messages.map(dropImages).map(dropReasoning).slice(tailStart);\n\n // Ask the model to summarize the head. Append the instruction to the final head\n // message when it is a `user` turn (avoids introducing consecutive user turns).\n const reqMessages: AgentMessage[] = head.map((m) => ({ role: m.role, content: [...m.content] }));\n const lastMsg = reqMessages[reqMessages.length - 1];\n if (lastMsg && lastMsg.role === 'user') {\n lastMsg.content = [...lastMsg.content, { type: 'text', text: SUMMARY_INSTRUCTION }];\n } else {\n reqMessages.push({ role: 'user', content: [{ type: 'text', text: SUMMARY_INSTRUCTION }] });\n }\n\n let summaryText = '(summary unavailable)';\n try {\n const res = await client.createMessage({ system: SUMMARY_SYSTEM, messages: reqMessages, tools: [] });\n summaryText = textOf(res.content).trim() || summaryText;\n } catch {\n // Summarization itself failed — keep the original transcript (caller will retry\n // or hit the safety-stop). Better a longer context than a lost transcript.\n return { messages, summarizedCount: 0 };\n }\n\n const summaryMsg: AgentMessage = {\n role: 'user',\n content: [{ type: 'text', text: COMPACTION_MARKER + summaryText }],\n };\n return { messages: [summaryMsg, ...tail], summarizedCount: head.length };\n}\n\n/** Strip blocks of one kind from a message, keeping everything else in order. A message\n * left with no content at all keeps a single empty text block so the role sequence\n * stays well-formed (a content-less message is rejected by most providers). */\nfunction dropBlocks(m: AgentMessage, kind: 'image' | 'reasoning'): AgentMessage {\n if (!m.content.some((b) => b.type === kind)) return m;\n const kept = m.content.filter((b) => b.type !== kind);\n return { role: m.role, content: kept.length ? kept : [{ type: 'text', text: '' }] };\n}\n\n/** Compaction's image-drop rule (R3-339). */\nconst dropImages = (m: AgentMessage): AgentMessage => dropBlocks(m, 'image');\n/** Compaction's reasoning-drop rule (R3-335). */\nconst dropReasoning = (m: AgentMessage): AgentMessage => dropBlocks(m, 'reasoning');\n\n/** Does this thrown error look like a hard context-window overflow? Used to trigger\n * recover-then-retry compaction (F3/exit-c) rather than a dead loop. */\nexport function isContextOverflow(e: unknown): boolean {\n const msg = ((e as Error)?.message ?? String(e)).toLowerCase();\n const code = String((e as { code?: unknown })?.code ?? '').toLowerCase();\n return (\n code.includes('context_length') ||\n code.includes('context-length') ||\n /context (?:length|window)|maximum context|too many tokens|prompt is too long|reduce the length/.test(msg)\n );\n}\n\n// The user turn injected when a truncated (`max_tokens`) turn emitted tool calls: we\n// fail the partial calls rather than execute them (F3), and tell the model to retry.\nconst TRUNCATED_RETRY_TEXT =\n 'That turn was cut off at the token limit mid tool-call, so the call was NOT executed. ' +\n 'Emit a smaller step: fewer/shorter tool calls, or a smaller file write.';\n\n/**\n * Drive the agent loop to completion. Returns the full message transcript\n * (including the kickoff user turn). Stops when the model returns without tool\n * calls (or a terminal stop reason), when the token budget is exhausted, or when\n * `maxTurns` (a large safety-stop) is reached. With a `contextWindow` set, the loop\n * accounts tokens and compacts automatically so it can run long.\n */\nexport async function runAgent(opts: RunAgentOptions): Promise<AgentMessage[]> {\n const { client, tools, execute, system, prompt, events, signal, steering } = opts;\n const maxTurns = opts.maxTurns ?? 100;\n const maxNudges = opts.maxNudges ?? 1;\n const maxTruncationRetries = opts.maxTruncationRetries ?? 2;\n const window = opts.contextWindow;\n const reserveTokens = opts.reserveTokens ?? (window ? Math.floor(window * 0.25) : 0);\n const keepRecentTurns = opts.keepRecentTurns ?? 8;\n\n let messages: AgentMessage[] = [...(opts.history ?? []), { role: 'user', content: [{ type: 'text', text: prompt }] }];\n\n // Consecutive-stall counter: how many times in a row we've nudged a no-tool-call\n // turn. Reset to 0 by any turn that DOES call a tool, so the budget is per stall\n // *episode*, not per run.\n let nudges = 0;\n let truncationRetries = 0;\n // True when the previous iteration's model turn was cut short by an `interrupt`\n // steer, so the injected correction can be reported as an interruption.\n let interruptedLastTurn = false;\n // Running context size (provider-reported when available) + cumulative spend.\n let contextTokens = 0;\n let spentTokens = 0;\n // R3-336 — cumulative cache accounting. `undefined` until a provider reports\n // something, so \"reports nothing\" stays distinguishable from \"cached nothing\".\n let cacheReadTokens: number | undefined;\n let cacheWriteTokens: number | undefined;\n\n for (let turn = 0; turn < maxTurns; turn++) {\n // R3-224 (§3.3): the stop button, checked between turns. Combined with the\n // per-request `signal` below (which aborts the in-flight upstream turn), this\n // halts \"the loop between tool calls AND aborts the in-flight LLM request\".\n if (signal?.aborted) break;\n\n // R3-333: apply any queued corrections at the TURN BOUNDARY, before the next\n // request, so the model's very next turn reflects them. Draining here (rather\n // than at the point of arrival) is what makes a steer safe: whatever the loop\n // was doing — streaming a turn, running a tool batch — has finished.\n if (steering) {\n const steers = steering.drain();\n if (steers.length) {\n messages.push({\n role: 'user',\n content: steers.map((m) => ({ type: 'text' as const, text: steerWireText(m) })),\n });\n events?.onSteer?.({ messages: steers, interrupted: interruptedLastTurn });\n }\n interruptedLastTurn = false;\n steering.rearm();\n }\n\n // Compact BEFORE the next request when the running context is near the window.\n if (shouldCompact(contextTokens, window, reserveTokens)) {\n const { messages: compacted, summarizedCount } = await compactTranscript(messages, client, keepRecentTurns);\n if (summarizedCount > 0) {\n messages = compacted;\n contextTokens = estimateTokens(messages);\n events?.onCompact?.({\n summarizedCount,\n ...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),\n ...(cacheWriteTokens !== undefined ? { cacheWriteTokens } : {}),\n });\n }\n }\n\n // The in-flight turn is abortable by EITHER verb: STOP (ends the run) or an\n // `interrupt`-mode STEER (ends the turn, keeps the run). They are composed into\n // one per-turn signal, and told apart in the catch by asking which fired.\n const turnAbort = anySignal([signal, steering?.interrupt]);\n // Capture what the model had streamed when a steer cut in, so the interrupted\n // turn is recorded as what actually happened rather than dropped.\n let partialText = '';\n const onTextDelta = (text: string): void => {\n partialText += text;\n events?.onAssistantDelta?.(text);\n };\n const sendTurn = () =>\n client.createMessage({\n system,\n messages,\n tools,\n // R3-333's local `onTextDelta` (it captures the partial text a steer may cut\n // short) — NOT `events.onAssistantDelta` directly.\n onTextDelta,\n // R3-335's reasoning stream rides alongside it.\n onReasoningDelta: events?.onReasoningDelta,\n // R3-333: STOP composed with the steer INTERRUPT, so either verb ends the turn.\n signal: turnAbort.signal,\n });\n let res: ModelResponse;\n try {\n try {\n res = await sendTurn();\n } catch (e) {\n // Recover-then-retry on a hard context-overflow (exit-c): compact once and\n // re-send. If there is nothing to compact, or the retry also overflows, the\n // error propagates — a bounded recovery, never a dead loop.\n if (turnAbort.signal.aborted || !isContextOverflow(e)) throw e;\n const { messages: compacted, summarizedCount } = await compactTranscript(messages, client, keepRecentTurns);\n if (summarizedCount === 0) throw e;\n messages = compacted;\n contextTokens = estimateTokens(messages);\n events?.onCompact?.({\n summarizedCount,\n ...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),\n ...(cacheWriteTokens !== undefined ? { cacheWriteTokens } : {}),\n });\n res = await sendTurn();\n }\n } catch (e) {\n // R3-224: a mid-turn abort surfaces as a thrown (Abort/Stream)Error. Treat it\n // as a CLEAN stop — return the transcript so far — not a failure to bubble up.\n if (signal?.aborted) {\n turnAbort.dispose();\n break;\n }\n // R3-333: the SAME thrown abort, but from a steer — the run continues. Record\n // the turn the user cut short (an assistant message, so the transcript keeps\n // strict role alternation and replay shows the interruption where it happened),\n // then loop: the drain at the top of the next iteration injects the correction.\n if (steering?.interrupt.aborted) {\n turnAbort.dispose();\n messages.push({\n role: 'assistant',\n content: [{ type: 'text', text: partialText.trim() || INTERRUPTED_TURN_TEXT }],\n });\n events?.onAssistantText?.(partialText.trim() || INTERRUPTED_TURN_TEXT);\n interruptedLastTurn = true;\n continue;\n }\n turnAbort.dispose();\n throw e;\n }\n turnAbort.dispose();\n\n // Token accounting (R3-220): prefer the provider `usage`, else estimate. `turnCost`\n // is what this turn billed (input + output); `contextTokens` is the current window\n // occupancy (drives compaction); `spentTokens` is cumulative run spend (input is\n // re-billed every turn, so summing turnCost is the true cost signal).\n const turnCost = res.usage\n ? res.usage.inputTokens + res.usage.outputTokens\n : estimateTokens(messages) + Math.ceil(textOf(res.content).length / 4);\n contextTokens = turnCost;\n spentTokens += turnCost;\n if (res.usage?.cacheReadTokens !== undefined) {\n cacheReadTokens = (cacheReadTokens ?? 0) + res.usage.cacheReadTokens;\n }\n if (res.usage?.cacheWriteTokens !== undefined) {\n cacheWriteTokens = (cacheWriteTokens ?? 0) + res.usage.cacheWriteTokens;\n }\n events?.onUsage?.({\n contextTokens,\n window,\n spentTokens,\n ...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),\n ...(cacheWriteTokens !== undefined ? { cacheWriteTokens } : {}),\n });\n\n const assistantText = textOf(res.content);\n if (assistantText) events?.onAssistantText?.(assistantText);\n // R3-335: reasoning stays IN the message sequence — a provider that requires the\n // block echoed back gets it from `messages`, not from a side channel.\n for (const b of res.content) if (b.type === 'reasoning') events?.onReasoning?.(b);\n messages.push({ role: 'assistant', content: res.content });\n\n const toolUses = res.content.filter((b): b is ToolUseBlock => b.type === 'tool_use');\n\n // Truncated-tool-call guard (F3): a `max_tokens` turn that emitted tool calls\n // was cut off mid-call, so its args may be partial. Do NOT execute them — fail\n // each with an error tool_result (keeps the conversation well-formed) and\n // re-prompt for a smaller step, bounded by maxTruncationRetries.\n if (res.stopReason === 'max_tokens' && toolUses.length > 0) {\n events?.onTruncatedToolCall?.();\n const failed: ContentBlock[] = toolUses.map((c) => ({\n type: 'tool_result',\n tool_use_id: c.id,\n content: 'tool call truncated by the token limit — not executed',\n is_error: true,\n }));\n failed.push({ type: 'text', text: TRUNCATED_RETRY_TEXT });\n messages.push({ role: 'user', content: failed });\n if (++truncationRetries > maxTruncationRetries) break;\n continue;\n }\n truncationRetries = 0;\n\n if (toolUses.length === 0) {\n // No tool calls. Usually the model is genuinely done — but GLM/OpenRouter\n // intermittently ends with \"I'll read the files…\" or an empty turn after a\n // tool error and no call (findings §2). Nudge such a STALL back into action\n // once (per episode), respecting terminal stops and a real wrap-up.\n const stall = TERMINAL_STOPS.has(res.stopReason) ? null : detectStall(assistantText);\n if (stall && nudges < maxNudges) {\n nudges++;\n events?.onNudge?.(stall);\n messages.push({ role: 'user', content: [{ type: 'text', text: NUDGE_TEXT }] });\n continue;\n }\n // R3-333 follow-up: the model is done, but the user queued something while it\n // was working. Continue rather than end — the drain at the top of the next\n // iteration turns the queued message into the next turn's prompt. This is the\n // difference between a follow-up and a restart.\n if (steering?.hasPending()) continue;\n break;\n }\n\n nudges = 0; // a productive turn clears the stall budget\n\n const results: ToolResultBlock[] = [];\n // R3-339 — image parts produced by tools this turn. They ride in the SAME user\n // message as the results (after them), because a `tool_result`'s content is a string\n // on the wire; this is the shape both host adapters map to their provider.\n const images: ImageBlock[] = [];\n for (const call of toolUses) {\n events?.onToolUse?.(call.name, call.input);\n let outcome: ToolOutcome;\n try {\n outcome = await execute(call.name, call.input);\n } catch (e) {\n // A thrown executor error (e.g. host `forbidden`) becomes an error\n // tool_result so the model sees the gate's verdict and can adapt.\n const code = (e as { code?: string })?.code;\n const msg = (e as Error)?.message ?? String(e);\n outcome = { content: code ? `${code}: ${msg}` : msg, isError: true };\n }\n events?.onToolResult?.(call.name, outcome);\n results.push({\n type: 'tool_result',\n tool_use_id: call.id,\n content: outcome.content,\n is_error: outcome.isError,\n });\n if (outcome.images?.length) images.push(...outcome.images);\n }\n messages.push({ role: 'user', content: [...results, ...images] });\n\n // Runaway-cost guard: stop once cumulative spend passes the budget (the token/\n // spend bound that replaces the old raw turn cap). Compaction keeps a single\n // request small; this bounds the whole run.\n if (opts.tokenBudget && spentTokens >= opts.tokenBudget) {\n events?.onBudgetStop?.({ spentTokens, tokenBudget: opts.tokenBudget });\n break;\n }\n }\n\n return messages;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var agentSteering_exports = {};
|
|
20
|
+
__export(agentSteering_exports, {
|
|
21
|
+
INTERRUPTED_TURN_TEXT: () => INTERRUPTED_TURN_TEXT,
|
|
22
|
+
STEER_INTERRUPT_MARKER: () => STEER_INTERRUPT_MARKER,
|
|
23
|
+
STEER_MARKER: () => STEER_MARKER,
|
|
24
|
+
SteerController: () => SteerController,
|
|
25
|
+
anySignal: () => anySignal,
|
|
26
|
+
parseSteer: () => parseSteer,
|
|
27
|
+
steerWireText: () => steerWireText
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(agentSteering_exports);
|
|
30
|
+
const STEER_MARKER = "\u241F[steer]\n";
|
|
31
|
+
const STEER_INTERRUPT_MARKER = "\u241F[steer:interrupt]\n";
|
|
32
|
+
const INTERRUPTED_TURN_TEXT = "(turn interrupted by the user)";
|
|
33
|
+
const steerWireText = (m) => (m.mode === "interrupt" ? STEER_INTERRUPT_MARKER : STEER_MARKER) + m.text;
|
|
34
|
+
function parseSteer(text) {
|
|
35
|
+
if (text.startsWith(STEER_INTERRUPT_MARKER)) {
|
|
36
|
+
return { mode: "interrupt", text: text.slice(STEER_INTERRUPT_MARKER.length) };
|
|
37
|
+
}
|
|
38
|
+
if (text.startsWith(STEER_MARKER)) return { mode: "queue", text: text.slice(STEER_MARKER.length) };
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
let seq = 0;
|
|
42
|
+
const nextId = () => {
|
|
43
|
+
seq += 1;
|
|
44
|
+
return `steer-${Date.now().toString(36)}-${seq}`;
|
|
45
|
+
};
|
|
46
|
+
class SteerController {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.queue = [];
|
|
49
|
+
this.controller = new AbortController();
|
|
50
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
51
|
+
}
|
|
52
|
+
/** Queue a correction. Returns its id so the UI can cancel it while it waits. */
|
|
53
|
+
enqueue(text, mode = "queue") {
|
|
54
|
+
const trimmed = text.trim();
|
|
55
|
+
if (!trimmed) return null;
|
|
56
|
+
const msg = { id: nextId(), text: trimmed, mode };
|
|
57
|
+
this.queue.push(msg);
|
|
58
|
+
if (mode === "interrupt") this.controller.abort();
|
|
59
|
+
this.emit();
|
|
60
|
+
return msg;
|
|
61
|
+
}
|
|
62
|
+
/** Drop a queued steer that has not been applied yet. */
|
|
63
|
+
cancel(id) {
|
|
64
|
+
const before = this.queue.length;
|
|
65
|
+
this.queue = this.queue.filter((m) => m.id !== id);
|
|
66
|
+
if (this.queue.length === before) return false;
|
|
67
|
+
if (!this.queue.some((m) => m.mode === "interrupt")) this.rearm();
|
|
68
|
+
this.emit();
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/** Everything still waiting, for the UI's "queued" affordance. */
|
|
72
|
+
pending() {
|
|
73
|
+
return this.queue;
|
|
74
|
+
}
|
|
75
|
+
hasPending() {
|
|
76
|
+
return this.queue.length > 0;
|
|
77
|
+
}
|
|
78
|
+
drain() {
|
|
79
|
+
const out = this.queue;
|
|
80
|
+
this.queue = [];
|
|
81
|
+
if (out.length) this.emit();
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
get interrupt() {
|
|
85
|
+
return this.controller.signal;
|
|
86
|
+
}
|
|
87
|
+
rearm() {
|
|
88
|
+
if (this.controller.signal.aborted) this.controller = new AbortController();
|
|
89
|
+
}
|
|
90
|
+
/** Subscribe to queue changes (the UI re-renders its queued chips). */
|
|
91
|
+
onChange(listener) {
|
|
92
|
+
this.listeners.add(listener);
|
|
93
|
+
return () => this.listeners.delete(listener);
|
|
94
|
+
}
|
|
95
|
+
emit() {
|
|
96
|
+
for (const l of this.listeners) l(this.queue);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function anySignal(signals) {
|
|
100
|
+
const live = signals.filter((s) => !!s);
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const already = live.find((s) => s.aborted);
|
|
103
|
+
if (already) {
|
|
104
|
+
controller.abort(already.reason);
|
|
105
|
+
return { signal: controller.signal, dispose: () => {
|
|
106
|
+
} };
|
|
107
|
+
}
|
|
108
|
+
const onAbort = (e) => controller.abort(e.target.reason);
|
|
109
|
+
for (const s of live) s.addEventListener("abort", onAbort, { once: true });
|
|
110
|
+
return {
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
dispose: () => {
|
|
113
|
+
for (const s of live) s.removeEventListener("abort", onAbort);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
118
|
+
0 && (module.exports = {
|
|
119
|
+
INTERRUPTED_TURN_TEXT,
|
|
120
|
+
STEER_INTERRUPT_MARKER,
|
|
121
|
+
STEER_MARKER,
|
|
122
|
+
SteerController,
|
|
123
|
+
anySignal,
|
|
124
|
+
parseSteer,
|
|
125
|
+
steerWireText
|
|
126
|
+
});
|
|
127
|
+
//# sourceMappingURL=agentSteering.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/agentSteering.ts"],"sourcesContent":["// Steering + follow-up queue (R3-333 / AHG-T2-3).\n//\n// WHY. R3-224 resolved the hard half — `ChatRequest.signal`, per-message abort in\n// the host that stops the upstream generator and the billing with it, and\n// `runAgent(signal)` behind the stop button. What sits on top of it was never\n// built: the human's vocabulary was still START and KILL. There was no way to say\n// \"yes, but not that file\" or \"stop and do this instead\", or to queue a follow-up\n// while a turn is in flight. Correction meant restart — which discards the\n// transcript's accumulated understanding and pays for it again.\n//\n// This is the app-layer half: a queue the loop drains at a turn boundary, plus an\n// INTERRUPT signal that aborts the in-flight model turn without ending the run.\n//\n// THE TWO VERBS ARE NOT THE SAME.\n// * STOP — `RunAgentOptions.signal`. Ends the run. The transcript so far is kept.\n// * STEER — this controller. Ends the current TURN (or waits for the next\n// boundary), injects the user's correction, and CONTINUES.\n// They are distinguishable in the transcript (`STEER_MARKER`) and in the UI, which\n// exit criterion 3 requires.\n//\n// WHY A STEER NEVER INTERRUPTS TOOL EXECUTION. A turn's `tool_use` blocks must each\n// get a matching `tool_result`, or the next request is malformed and the provider\n// rejects the whole conversation. So the interrupt signal only ever aborts the\n// in-flight MODEL call; a steer that arrives while tools are running is applied at\n// the boundary after the batch completes. That is exit criterion 5, and it is a\n// property of the design rather than a race the loop has to win.\n\n/** How a steer applies. */\nexport type SteerMode =\n /** Apply at the next turn boundary — the in-flight turn finishes first. */\n | 'queue'\n /** Abort the in-flight model turn now, then apply. Never interrupts a tool call. */\n | 'interrupt';\n\nexport interface SteerMessage {\n id: string;\n text: string;\n mode: SteerMode;\n}\n\n/** Prefix marking a `user` message as a STEER rather than something typed at the\n * start of a run — so a replayed conversation shows the interruption where it\n * happened, instead of reconstructing a history that never ran (scope 5). Mirrors\n * `COMPACTION_MARKER`/`NUDGE_TEXT`, which the transcript renderer already keys on. */\nexport const STEER_MARKER = '␟[steer]\\n';\n/** As above, for a steer that ABORTED an in-flight turn. */\nexport const STEER_INTERRUPT_MARKER = '␟[steer:interrupt]\\n';\n\n/** Text recorded in place of the assistant turn a steer cut short. The turn has to\n * appear in the transcript — dropping it would put two `user` messages back to back\n * and lose the fact that the model was mid-sentence when the user cut in. */\nexport const INTERRUPTED_TURN_TEXT = '(turn interrupted by the user)';\n\n/** Wrap a steer's text for the wire. */\nexport const steerWireText = (m: SteerMessage): string =>\n (m.mode === 'interrupt' ? STEER_INTERRUPT_MARKER : STEER_MARKER) + m.text;\n\n/** Recognise a steer on replay; returns the mode + text, or `null`. */\nexport function parseSteer(text: string): { mode: SteerMode; text: string } | null {\n if (text.startsWith(STEER_INTERRUPT_MARKER)) {\n return { mode: 'interrupt', text: text.slice(STEER_INTERRUPT_MARKER.length) };\n }\n if (text.startsWith(STEER_MARKER)) return { mode: 'queue', text: text.slice(STEER_MARKER.length) };\n return null;\n}\n\n/** What the loop needs from a steering source (so it can be faked in tests). */\nexport interface SteerSource {\n /** Remove and return everything pending, in arrival order. */\n drain(): SteerMessage[];\n /** Is anything waiting? Checked before the loop would otherwise END, so a\n * follow-up queued against a finishing run continues it instead. */\n hasPending(): boolean;\n /** Fires when an `interrupt`-mode steer arrives. The loop composes it with the\n * stop signal for the in-flight model call. */\n readonly interrupt: AbortSignal;\n /** Called by the loop once an interrupt has been consumed, so the next turn is\n * not aborted by a stale signal. */\n rearm(): void;\n}\n\nlet seq = 0;\nconst nextId = (): string => {\n // `crypto.randomUUID` is not available in every host the app runs in; the id only\n // has to be unique within one run, so a counter + timestamp is enough.\n seq += 1;\n return `steer-${Date.now().toString(36)}-${seq}`;\n};\n\n/**\n * The live steering queue. Owned by the UI (which enqueues and cancels) and read\n * by the loop (which drains at a turn boundary).\n */\nexport class SteerController implements SteerSource {\n private queue: SteerMessage[] = [];\n private controller = new AbortController();\n private listeners = new Set<(pending: readonly SteerMessage[]) => void>();\n\n /** Queue a correction. Returns its id so the UI can cancel it while it waits. */\n enqueue(text: string, mode: SteerMode = 'queue'): SteerMessage | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n const msg: SteerMessage = { id: nextId(), text: trimmed, mode };\n this.queue.push(msg);\n if (mode === 'interrupt') this.controller.abort();\n this.emit();\n return msg;\n }\n\n /** Drop a queued steer that has not been applied yet. */\n cancel(id: string): boolean {\n const before = this.queue.length;\n this.queue = this.queue.filter((m) => m.id !== id);\n if (this.queue.length === before) return false;\n // Cancelling the only interrupt leaves a fired signal behind; re-arm so the\n // next turn is not aborted by a correction the user took back.\n if (!this.queue.some((m) => m.mode === 'interrupt')) this.rearm();\n this.emit();\n return true;\n }\n\n /** Everything still waiting, for the UI's \"queued\" affordance. */\n pending(): readonly SteerMessage[] {\n return this.queue;\n }\n\n hasPending(): boolean {\n return this.queue.length > 0;\n }\n\n drain(): SteerMessage[] {\n const out = this.queue;\n this.queue = [];\n if (out.length) this.emit();\n return out;\n }\n\n get interrupt(): AbortSignal {\n return this.controller.signal;\n }\n\n rearm(): void {\n if (this.controller.signal.aborted) this.controller = new AbortController();\n }\n\n /** Subscribe to queue changes (the UI re-renders its queued chips). */\n onChange(listener: (pending: readonly SteerMessage[]) => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n private emit(): void {\n for (const l of this.listeners) l(this.queue);\n }\n}\n\n/**\n * Combine abort signals into one that fires when any of them does.\n *\n * `AbortSignal.any` exists in modern engines but not everywhere the sandbox runs,\n * and a silent `undefined` here would mean the stop button quietly stops working —\n * so this is explicit, and returns a disposer the caller uses to drop its listeners\n * (a per-turn signal that stayed subscribed to a run-long controller would leak one\n * listener per turn on a long run).\n */\nexport function anySignal(signals: Array<AbortSignal | undefined>): {\n signal: AbortSignal;\n dispose: () => void;\n} {\n const live = signals.filter((s): s is AbortSignal => !!s);\n const controller = new AbortController();\n const already = live.find((s) => s.aborted);\n if (already) {\n controller.abort(already.reason);\n return { signal: controller.signal, dispose: () => {} };\n }\n const onAbort = (e: Event): void => controller.abort((e.target as AbortSignal).reason);\n for (const s of live) s.addEventListener('abort', onAbort, { once: true });\n return {\n signal: controller.signal,\n dispose: () => {\n for (const s of live) s.removeEventListener('abort', onAbort);\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** How a steer applies. */
|
|
2
|
+
type SteerMode =
|
|
3
|
+
/** Apply at the next turn boundary — the in-flight turn finishes first. */
|
|
4
|
+
'queue'
|
|
5
|
+
/** Abort the in-flight model turn now, then apply. Never interrupts a tool call. */
|
|
6
|
+
| 'interrupt';
|
|
7
|
+
interface SteerMessage {
|
|
8
|
+
id: string;
|
|
9
|
+
text: string;
|
|
10
|
+
mode: SteerMode;
|
|
11
|
+
}
|
|
12
|
+
/** Prefix marking a `user` message as a STEER rather than something typed at the
|
|
13
|
+
* start of a run — so a replayed conversation shows the interruption where it
|
|
14
|
+
* happened, instead of reconstructing a history that never ran (scope 5). Mirrors
|
|
15
|
+
* `COMPACTION_MARKER`/`NUDGE_TEXT`, which the transcript renderer already keys on. */
|
|
16
|
+
declare const STEER_MARKER = "\u241F[steer]\n";
|
|
17
|
+
/** As above, for a steer that ABORTED an in-flight turn. */
|
|
18
|
+
declare const STEER_INTERRUPT_MARKER = "\u241F[steer:interrupt]\n";
|
|
19
|
+
/** Text recorded in place of the assistant turn a steer cut short. The turn has to
|
|
20
|
+
* appear in the transcript — dropping it would put two `user` messages back to back
|
|
21
|
+
* and lose the fact that the model was mid-sentence when the user cut in. */
|
|
22
|
+
declare const INTERRUPTED_TURN_TEXT = "(turn interrupted by the user)";
|
|
23
|
+
/** Wrap a steer's text for the wire. */
|
|
24
|
+
declare const steerWireText: (m: SteerMessage) => string;
|
|
25
|
+
/** Recognise a steer on replay; returns the mode + text, or `null`. */
|
|
26
|
+
declare function parseSteer(text: string): {
|
|
27
|
+
mode: SteerMode;
|
|
28
|
+
text: string;
|
|
29
|
+
} | null;
|
|
30
|
+
/** What the loop needs from a steering source (so it can be faked in tests). */
|
|
31
|
+
interface SteerSource {
|
|
32
|
+
/** Remove and return everything pending, in arrival order. */
|
|
33
|
+
drain(): SteerMessage[];
|
|
34
|
+
/** Is anything waiting? Checked before the loop would otherwise END, so a
|
|
35
|
+
* follow-up queued against a finishing run continues it instead. */
|
|
36
|
+
hasPending(): boolean;
|
|
37
|
+
/** Fires when an `interrupt`-mode steer arrives. The loop composes it with the
|
|
38
|
+
* stop signal for the in-flight model call. */
|
|
39
|
+
readonly interrupt: AbortSignal;
|
|
40
|
+
/** Called by the loop once an interrupt has been consumed, so the next turn is
|
|
41
|
+
* not aborted by a stale signal. */
|
|
42
|
+
rearm(): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The live steering queue. Owned by the UI (which enqueues and cancels) and read
|
|
46
|
+
* by the loop (which drains at a turn boundary).
|
|
47
|
+
*/
|
|
48
|
+
declare class SteerController implements SteerSource {
|
|
49
|
+
private queue;
|
|
50
|
+
private controller;
|
|
51
|
+
private listeners;
|
|
52
|
+
/** Queue a correction. Returns its id so the UI can cancel it while it waits. */
|
|
53
|
+
enqueue(text: string, mode?: SteerMode): SteerMessage | null;
|
|
54
|
+
/** Drop a queued steer that has not been applied yet. */
|
|
55
|
+
cancel(id: string): boolean;
|
|
56
|
+
/** Everything still waiting, for the UI's "queued" affordance. */
|
|
57
|
+
pending(): readonly SteerMessage[];
|
|
58
|
+
hasPending(): boolean;
|
|
59
|
+
drain(): SteerMessage[];
|
|
60
|
+
get interrupt(): AbortSignal;
|
|
61
|
+
rearm(): void;
|
|
62
|
+
/** Subscribe to queue changes (the UI re-renders its queued chips). */
|
|
63
|
+
onChange(listener: (pending: readonly SteerMessage[]) => void): () => void;
|
|
64
|
+
private emit;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Combine abort signals into one that fires when any of them does.
|
|
68
|
+
*
|
|
69
|
+
* `AbortSignal.any` exists in modern engines but not everywhere the sandbox runs,
|
|
70
|
+
* and a silent `undefined` here would mean the stop button quietly stops working —
|
|
71
|
+
* so this is explicit, and returns a disposer the caller uses to drop its listeners
|
|
72
|
+
* (a per-turn signal that stayed subscribed to a run-long controller would leak one
|
|
73
|
+
* listener per turn on a long run).
|
|
74
|
+
*/
|
|
75
|
+
declare function anySignal(signals: Array<AbortSignal | undefined>): {
|
|
76
|
+
signal: AbortSignal;
|
|
77
|
+
dispose: () => void;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export { INTERRUPTED_TURN_TEXT, STEER_INTERRUPT_MARKER, STEER_MARKER, SteerController, type SteerMessage, type SteerMode, type SteerSource, anySignal, parseSteer, steerWireText };
|