@glbgpt/cli 0.1.4 → 0.2.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/README.md +74 -39
- package/dist/src/coding/backgroundTasks.d.ts +34 -0
- package/dist/src/coding/backgroundTasks.js +154 -0
- package/dist/src/coding/backgroundTasks.js.map +1 -0
- package/dist/src/coding/backgroundTools.d.ts +9 -0
- package/dist/src/coding/backgroundTools.js +63 -0
- package/dist/src/coding/backgroundTools.js.map +1 -0
- package/dist/src/coding/bashTool.d.ts +27 -0
- package/dist/src/coding/bashTool.js +144 -0
- package/dist/src/coding/bashTool.js.map +1 -0
- package/dist/src/coding/dangerousCommands.d.ts +29 -0
- package/dist/src/coding/dangerousCommands.js +156 -0
- package/dist/src/coding/dangerousCommands.js.map +1 -0
- package/dist/src/coding/editDiff.d.ts +45 -0
- package/dist/src/coding/editDiff.js +156 -0
- package/dist/src/coding/editDiff.js.map +1 -0
- package/dist/src/coding/editTool.d.ts +14 -0
- package/dist/src/coding/editTool.js +154 -0
- package/dist/src/coding/editTool.js.map +1 -0
- package/dist/src/coding/index.d.ts +25 -0
- package/dist/src/coding/index.js +30 -0
- package/dist/src/coding/index.js.map +1 -0
- package/dist/src/coding/outputAccumulator.d.ts +34 -0
- package/dist/src/coding/outputAccumulator.js +111 -0
- package/dist/src/coding/outputAccumulator.js.map +1 -0
- package/dist/src/coding/permissionGate.d.ts +65 -0
- package/dist/src/coding/permissionGate.js +138 -0
- package/dist/src/coding/permissionGate.js.map +1 -0
- package/dist/src/coding/permissionStore.d.ts +36 -0
- package/dist/src/coding/permissionStore.js +56 -0
- package/dist/src/coding/permissionStore.js.map +1 -0
- package/dist/src/coding/permissions.d.ts +34 -0
- package/dist/src/coding/permissions.js +18 -0
- package/dist/src/coding/permissions.js.map +1 -0
- package/dist/src/coding/projectContext.d.ts +21 -0
- package/dist/src/coding/projectContext.js +118 -0
- package/dist/src/coding/projectContext.js.map +1 -0
- package/dist/src/coding/readState.d.ts +22 -0
- package/dist/src/coding/readState.js +30 -0
- package/dist/src/coding/readState.js.map +1 -0
- package/dist/src/coding/truncate.d.ts +39 -0
- package/dist/src/coding/truncate.js +83 -0
- package/dist/src/coding/truncate.js.map +1 -0
- package/dist/src/coding/writeTool.d.ts +14 -0
- package/dist/src/coding/writeTool.js +82 -0
- package/dist/src/coding/writeTool.js.map +1 -0
- package/dist/src/commands/execCommand.d.ts +8 -0
- package/dist/src/commands/execCommand.js +45 -5
- package/dist/src/commands/execCommand.js.map +1 -1
- package/dist/src/core/catalog.js +5 -0
- package/dist/src/core/catalog.js.map +1 -1
- package/dist/src/core/compaction.d.ts +52 -0
- package/dist/src/core/compaction.js +151 -0
- package/dist/src/core/compaction.js.map +1 -0
- package/dist/src/core/contextPrune.js +8 -1
- package/dist/src/core/contextPrune.js.map +1 -1
- package/dist/src/core/runChat.d.ts +1 -1
- package/dist/src/core/runChat.js +1 -1
- package/dist/src/core/runChat.js.map +1 -1
- package/dist/src/core/session.d.ts +10 -1
- package/dist/src/core/session.js +11 -2
- package/dist/src/core/session.js.map +1 -1
- package/dist/src/core/turnLoop.d.ts +18 -2
- package/dist/src/core/turnLoop.js +79 -12
- package/dist/src/core/turnLoop.js.map +1 -1
- package/dist/src/exec/output.d.ts +3 -2
- package/dist/src/exec/output.js +17 -4
- package/dist/src/exec/output.js.map +1 -1
- package/dist/src/fs/index.d.ts +4 -1
- package/dist/src/fs/index.js +7 -2
- package/dist/src/fs/index.js.map +1 -1
- package/dist/src/fs/limits.d.ts +1 -1
- package/dist/src/fs/limits.js +3 -1
- package/dist/src/fs/limits.js.map +1 -1
- package/dist/src/fs/readFileTool.d.ts +2 -1
- package/dist/src/fs/readFileTool.js +4 -1
- package/dist/src/fs/readFileTool.js.map +1 -1
- package/dist/src/fs/workspaceGuard.d.ts +8 -0
- package/dist/src/fs/workspaceGuard.js +48 -0
- package/dist/src/fs/workspaceGuard.js.map +1 -1
- package/dist/src/index.js +37 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/tui/App.d.ts +10 -1
- package/dist/src/tui/App.js +114 -15
- package/dist/src/tui/App.js.map +1 -1
- package/dist/src/tui/approvalBridge.d.ts +21 -0
- package/dist/src/tui/approvalBridge.js +45 -0
- package/dist/src/tui/approvalBridge.js.map +1 -0
- package/dist/src/tui/chatController.d.ts +21 -0
- package/dist/src/tui/chatController.js +155 -16
- package/dist/src/tui/chatController.js.map +1 -1
- package/dist/src/tui/commands.js +16 -2
- package/dist/src/tui/commands.js.map +1 -1
- package/dist/src/tui/models.d.ts +4 -0
- package/dist/src/tui/models.js +8 -1
- package/dist/src/tui/models.js.map +1 -1
- package/dist/src/tui/run.d.ts +7 -0
- package/dist/src/tui/run.js +69 -9
- package/dist/src/tui/run.js.map +1 -1
- package/dist/src/web/webFetchTool.js +41 -5
- package/dist/src/web/webFetchTool.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ChatBackend, ChatMessage } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Context compaction (spec lineage: pi B1 blueprint, 2026-08-08 调研; implemented
|
|
4
|
+
* 2026-08-21). When a conversation approaches the model's context window, the older
|
|
5
|
+
* part is summarized into one structured note and only a recent tail keeps riding
|
|
6
|
+
* along. Same philosophy as microcompact (contextPrune.ts): the DECISION is stored
|
|
7
|
+
* on the session and applied as a request-time PROJECTION — raw messages are never
|
|
8
|
+
* edited, so resume/timeline/recovery keep the full history.
|
|
9
|
+
*
|
|
10
|
+
* Triggers:
|
|
11
|
+
* - proactive: last call's prompt_tokens crossed (context window - reserve);
|
|
12
|
+
* - reactive: a turn failed with a provider context-overflow error (regex set below)
|
|
13
|
+
* → compact and retry once;
|
|
14
|
+
* - manual: /compact.
|
|
15
|
+
*/
|
|
16
|
+
export interface CompactionState {
|
|
17
|
+
/** Structured summary standing in for messages [tailStart..upToIndex) of the raw array. */
|
|
18
|
+
summary: string;
|
|
19
|
+
/** Raw-array index the summary covers up to (exclusive). Tail = messages[upToIndex..]. */
|
|
20
|
+
upToIndex: number;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_CONTEXT_WINDOW = 128000;
|
|
24
|
+
/** Output + safety headroom subtracted from the window to get the trigger. */
|
|
25
|
+
export declare const COMPACT_RESERVE_TOKENS = 24000;
|
|
26
|
+
/** Token budget for the retained tail after a compaction. */
|
|
27
|
+
export declare const RETAIN_TAIL_TOKENS = 20000;
|
|
28
|
+
export declare function contextWindowFor(model: string, catalogOverride?: number): number;
|
|
29
|
+
export declare function compactionTriggerTokens(model: string, catalogOverride?: number): number;
|
|
30
|
+
/** Cheap token estimate (bytes/4) — same heuristic the pruning note uses. */
|
|
31
|
+
export declare function estimateMessageTokens(m: ChatMessage): number;
|
|
32
|
+
/** Count of consecutive system messages at the head (base prompts — always kept verbatim). */
|
|
33
|
+
export declare function leadingSystemCount(messages: ChatMessage[]): number;
|
|
34
|
+
/**
|
|
35
|
+
* Choose the compaction cut. Cut points are USER-turn starts only — that boundary can
|
|
36
|
+
* never split an assistant tool_calls message from its tool replies. Picks the earliest
|
|
37
|
+
* user index that keeps the tail within RETAIN_TAIL_TOKENS; falls back to the LAST user
|
|
38
|
+
* turn when even a single turn overflows the tail budget. Returns null when there is
|
|
39
|
+
* nothing worth compacting (too little history, or no meaningful advance over the
|
|
40
|
+
* existing compaction).
|
|
41
|
+
*/
|
|
42
|
+
export declare function planCompaction(messages: ChatMessage[], existing: CompactionState | null): {
|
|
43
|
+
cutIndex: number;
|
|
44
|
+
} | null;
|
|
45
|
+
/** The projection: leading system prompts + one summary note + the retained tail. */
|
|
46
|
+
export declare function applyCompaction(messages: ChatMessage[], c: CompactionState | null): ChatMessage[];
|
|
47
|
+
/**
|
|
48
|
+
* Summarize `toSummarize` (which already embeds any prior summary note via the
|
|
49
|
+
* projection) with a plain, tool-less model call. Returns the summary text.
|
|
50
|
+
*/
|
|
51
|
+
export declare function summarizeForCompaction(backend: ChatBackend, model: string, toSummarize: ChatMessage[], signal?: AbortSignal): Promise<string>;
|
|
52
|
+
export declare function isContextOverflowError(message: string): boolean;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context windows by model id prefix (longest match wins). Server catalog may later
|
|
3
|
+
* serve a per-model `context_window`; the catalog value takes precedence when present.
|
|
4
|
+
* Being wrong LOW just compacts early (harmless); being wrong HIGH is caught by the
|
|
5
|
+
* reactive overflow path.
|
|
6
|
+
*/
|
|
7
|
+
const CONTEXT_WINDOWS = [
|
|
8
|
+
['kimi-', 1_000_000],
|
|
9
|
+
['gemini-', 1_000_000],
|
|
10
|
+
];
|
|
11
|
+
export const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
12
|
+
/** Output + safety headroom subtracted from the window to get the trigger. */
|
|
13
|
+
export const COMPACT_RESERVE_TOKENS = 24_000;
|
|
14
|
+
/** Token budget for the retained tail after a compaction. */
|
|
15
|
+
export const RETAIN_TAIL_TOKENS = 20_000;
|
|
16
|
+
/** A new compaction must advance the covered range by at least this many messages. */
|
|
17
|
+
const MIN_ADVANCE_MESSAGES = 4;
|
|
18
|
+
export function contextWindowFor(model, catalogOverride) {
|
|
19
|
+
if (typeof catalogOverride === 'number' && catalogOverride > 0)
|
|
20
|
+
return catalogOverride;
|
|
21
|
+
for (const [prefix, tokens] of CONTEXT_WINDOWS) {
|
|
22
|
+
if (model.startsWith(prefix))
|
|
23
|
+
return tokens;
|
|
24
|
+
}
|
|
25
|
+
return DEFAULT_CONTEXT_WINDOW;
|
|
26
|
+
}
|
|
27
|
+
export function compactionTriggerTokens(model, catalogOverride) {
|
|
28
|
+
return contextWindowFor(model, catalogOverride) - COMPACT_RESERVE_TOKENS;
|
|
29
|
+
}
|
|
30
|
+
/** Cheap token estimate (bytes/4) — same heuristic the pruning note uses. */
|
|
31
|
+
export function estimateMessageTokens(m) {
|
|
32
|
+
const content = typeof m.content === 'string' ? m.content : m.content == null ? '' : JSON.stringify(m.content);
|
|
33
|
+
const extra = m.tool_calls ? JSON.stringify(m.tool_calls) : '';
|
|
34
|
+
return Math.ceil((Buffer.byteLength(content, 'utf8') + Buffer.byteLength(extra, 'utf8')) / 4);
|
|
35
|
+
}
|
|
36
|
+
/** Count of consecutive system messages at the head (base prompts — always kept verbatim). */
|
|
37
|
+
export function leadingSystemCount(messages) {
|
|
38
|
+
let n = 0;
|
|
39
|
+
while (n < messages.length && messages[n].role === 'system')
|
|
40
|
+
n += 1;
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Choose the compaction cut. Cut points are USER-turn starts only — that boundary can
|
|
45
|
+
* never split an assistant tool_calls message from its tool replies. Picks the earliest
|
|
46
|
+
* user index that keeps the tail within RETAIN_TAIL_TOKENS; falls back to the LAST user
|
|
47
|
+
* turn when even a single turn overflows the tail budget. Returns null when there is
|
|
48
|
+
* nothing worth compacting (too little history, or no meaningful advance over the
|
|
49
|
+
* existing compaction).
|
|
50
|
+
*/
|
|
51
|
+
export function planCompaction(messages, existing) {
|
|
52
|
+
const head = leadingSystemCount(messages);
|
|
53
|
+
const userIndexes = [];
|
|
54
|
+
for (let i = head; i < messages.length; i++) {
|
|
55
|
+
if (messages[i].role === 'user')
|
|
56
|
+
userIndexes.push(i);
|
|
57
|
+
}
|
|
58
|
+
// Need at least two turns: one to summarize away, one to retain.
|
|
59
|
+
if (userIndexes.length < 2)
|
|
60
|
+
return null;
|
|
61
|
+
// Suffix token sums, walked from the end: the first (earliest) user index whose
|
|
62
|
+
// tail fits the budget.
|
|
63
|
+
let cutIndex = userIndexes[userIndexes.length - 1]; // fallback: retain only the last turn
|
|
64
|
+
for (let k = 0; k < userIndexes.length; k++) {
|
|
65
|
+
const idx = userIndexes[k];
|
|
66
|
+
let tail = 0;
|
|
67
|
+
for (let i = idx; i < messages.length; i++)
|
|
68
|
+
tail += estimateMessageTokens(messages[i]);
|
|
69
|
+
if (tail <= RETAIN_TAIL_TOKENS) {
|
|
70
|
+
cutIndex = idx;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const floor = Math.max(head, existing?.upToIndex ?? 0);
|
|
75
|
+
if (cutIndex <= floor + MIN_ADVANCE_MESSAGES - 1)
|
|
76
|
+
return null; // no meaningful advance
|
|
77
|
+
return { cutIndex };
|
|
78
|
+
}
|
|
79
|
+
/** The projection: leading system prompts + one summary note + the retained tail. */
|
|
80
|
+
export function applyCompaction(messages, c) {
|
|
81
|
+
if (!c)
|
|
82
|
+
return messages;
|
|
83
|
+
const head = leadingSystemCount(messages);
|
|
84
|
+
const upTo = Math.min(c.upToIndex, messages.length);
|
|
85
|
+
return [
|
|
86
|
+
...messages.slice(0, head),
|
|
87
|
+
{
|
|
88
|
+
role: 'system',
|
|
89
|
+
content: 'Earlier conversation was summarized to stay within the context limit ' +
|
|
90
|
+
'(the full history is kept locally):\n\n' +
|
|
91
|
+
c.summary,
|
|
92
|
+
},
|
|
93
|
+
...messages.slice(Math.max(upTo, head)),
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
const SUMMARIZE_SYSTEM = 'You summarize a conversation so it can continue seamlessly with the summary standing in ' +
|
|
97
|
+
'for the older messages. Write a dense, factual summary in the language the user has been ' +
|
|
98
|
+
'writing in. Use exactly these sections (markdown headers), omitting any that are empty:\n' +
|
|
99
|
+
'## Goal\nWhat the user is trying to accomplish overall.\n' +
|
|
100
|
+
'## Constraints & decisions\nRequirements, preferences, and decisions already made.\n' +
|
|
101
|
+
'## Progress\nWhat has been done so far, including tool actions and their outcomes.\n' +
|
|
102
|
+
'## Files\nFiles read or modified, with paths and what matters about each.\n' +
|
|
103
|
+
'## Next steps\nWhat remains or was about to happen.\n' +
|
|
104
|
+
'Never invent details. Keep concrete identifiers verbatim (paths, commands, URLs, ids, ' +
|
|
105
|
+
'numbers, code names). Do not add commentary about the summarization itself.';
|
|
106
|
+
/**
|
|
107
|
+
* Summarize `toSummarize` (which already embeds any prior summary note via the
|
|
108
|
+
* projection) with a plain, tool-less model call. Returns the summary text.
|
|
109
|
+
*/
|
|
110
|
+
export async function summarizeForCompaction(backend, model, toSummarize, signal) {
|
|
111
|
+
const req = {
|
|
112
|
+
model,
|
|
113
|
+
messages: [
|
|
114
|
+
{ role: 'system', content: SUMMARIZE_SYSTEM },
|
|
115
|
+
...toSummarize,
|
|
116
|
+
{
|
|
117
|
+
role: 'user',
|
|
118
|
+
content: '[Summarize the conversation above following your instructions. Output ONLY the summary.]',
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
stream: true,
|
|
122
|
+
};
|
|
123
|
+
let text = '';
|
|
124
|
+
for await (const chunk of backend.stream(req, signal)) {
|
|
125
|
+
const t = chunk.choices?.[0]?.delta?.content;
|
|
126
|
+
if (typeof t === 'string')
|
|
127
|
+
text += t;
|
|
128
|
+
}
|
|
129
|
+
const trimmed = text.trim();
|
|
130
|
+
if (trimmed.length === 0)
|
|
131
|
+
throw new Error('Compaction failed: the model returned an empty summary.');
|
|
132
|
+
return trimmed;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Provider context-overflow classification (subset of pi's overflow regex library,
|
|
136
|
+
* matched against the error text our backend relays from the upstream).
|
|
137
|
+
*/
|
|
138
|
+
const OVERFLOW_PATTERNS = [
|
|
139
|
+
/context.{0,20}(length|window)/i,
|
|
140
|
+
/maximum context/i,
|
|
141
|
+
/context_length_exceeded/i,
|
|
142
|
+
/prompt (is )?too long/i,
|
|
143
|
+
/input (is )?too long/i,
|
|
144
|
+
/too many tokens/i,
|
|
145
|
+
/token (count |limit )?exceed/i,
|
|
146
|
+
/exceeds? .{0,30}token/i,
|
|
147
|
+
];
|
|
148
|
+
export function isContextOverflowError(message) {
|
|
149
|
+
return OVERFLOW_PATTERNS.some((p) => p.test(message));
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=compaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../../src/core/compaction.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FS_TOOL_NAMES } from '../fs/index.js';
|
|
2
|
+
import { CODING_TOOL_NAMES } from '../coding/index.js';
|
|
2
3
|
import { PRUNE_KEEP_RECENT, PRUNE_MIN_CONTENT_BYTES, PRUNE_PLACEHOLDER } from '../fs/limits.js';
|
|
3
4
|
/**
|
|
4
5
|
* Microcompact selection and projection (spec: 2026-07-19-context-pruning-design.md).
|
|
@@ -12,7 +13,13 @@ import { PRUNE_KEEP_RECENT, PRUNE_MIN_CONTENT_BYTES, PRUNE_PLACEHOLDER } from '.
|
|
|
12
13
|
* Prunable = free to re-run. web_fetch joins the fs tools (local, no cost); web_search
|
|
13
14
|
* does NOT — clearing its results would nudge the model into re-searching, which bills.
|
|
14
15
|
*/
|
|
15
|
-
|
|
16
|
+
// Coding tool results are prunable too: bash output goes stale quickly, and edit/write
|
|
17
|
+
// results are one-line confirmations whose loss costs nothing.
|
|
18
|
+
const PRUNABLE_TOOL_NAMES = new Set([
|
|
19
|
+
...FS_TOOL_NAMES,
|
|
20
|
+
...CODING_TOOL_NAMES,
|
|
21
|
+
'web_fetch',
|
|
22
|
+
]);
|
|
16
23
|
/** True for a tool result we may clear: successful re-runnable output big enough to matter. */
|
|
17
24
|
function isPrunable(m) {
|
|
18
25
|
if (m.role !== 'tool' || typeof m.content !== 'string' || !m.tool_call_id)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contextPrune.js","sourceRoot":"","sources":["../../../src/core/contextPrune.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEhG;;;;;;;GAOG;AAEH;;;GAGG;AACH,MAAM,mBAAmB,GAAwB,IAAI,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"contextPrune.js","sourceRoot":"","sources":["../../../src/core/contextPrune.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEhG;;;;;;;GAOG;AAEH;;;GAGG;AACH,uFAAuF;AACvF,+DAA+D;AAC/D,MAAM,mBAAmB,GAAwB,IAAI,GAAG,CAAC;IACvD,GAAG,aAAa;IAChB,GAAG,iBAAiB;IACpB,WAAW;CACZ,CAAC,CAAC;AAEH,+FAA+F;AAC/F,SAAS,UAAU,CAAC,CAAc;IAChC,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,YAAY;QAAE,OAAO,KAAK,CAAC;IACxF,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,sCAAsC;IACrG,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,uBAAuB;QAAE,OAAO,KAAK,CAAC;IAClF,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QAC9C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAChE,IAAI,OAAO,IAAI,MAAM;YAAE,OAAO,KAAK,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC,CAAC,sCAAsC;IACtD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,QAAuB,EAAE,aAAkC;IACxF,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,0EAA0E;QAC1E,6EAA6E;QAC7E,6EAA6E;QAC7E,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,YAAY,IAAI,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC;YAAE,SAAS;QACxF,IAAI,UAAU,CAAC,CAAC,CAAC;YAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,MAAM,GAAG,iBAAiB,CAAC,CAAC,CAAC;AACjF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,QAAuB,EAAE,SAA8B;IAClF,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC1C,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACxB,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,YAAY,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC;QAClE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE;QACtC,CAAC,CAAC,CAAC,CACN,CAAC;AACJ,CAAC"}
|
|
@@ -39,4 +39,4 @@ export interface ChatTurnParams {
|
|
|
39
39
|
* tool events live (exec's --jsonl uses it); callers that only want the final result omit it.
|
|
40
40
|
* Throws on a backend/turn error or when structured output does not parse.
|
|
41
41
|
*/
|
|
42
|
-
export declare function runChatTurn(params: ChatTurnParams, onEvent?: (event: EngineEvent) => void): Promise<ChatTurnResult>;
|
|
42
|
+
export declare function runChatTurn(params: ChatTurnParams, onEvent?: (event: EngineEvent) => void | Promise<void>): Promise<ChatTurnResult>;
|
package/dist/src/core/runChat.js
CHANGED
|
@@ -67,7 +67,7 @@ async function runOnce(params, plan, onEvent) {
|
|
|
67
67
|
// the switch above forwards every event via onEvent below, so callers see them too.
|
|
68
68
|
// turn_complete is implicit in runTurn resolving.
|
|
69
69
|
}
|
|
70
|
-
onEvent?.(event);
|
|
70
|
+
return onEvent?.(event);
|
|
71
71
|
}, { responseFormat });
|
|
72
72
|
}
|
|
73
73
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runChat.js","sourceRoot":"","sources":["../../../src/core/runChat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,GAG1B,MAAM,uBAAuB,CAAC;AA+B/B;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAsB,EACtB,
|
|
1
|
+
{"version":3,"file":"runChat.js","sourceRoot":"","sources":["../../../src/core/runChat.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatMessage, ToolCall, ToolResult } from './types.js';
|
|
2
|
+
import { type CompactionState } from './compaction.js';
|
|
2
3
|
/** Serializable snapshot of a conversation, used for local persistence and resume. */
|
|
3
4
|
export interface SessionSnapshot {
|
|
4
5
|
id: string;
|
|
@@ -8,6 +9,8 @@ export interface SessionSnapshot {
|
|
|
8
9
|
messages: ChatMessage[];
|
|
9
10
|
/** Context-pruning decision set (absent in pre-pruning snapshots → empty). */
|
|
10
11
|
prunedToolCallIds?: string[];
|
|
12
|
+
/** Context-compaction decision (absent when never compacted). */
|
|
13
|
+
compaction?: CompactionState;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
16
|
* A single conversation the CLI owns end to end.
|
|
@@ -27,14 +30,20 @@ export declare class Session {
|
|
|
27
30
|
* so policy changes and bugs stay recoverable. Persisted in the snapshot.
|
|
28
31
|
*/
|
|
29
32
|
prunedToolCallIds: Set<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Context-compaction decision (compaction.ts): a summary standing in for the
|
|
35
|
+
* covered prefix in the REQUEST view. Raw messages stay intact, like pruning.
|
|
36
|
+
*/
|
|
37
|
+
compaction: CompactionState | null;
|
|
30
38
|
constructor(params: {
|
|
31
39
|
id: string;
|
|
32
40
|
model: string;
|
|
33
41
|
createdAt?: number;
|
|
34
42
|
messages?: ChatMessage[];
|
|
35
43
|
prunedToolCallIds?: string[];
|
|
44
|
+
compaction?: CompactionState | null;
|
|
36
45
|
});
|
|
37
|
-
/** The messages to SEND:
|
|
46
|
+
/** The messages to SEND: compaction, then pruning, projected over the intact raw history. */
|
|
38
47
|
requestMessages(): ChatMessage[];
|
|
39
48
|
/** Prepend a system message (must precede the first user message to steer the model). */
|
|
40
49
|
addSystemMessage(text: string): void;
|
package/dist/src/core/session.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { applyPruning } from './contextPrune.js';
|
|
2
|
+
import { applyCompaction } from './compaction.js';
|
|
2
3
|
/**
|
|
3
4
|
* A single conversation the CLI owns end to end.
|
|
4
5
|
*
|
|
@@ -17,16 +18,22 @@ export class Session {
|
|
|
17
18
|
* so policy changes and bugs stay recoverable. Persisted in the snapshot.
|
|
18
19
|
*/
|
|
19
20
|
prunedToolCallIds;
|
|
21
|
+
/**
|
|
22
|
+
* Context-compaction decision (compaction.ts): a summary standing in for the
|
|
23
|
+
* covered prefix in the REQUEST view. Raw messages stay intact, like pruning.
|
|
24
|
+
*/
|
|
25
|
+
compaction;
|
|
20
26
|
constructor(params) {
|
|
21
27
|
this.id = params.id;
|
|
22
28
|
this.model = params.model;
|
|
23
29
|
this.createdAt = params.createdAt ?? nowMs();
|
|
24
30
|
this.messages = params.messages ?? [];
|
|
25
31
|
this.prunedToolCallIds = new Set(params.prunedToolCallIds ?? []);
|
|
32
|
+
this.compaction = params.compaction ?? null;
|
|
26
33
|
}
|
|
27
|
-
/** The messages to SEND:
|
|
34
|
+
/** The messages to SEND: compaction, then pruning, projected over the intact raw history. */
|
|
28
35
|
requestMessages() {
|
|
29
|
-
return applyPruning(this.messages, this.prunedToolCallIds);
|
|
36
|
+
return applyPruning(applyCompaction(this.messages, this.compaction), this.prunedToolCallIds);
|
|
30
37
|
}
|
|
31
38
|
/** Prepend a system message (must precede the first user message to steer the model). */
|
|
32
39
|
addSystemMessage(text) {
|
|
@@ -80,6 +87,7 @@ export class Session {
|
|
|
80
87
|
updatedAt: nowMs(),
|
|
81
88
|
messages: this.messages,
|
|
82
89
|
...(this.prunedToolCallIds.size > 0 ? { prunedToolCallIds: [...this.prunedToolCallIds] } : {}),
|
|
90
|
+
...(this.compaction ? { compaction: this.compaction } : {}),
|
|
83
91
|
};
|
|
84
92
|
}
|
|
85
93
|
static fromSnapshot(s) {
|
|
@@ -89,6 +97,7 @@ export class Session {
|
|
|
89
97
|
createdAt: s.createdAt,
|
|
90
98
|
messages: s.messages,
|
|
91
99
|
prunedToolCallIds: s.prunedToolCallIds,
|
|
100
|
+
compaction: s.compaction ?? null,
|
|
92
101
|
});
|
|
93
102
|
}
|
|
94
103
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/core/session.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/core/session.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAwB,MAAM,iBAAiB,CAAC;AAexE;;;;;;GAMG;AACH,MAAM,OAAO,OAAO;IACT,EAAE,CAAS;IACX,SAAS,CAAS;IAC3B,KAAK,CAAS;IACd,QAAQ,CAAgB;IACxB;;;;OAIG;IACH,iBAAiB,CAAc;IAC/B;;;OAGG;IACH,UAAU,CAAyB;IAEnC,YAAY,MAOX;QACC,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;QACpB,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAC1B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,KAAK,EAAE,CAAC;QAC7C,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC;QACtC,IAAI,CAAC,iBAAiB,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC;QACjE,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC;IAC9C,CAAC;IAED,6FAA6F;IAC7F,eAAe;QACb,OAAO,YAAY,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAC/F,CAAC;IAED,yFAAyF;IACzF,gBAAgB,CAAC,IAAY;QAC3B,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED;;;OAGG;IACH,aAAa,CAAC,IAAY;QACxB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,cAAc,CAAC,OAAoD;QACjE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,0EAA0E;IAC1E,YAAY;QACV,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,+FAA+F;IAC/F,UAAU,CAAC,KAAa;QACtB,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM;YAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC;IAC/E,CAAC;IAED,wFAAwF;IACxF,mBAAmB,CAAC,OAAsB,EAAE,SAAqB;QAC/D,MAAM,GAAG,GAAgB,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,CAAC;QACzE,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzB,GAAG,CAAC,UAAU,GAAG,SAAS,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,mEAAmE;IACnE,cAAc,CAAC,UAAkB,EAAE,IAAY,EAAE,MAAkB;QACjE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YACjB,IAAI,EAAE,MAAM;YACZ,YAAY,EAAE,UAAU;YACxB,IAAI;YACJ,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;SAChC,CAAC,CAAC;IACL,CAAC;IAED,sEAAsE;IACtE,WAAW,CAAC,KAAa;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,UAAU;QACR,OAAO;YACL,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,SAAS,EAAE,KAAK,EAAE;YAClB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,GAAG,IAAI,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9F,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5D,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,YAAY,CAAC,CAAkB;QACpC,OAAO,IAAI,OAAO,CAAC;YACjB,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;YACtC,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,IAAI;SACjC,CAAC,CAAC;IACL,CAAC;CACF;AAED,SAAS,KAAK;IACZ,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC;AACpB,CAAC"}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { ChatBackend, EngineEvent } from './types.js';
|
|
2
2
|
import type { Session } from './session.js';
|
|
3
3
|
import type { ToolRegistry } from './toolRegistry.js';
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Event sink. A returned promise applies BACKPRESSURE on the streaming pump (pi A3):
|
|
6
|
+
* consumeStream awaits it before pulling the next chunk, so a slow consumer (exec piping
|
|
7
|
+
* into a slow reader) bounds memory instead of buffering the whole model stream.
|
|
8
|
+
*/
|
|
9
|
+
export type Emit = (event: EngineEvent) => void | Promise<void>;
|
|
5
10
|
/** Per-turn options. `exec --output-schema` passes a `response_format` value here. */
|
|
6
11
|
export interface TurnOptions {
|
|
7
12
|
responseFormat?: unknown;
|
|
@@ -11,6 +16,14 @@ export interface TurnOptions {
|
|
|
11
16
|
includeGatedTools?: boolean;
|
|
12
17
|
/** Hard cap on tool calls dispatched in this turn. Default MAX_TOOL_CALLS_PER_TURN. */
|
|
13
18
|
maxToolCalls?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Steering (pi B2): called at each round boundary AFTER tool results are appended and
|
|
21
|
+
* BEFORE the next model call. Returned items are appended as user messages, so text the
|
|
22
|
+
* user typed mid-turn reaches the model without waiting for the whole turn to finish.
|
|
23
|
+
* Only multi-round (tool-using) turns have injection points; anything not injected by
|
|
24
|
+
* turn end stays queued for the caller's normal follow-up flush.
|
|
25
|
+
*/
|
|
26
|
+
takeSteering?: () => Array<string | import('./types.js').ContentPart[]>;
|
|
14
27
|
}
|
|
15
28
|
/**
|
|
16
29
|
* Drives one user turn to completion, following the OpenAI tool-calling protocol:
|
|
@@ -32,6 +45,9 @@ export declare class TurnLoop {
|
|
|
32
45
|
runTurn(session: Session, userContent: string | import('./types.js').ContentPart[], emit: Emit, opts?: TurnOptions): Promise<void>;
|
|
33
46
|
/** Stream a single model call, emitting text deltas and accumulating tool_calls. */
|
|
34
47
|
private streamOneCall;
|
|
35
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Iterate the backend stream, forwarding deltas/usage and accumulating tool-call pieces.
|
|
50
|
+
* Returns the last non-null finish_reason seen (providers send it on the closing chunk).
|
|
51
|
+
*/
|
|
36
52
|
private consumeStream;
|
|
37
53
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { BUDGET_EXHAUSTED_MESSAGE, MAX_TOOL_CALLS_PER_TURN } from '../fs/limits.js';
|
|
2
2
|
import { track } from '../telemetry/telemetry.js';
|
|
3
3
|
/** Pathological backstop only — the tool budget below governs normal turns. */
|
|
4
|
-
const MAX_ROUNDS =
|
|
4
|
+
const MAX_ROUNDS = 32;
|
|
5
|
+
/**
|
|
6
|
+
* Fed back for every tool call on a message the provider cut off with finish_reason=length:
|
|
7
|
+
* streamed tool-call arguments are accumulated incrementally, so a length-truncated message
|
|
8
|
+
* can yield arguments that parse as valid JSON yet are semantically incomplete. With tools
|
|
9
|
+
* that mutate files or spend credits, executing them would corrupt state — discard them all
|
|
10
|
+
* and let the model re-issue (pi agent-loop behavior, 2026-08-08 调研 A1).
|
|
11
|
+
*/
|
|
12
|
+
const LENGTH_TRUNCATED_TOOLS_MESSAGE = 'Response was truncated by the output limit; this tool call was discarded because its ' +
|
|
13
|
+
'arguments may be incomplete. Re-issue the call (smaller, if possible).';
|
|
5
14
|
/**
|
|
6
15
|
* Drives one user turn to completion, following the OpenAI tool-calling protocol:
|
|
7
16
|
*
|
|
@@ -39,8 +48,9 @@ export class TurnLoop {
|
|
|
39
48
|
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
40
49
|
let content;
|
|
41
50
|
let toolCalls;
|
|
51
|
+
let finishReason;
|
|
42
52
|
try {
|
|
43
|
-
({ content, toolCalls } = await this.streamOneCall(session, emit, opts, toolCallsUsed >= budget));
|
|
53
|
+
({ content, toolCalls, finishReason } = await this.streamOneCall(session, emit, opts, toolCallsUsed >= budget));
|
|
44
54
|
}
|
|
45
55
|
catch (err) {
|
|
46
56
|
// User interruption (Esc): keep whatever partial text already streamed so the
|
|
@@ -61,7 +71,10 @@ export class TurnLoop {
|
|
|
61
71
|
if (content.length === 0 && toolCalls.length === 0) {
|
|
62
72
|
// The request itself succeeded (no throw) — the model just returned nothing. Keep the
|
|
63
73
|
// user message (not a poison payload) so a retry has context; only add nothing more.
|
|
64
|
-
emit({
|
|
74
|
+
emit({
|
|
75
|
+
type: 'error',
|
|
76
|
+
message: 'The model returned no output. Please try again.',
|
|
77
|
+
});
|
|
65
78
|
return;
|
|
66
79
|
}
|
|
67
80
|
session.addAssistantMessage(content.length > 0 ? content : null, toolCalls);
|
|
@@ -74,6 +87,28 @@ export class TurnLoop {
|
|
|
74
87
|
emit({ type: 'turn_complete' });
|
|
75
88
|
return;
|
|
76
89
|
}
|
|
90
|
+
// Length-truncated message with tool calls: never execute them (arguments may be
|
|
91
|
+
// silently incomplete). Answer every call with an error result and loop so the
|
|
92
|
+
// model re-issues. Consumes no budget — nothing ran.
|
|
93
|
+
if (finishReason === 'length') {
|
|
94
|
+
for (const call of toolCalls) {
|
|
95
|
+
const result = { error: LENGTH_TRUNCATED_TOOLS_MESSAGE };
|
|
96
|
+
emit({
|
|
97
|
+
type: 'tool_call_begin',
|
|
98
|
+
id: call.id,
|
|
99
|
+
name: call.function.name,
|
|
100
|
+
args: safeParseArgs(call.function.arguments),
|
|
101
|
+
});
|
|
102
|
+
emit({
|
|
103
|
+
type: 'tool_call_end',
|
|
104
|
+
id: call.id,
|
|
105
|
+
name: call.function.name,
|
|
106
|
+
result,
|
|
107
|
+
});
|
|
108
|
+
session.addToolMessage(call.id, call.function.name, result);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
77
112
|
// Execute the requested tools and feed results back into the conversation.
|
|
78
113
|
// Tool execution itself is never aborted mid-flight (a paid submit must not be torn),
|
|
79
114
|
// but an interrupt takes effect before the next model call.
|
|
@@ -116,7 +151,12 @@ export class TurnLoop {
|
|
|
116
151
|
? { error: denied }
|
|
117
152
|
: await this.registry.dispatch(call.function.name, call.function.arguments);
|
|
118
153
|
// Ends may arrive out of completion order; the TUI matches rows by call id.
|
|
119
|
-
emit({
|
|
154
|
+
emit({
|
|
155
|
+
type: 'tool_call_end',
|
|
156
|
+
id: call.id,
|
|
157
|
+
name: call.function.name,
|
|
158
|
+
result,
|
|
159
|
+
});
|
|
120
160
|
return result;
|
|
121
161
|
}));
|
|
122
162
|
// Tool messages must follow the tool_calls order the model emitted.
|
|
@@ -126,14 +166,24 @@ export class TurnLoop {
|
|
|
126
166
|
for (let i = 0; i < toolCalls.length; i++) {
|
|
127
167
|
const call = toolCalls[i];
|
|
128
168
|
const args = safeParseArgs(call.function.arguments);
|
|
129
|
-
emit({
|
|
169
|
+
emit({
|
|
170
|
+
type: 'tool_call_begin',
|
|
171
|
+
id: call.id,
|
|
172
|
+
name: call.function.name,
|
|
173
|
+
args,
|
|
174
|
+
});
|
|
130
175
|
// Past a budget/cap, don't dispatch: feed the model an error instead, so it
|
|
131
176
|
// wraps up with what it has (a normal answer, not a hard stop).
|
|
132
177
|
const denied = denial[i];
|
|
133
178
|
const result = denied
|
|
134
179
|
? { error: denied }
|
|
135
180
|
: await this.registry.dispatch(call.function.name, call.function.arguments);
|
|
136
|
-
emit({
|
|
181
|
+
emit({
|
|
182
|
+
type: 'tool_call_end',
|
|
183
|
+
id: call.id,
|
|
184
|
+
name: call.function.name,
|
|
185
|
+
result,
|
|
186
|
+
});
|
|
137
187
|
session.addToolMessage(call.id, call.function.name, result);
|
|
138
188
|
}
|
|
139
189
|
}
|
|
@@ -141,10 +191,17 @@ export class TurnLoop {
|
|
|
141
191
|
emit({ type: 'turn_interrupted' });
|
|
142
192
|
return;
|
|
143
193
|
}
|
|
144
|
-
//
|
|
194
|
+
// Steering: text typed mid-turn joins the conversation before the next model call.
|
|
195
|
+
for (const content of opts.takeSteering?.() ?? []) {
|
|
196
|
+
session.addUserMessage(content);
|
|
197
|
+
}
|
|
198
|
+
// Loop: stream again now that tool results (and any steering) are in the messages.
|
|
145
199
|
}
|
|
146
200
|
session.rollbackTo(checkpoint);
|
|
147
|
-
emit({
|
|
201
|
+
emit({
|
|
202
|
+
type: 'error',
|
|
203
|
+
message: `Turn exceeded ${MAX_ROUNDS} tool-calling rounds`,
|
|
204
|
+
});
|
|
148
205
|
}
|
|
149
206
|
/** Stream a single model call, emitting text deltas and accumulating tool_calls. */
|
|
150
207
|
async streamOneCall(session, emit, opts, forceNoTools) {
|
|
@@ -159,8 +216,9 @@ export class TurnLoop {
|
|
|
159
216
|
};
|
|
160
217
|
let content = '';
|
|
161
218
|
const toolAcc = new Map();
|
|
219
|
+
let finishReason = null;
|
|
162
220
|
try {
|
|
163
|
-
await this.consumeStream(req, opts, emit, (text) => (content += text), toolAcc);
|
|
221
|
+
finishReason = await this.consumeStream(req, opts, emit, (text) => (content += text), toolAcc);
|
|
164
222
|
}
|
|
165
223
|
catch (err) {
|
|
166
224
|
// Attach the partial text so an interrupt can preserve what already streamed.
|
|
@@ -178,10 +236,14 @@ export class TurnLoop {
|
|
|
178
236
|
...(e.extraContent !== undefined ? { extra_content: e.extraContent } : {}),
|
|
179
237
|
}))
|
|
180
238
|
.filter((c) => c.function.name.length > 0);
|
|
181
|
-
return { content, toolCalls };
|
|
239
|
+
return { content, toolCalls, finishReason };
|
|
182
240
|
}
|
|
183
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* Iterate the backend stream, forwarding deltas/usage and accumulating tool-call pieces.
|
|
243
|
+
* Returns the last non-null finish_reason seen (providers send it on the closing chunk).
|
|
244
|
+
*/
|
|
184
245
|
async consumeStream(req, opts, emit, onText, toolAcc) {
|
|
246
|
+
let finishReason = null;
|
|
185
247
|
for await (const chunk of this.backend.stream(req, opts.signal)) {
|
|
186
248
|
// The final chunk (include_usage) carries token counts with empty/no choices; surface
|
|
187
249
|
// them before the delta guard below skips it.
|
|
@@ -193,12 +255,16 @@ export class TurnLoop {
|
|
|
193
255
|
});
|
|
194
256
|
}
|
|
195
257
|
const choice = chunk.choices?.[0];
|
|
258
|
+
if (typeof choice?.finish_reason === 'string')
|
|
259
|
+
finishReason = choice.finish_reason;
|
|
196
260
|
const delta = choice?.delta;
|
|
197
261
|
if (!delta)
|
|
198
262
|
continue;
|
|
199
263
|
if (typeof delta.content === 'string' && delta.content.length > 0) {
|
|
200
264
|
onText(delta.content);
|
|
201
|
-
emit({ type: 'assistant_delta', text: delta.content });
|
|
265
|
+
const r = emit({ type: 'assistant_delta', text: delta.content });
|
|
266
|
+
if (r)
|
|
267
|
+
await r; // backpressure: wait for the consumer to drain
|
|
202
268
|
}
|
|
203
269
|
if (delta.tool_calls) {
|
|
204
270
|
for (const tc of delta.tool_calls) {
|
|
@@ -215,6 +281,7 @@ export class TurnLoop {
|
|
|
215
281
|
}
|
|
216
282
|
}
|
|
217
283
|
}
|
|
284
|
+
return finishReason;
|
|
218
285
|
}
|
|
219
286
|
}
|
|
220
287
|
/** Thrown when the user aborts mid-stream; carries the text streamed so far. */
|