@corenel/harness 0.7.4 → 0.8.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/conversation.d.ts +15 -0
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +13 -1
- package/dist/conversation.js.map +1 -1
- package/dist/core/compactHistory.d.ts +84 -0
- package/dist/core/compactHistory.d.ts.map +1 -0
- package/dist/core/compactHistory.js +191 -0
- package/dist/core/compactHistory.js.map +1 -0
- package/dist/core/loop.d.ts +25 -0
- package/dist/core/loop.d.ts.map +1 -1
- package/dist/core/loop.js +99 -1
- package/dist/core/loop.js.map +1 -1
- package/dist/core/summarize.d.ts +10 -0
- package/dist/core/summarize.d.ts.map +1 -0
- package/dist/core/summarize.js +66 -0
- package/dist/core/summarize.js.map +1 -0
- package/dist/prompts/library/defaults.generated.d.ts.map +1 -1
- package/dist/prompts/library/defaults.generated.js +35 -6
- package/dist/prompts/library/defaults.generated.js.map +1 -1
- package/dist/sessions/checkpointStore.d.ts +12 -0
- package/dist/sessions/checkpointStore.d.ts.map +1 -0
- package/dist/sessions/checkpointStore.js +48 -0
- package/dist/sessions/checkpointStore.js.map +1 -0
- package/dist/sessions/fileSessionStore.d.ts +4 -0
- package/dist/sessions/fileSessionStore.d.ts.map +1 -1
- package/dist/sessions/fileSessionStore.js +28 -2
- package/dist/sessions/fileSessionStore.js.map +1 -1
- package/dist/tools/builtins.d.ts.map +1 -1
- package/dist/tools/builtins.js +2 -0
- package/dist/tools/builtins.js.map +1 -1
- package/dist/tools/project.d.ts +11 -0
- package/dist/tools/project.d.ts.map +1 -0
- package/dist/tools/project.js +133 -0
- package/dist/tools/project.js.map +1 -0
- package/package.json +1 -1
package/dist/conversation.d.ts
CHANGED
|
@@ -14,5 +14,20 @@ export interface ConversationTurn {
|
|
|
14
14
|
messages: ChatCompletionMessageParam[];
|
|
15
15
|
/** Token/cost/latency for the assistant response, once the turn settles. */
|
|
16
16
|
stats?: TurnStats;
|
|
17
|
+
/** Set on a COMPACTION SEAM: the checkpoint body the summarizer produced for
|
|
18
|
+
* the stretch this seam replaced. Carried on the turn rather than left to a
|
|
19
|
+
* file because the question a seam raises ("what did it keep?") is a question
|
|
20
|
+
* about the conversation in front of you, and because the file only exists
|
|
21
|
+
* for a run that had a session to write beside. Absent on every other turn. */
|
|
22
|
+
checkpoint?: string;
|
|
17
23
|
}
|
|
24
|
+
/** Is this turn a COMPACTION SEAM rather than something said?
|
|
25
|
+
*
|
|
26
|
+
* Recognized by the marker the harness writes into the window, so a surface
|
|
27
|
+
* cannot look for one string while the loop writes another. Lives here, beside
|
|
28
|
+
* the type, because three consumers need it now -- the transcript draws the
|
|
29
|
+
* seam, the context panel counts seams and must keep them OUT of the window it
|
|
30
|
+
* reports (a seam is an unsent note; counting it would inflate the very number
|
|
31
|
+
* the panel exists to give), and the runtime writes them. */
|
|
32
|
+
export declare function isCheckpointTurn(t: ConversationTurn): boolean;
|
|
18
33
|
//# sourceMappingURL=conversation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../conversation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../conversation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAGpF,qFAAqF;AACrF,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;+EAC+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,0BAA0B,EAAE,CAAC;IACvC,4EAA4E;IAC5E,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;mFAI+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;6DAO6D;AAC7D,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAG7D"}
|
package/dist/conversation.js
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { CHECKPOINT_MARKER } from './core/compactHistory.js';
|
|
2
|
+
/** Is this turn a COMPACTION SEAM rather than something said?
|
|
3
|
+
*
|
|
4
|
+
* Recognized by the marker the harness writes into the window, so a surface
|
|
5
|
+
* cannot look for one string while the loop writes another. Lives here, beside
|
|
6
|
+
* the type, because three consumers need it now -- the transcript draws the
|
|
7
|
+
* seam, the context panel counts seams and must keep them OUT of the window it
|
|
8
|
+
* reports (a seam is an unsent note; counting it would inflate the very number
|
|
9
|
+
* the panel exists to give), and the runtime writes them. */
|
|
10
|
+
export function isCheckpointTurn(t) {
|
|
11
|
+
const first = t.messages[0];
|
|
12
|
+
return typeof first?.content === 'string' && first.content.startsWith(CHECKPOINT_MARKER);
|
|
13
|
+
}
|
|
2
14
|
//# sourceMappingURL=conversation.js.map
|
package/dist/conversation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation.js","sourceRoot":"","sources":["../conversation.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"conversation.js","sourceRoot":"","sources":["../conversation.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AA0B1D;;;;;;;6DAO6D;AAC7D,MAAM,UAAU,gBAAgB,CAAC,CAAmB;IAClD,MAAM,KAAK,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5B,OAAO,OAAO,KAAK,EAAE,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;AAC3F,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
|
|
2
|
+
export interface CompactHistoryOptions {
|
|
3
|
+
/** Summarizes the collapsed stretch. Injected so the partitioning is testable
|
|
4
|
+
* without a model call. */
|
|
5
|
+
summarize: (collapsed: ChatCompletionMessageParam[]) => Promise<string>;
|
|
6
|
+
/** Produces the one-line referent for a preserved user message: what it was
|
|
7
|
+
* answering. `answered` is the nearest preceding assistant turn, or null when
|
|
8
|
+
* the user opened the conversation. Omit to preserve user messages bare. */
|
|
9
|
+
anchor?: (user: ChatCompletionMessageParam, answered: ChatCompletionMessageParam | null) => Promise<string>;
|
|
10
|
+
/** Byte size past which a user message's own content is offloaded rather than
|
|
11
|
+
* kept inline. Requires `offload`. */
|
|
12
|
+
offloadOverBytes?: number;
|
|
13
|
+
/** Records oversized user content and returns the pointer to leave in its
|
|
14
|
+
* place — the same treatment rung 1 gives an oversized tool result. */
|
|
15
|
+
offload?: (content: string) => Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
export interface CompactHistoryResult {
|
|
18
|
+
/** The rebuilt message array to send from here on. */
|
|
19
|
+
messages: ChatCompletionMessageParam[];
|
|
20
|
+
/** The durable checkpoint body, destined for a file in the project. */
|
|
21
|
+
checkpoint: string;
|
|
22
|
+
}
|
|
23
|
+
/** The marker every checkpoint message starts with, so a later pass can
|
|
24
|
+
* recognize one it already wrote instead of collapsing it again. */
|
|
25
|
+
export declare const CHECKPOINT_MARKER = "[checkpoint] ";
|
|
26
|
+
/** The marker every anchor message starts with. An anchor is context ABOUT the
|
|
27
|
+
* user message that follows it, not something the assistant said. */
|
|
28
|
+
export declare const ANCHOR_MARKER = "[anchor] ";
|
|
29
|
+
/** Window fill ratio at which history compaction becomes worth its cost.
|
|
30
|
+
* High on purpose: see the one-shot reasoning at the top of this file. */
|
|
31
|
+
export declare const COMPACT_AT_FILL = 0.86;
|
|
32
|
+
/** Whether compacting now is worth rewriting the cached prefix. Two gates: the
|
|
33
|
+
* window is genuinely close to full, AND there is bulk to remove. A compaction
|
|
34
|
+
* that reduces nothing still invalidates the whole prefix, so "nothing to
|
|
35
|
+
* collapse" is a refusal, not a no-op. */
|
|
36
|
+
export declare function shouldCompact(fill: number, messages: ChatCompletionMessageParam[]): boolean;
|
|
37
|
+
/** Fill must fall back below this before compaction re-arms. */
|
|
38
|
+
export declare const REARM_BELOW_FILL = 0.6;
|
|
39
|
+
/** How much the window must refill, ABOVE where the last compaction left it,
|
|
40
|
+
* for another compaction to be worth its cost. */
|
|
41
|
+
export declare const REARM_ON_REGROWTH = 0.1;
|
|
42
|
+
/** The latch's state between turns.
|
|
43
|
+
*
|
|
44
|
+
* `floor` is the fill measured just after the last compaction -- how far down
|
|
45
|
+
* it actually got. Null means either nothing has compacted yet or the next
|
|
46
|
+
* usage reading has not arrived, and the gate falls back to the absolute rule
|
|
47
|
+
* until it has. It is deliberately taken from a REAL reading rather than
|
|
48
|
+
* estimateFill: it is compared against real readings, and mixing an estimate
|
|
49
|
+
* into one side of that comparison would bias every re-arm after it. */
|
|
50
|
+
export interface CompactGate {
|
|
51
|
+
armed: boolean;
|
|
52
|
+
floor: number | null;
|
|
53
|
+
}
|
|
54
|
+
/** The gate's state the moment a compaction has run: shut, and waiting for the
|
|
55
|
+
* next usage reading to tell it how far down that compaction got. */
|
|
56
|
+
export declare function afterCompaction(): CompactGate;
|
|
57
|
+
/** Window fill ratio read from the messages themselves.
|
|
58
|
+
*
|
|
59
|
+
* For the one turn where no usage reading exists: `fill` comes from the
|
|
60
|
+
* PREVIOUS response, so it is 0 on turn 1 and the gate could never fire before
|
|
61
|
+
* the first request. A run resuming an already-oversized conversation
|
|
62
|
+
* therefore put it on the wire uncompacted and took the fatal 400 immediately
|
|
63
|
+
* -- the case that needs rung 2 most was the case it could not run in.
|
|
64
|
+
*
|
|
65
|
+
* Reads low: the request also carries tool schemas, which are not visible
|
|
66
|
+
* here. That is the right direction to be wrong in, since the cost of reading
|
|
67
|
+
* high is a compaction nobody needed. */
|
|
68
|
+
export declare function estimateFill(messages: ChatCompletionMessageParam[], contextWindow: number | undefined): number;
|
|
69
|
+
export declare function compactionDecision(fill: number, messages: ChatCompletionMessageParam[], gate: CompactGate): {
|
|
70
|
+
compact: boolean;
|
|
71
|
+
next: CompactGate;
|
|
72
|
+
};
|
|
73
|
+
export declare function compactHistory(messages: ChatCompletionMessageParam[], opts: CompactHistoryOptions): Promise<CompactHistoryResult>;
|
|
74
|
+
/** An anchor or checkpoint a compaction wrote — bookkeeping ABOUT the
|
|
75
|
+
* conversation rather than part of it. Exported because a caller that has to
|
|
76
|
+
* tell "what this run added" from "what a rewrite inserted" cannot do it by
|
|
77
|
+
* position: a rewrite puts fresh objects into the array at arbitrary places. */
|
|
78
|
+
export declare const isCompactionMarker: typeof isOwnMarker;
|
|
79
|
+
/** An anchor or checkpoint this module wrote on an earlier pass. Dropped and
|
|
80
|
+
* rewritten each time rather than accumulated: the newest checkpoint already
|
|
81
|
+
* subsumes what the older ones recorded. */
|
|
82
|
+
declare function isOwnMarker(m: ChatCompletionMessageParam): boolean;
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=compactHistory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compactHistory.d.ts","sourceRoot":"","sources":["../../core/compactHistory.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAEpF,MAAM,WAAW,qBAAqB;IACpC;+BAC2B;IAC3B,SAAS,EAAE,CAAC,SAAS,EAAE,0BAA0B,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACxE;;gFAE4E;IAC5E,MAAM,CAAC,EAAE,CACP,IAAI,EAAE,0BAA0B,EAChC,QAAQ,EAAE,0BAA0B,GAAG,IAAI,KACxC,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB;0CACsC;IACtC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;2EACuE;IACvE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,oBAAoB;IACnC,sDAAsD;IACtD,QAAQ,EAAE,0BAA0B,EAAE,CAAC;IACvC,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;oEACoE;AACpE,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD;qEACqE;AACrE,eAAO,MAAM,aAAa,cAAc,CAAC;AAEzC;0EAC0E;AAC1E,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC;;;0CAG0C;AAC1C,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,0BAA0B,EAAE,GAAG,OAAO,CAG3F;AAED,gEAAgE;AAChE,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC;kDACkD;AAClD,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC;;;;;;;wEAOwE;AACxE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;qEACqE;AACrE,wBAAgB,eAAe,IAAI,WAAW,CAE7C;AAyBD;;;;;;;;;;yCAUyC;AACzC,wBAAgB,YAAY,CAAC,QAAQ,EAAE,0BAA0B,EAAE,EAAE,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAK9G;AA+BD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,0BAA0B,EAAE,EACtC,IAAI,EAAE,WAAW,GAChB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAQzC;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,0BAA0B,EAAE,EACtC,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,oBAAoB,CAAC,CAyC/B;AAeD;;;gFAGgF;AAChF,eAAO,MAAM,kBAAkB,oBAAc,CAAC;AAE9C;;4CAE4C;AAC5C,iBAAS,WAAW,CAAC,CAAC,EAAE,0BAA0B,GAAG,OAAO,CAG3D"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/* Rung 2 of the context ladder: one-shot history compaction.
|
|
2
|
+
*
|
|
3
|
+
* Rung 1 (compression.ts) offloads ONE oversized tool result at a time and is
|
|
4
|
+
* lossless. This is the other half: when the window is genuinely close to full,
|
|
5
|
+
* collapse the conversation's bulk in a single structural step.
|
|
6
|
+
*
|
|
7
|
+
* It is deliberately NOT incremental. Every form of history compression
|
|
8
|
+
* invalidates the provider's cached prefix — the cache is a prefix, so touching
|
|
9
|
+
* any byte of it recomputes everything after it at full price. A compactor that
|
|
10
|
+
* trims a little each turn therefore pays full reprocessing every turn and saves
|
|
11
|
+
* almost nothing. One shot, high threshold, large payoff. */
|
|
12
|
+
/** The marker every checkpoint message starts with, so a later pass can
|
|
13
|
+
* recognize one it already wrote instead of collapsing it again. */
|
|
14
|
+
export const CHECKPOINT_MARKER = '[checkpoint] ';
|
|
15
|
+
/** The marker every anchor message starts with. An anchor is context ABOUT the
|
|
16
|
+
* user message that follows it, not something the assistant said. */
|
|
17
|
+
export const ANCHOR_MARKER = '[anchor] ';
|
|
18
|
+
/** Window fill ratio at which history compaction becomes worth its cost.
|
|
19
|
+
* High on purpose: see the one-shot reasoning at the top of this file. */
|
|
20
|
+
export const COMPACT_AT_FILL = 0.86;
|
|
21
|
+
/** Whether compacting now is worth rewriting the cached prefix. Two gates: the
|
|
22
|
+
* window is genuinely close to full, AND there is bulk to remove. A compaction
|
|
23
|
+
* that reduces nothing still invalidates the whole prefix, so "nothing to
|
|
24
|
+
* collapse" is a refusal, not a no-op. */
|
|
25
|
+
export function shouldCompact(fill, messages) {
|
|
26
|
+
if (fill < COMPACT_AT_FILL)
|
|
27
|
+
return false;
|
|
28
|
+
return messages.some((m) => m.role !== 'system' && m.role !== 'user');
|
|
29
|
+
}
|
|
30
|
+
/** Fill must fall back below this before compaction re-arms. */
|
|
31
|
+
export const REARM_BELOW_FILL = 0.6;
|
|
32
|
+
/** How much the window must refill, ABOVE where the last compaction left it,
|
|
33
|
+
* for another compaction to be worth its cost. */
|
|
34
|
+
export const REARM_ON_REGROWTH = 0.1;
|
|
35
|
+
/** The gate's state the moment a compaction has run: shut, and waiting for the
|
|
36
|
+
* next usage reading to tell it how far down that compaction got. */
|
|
37
|
+
export function afterCompaction() {
|
|
38
|
+
return { armed: false, floor: null };
|
|
39
|
+
}
|
|
40
|
+
/* ~4 characters per token, matching estimateFactTokens in memory/tier.ts and
|
|
41
|
+
* the chars/4 fallback in @corenel/ui-kit. A BPE count is not worth a
|
|
42
|
+
* dependency for a number whose whole job is to be approximately right. */
|
|
43
|
+
function messageChars(m) {
|
|
44
|
+
let n = 0;
|
|
45
|
+
const content = m.content;
|
|
46
|
+
if (typeof content === 'string')
|
|
47
|
+
n += content.length;
|
|
48
|
+
else if (Array.isArray(content)) {
|
|
49
|
+
for (const part of content) {
|
|
50
|
+
const text = part.text;
|
|
51
|
+
if (typeof text === 'string')
|
|
52
|
+
n += text.length;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/* Tool-call arguments ARE the bulk of an agent conversation and live beside
|
|
56
|
+
* `content`, not in it. Leaving them out would make this read near zero on
|
|
57
|
+
* exactly the histories it exists to catch. */
|
|
58
|
+
const calls = m.tool_calls;
|
|
59
|
+
if (Array.isArray(calls)) {
|
|
60
|
+
for (const c of calls)
|
|
61
|
+
n += (c.function?.name?.length ?? 0) + (c.function?.arguments?.length ?? 0);
|
|
62
|
+
}
|
|
63
|
+
return n;
|
|
64
|
+
}
|
|
65
|
+
/** Window fill ratio read from the messages themselves.
|
|
66
|
+
*
|
|
67
|
+
* For the one turn where no usage reading exists: `fill` comes from the
|
|
68
|
+
* PREVIOUS response, so it is 0 on turn 1 and the gate could never fire before
|
|
69
|
+
* the first request. A run resuming an already-oversized conversation
|
|
70
|
+
* therefore put it on the wire uncompacted and took the fatal 400 immediately
|
|
71
|
+
* -- the case that needs rung 2 most was the case it could not run in.
|
|
72
|
+
*
|
|
73
|
+
* Reads low: the request also carries tool schemas, which are not visible
|
|
74
|
+
* here. That is the right direction to be wrong in, since the cost of reading
|
|
75
|
+
* high is a compaction nobody needed. */
|
|
76
|
+
export function estimateFill(messages, contextWindow) {
|
|
77
|
+
if (!contextWindow || contextWindow <= 0)
|
|
78
|
+
return 0;
|
|
79
|
+
let chars = 0;
|
|
80
|
+
for (const m of messages)
|
|
81
|
+
chars += messageChars(m);
|
|
82
|
+
return Math.min(1, chars / 4 / contextWindow);
|
|
83
|
+
}
|
|
84
|
+
/* The gate, with hysteresis.
|
|
85
|
+
*
|
|
86
|
+
* `shouldCompact` alone says "the window is full and there is bulk" — which
|
|
87
|
+
* stays TRUE on the turn after a compaction, and the one after that. A real
|
|
88
|
+
* run against a live model compacted over and over for exactly that reason,
|
|
89
|
+
* paying a full cached-prefix rewrite each time: the trickle failure this
|
|
90
|
+
* module's own header argues against, reintroduced by the missing edge.
|
|
91
|
+
*
|
|
92
|
+
* So the gate is a latch, not a predicate. It fires once at COMPACT_AT_FILL,
|
|
93
|
+
* disarms, and re-arms only once another compaction would be worth its cost.
|
|
94
|
+
*
|
|
95
|
+
* RE-ARMING ON PROGRESS, NOT ON A MARK. The first version re-armed only below
|
|
96
|
+
* REARM_BELOW_FILL, which assumed a compaction always drains past that mark.
|
|
97
|
+
* Nothing guarantees it: this codebase keeps EVERY user message verbatim, so a
|
|
98
|
+
* compaction's floor is system + all user turns + the checkpoint, and in a long
|
|
99
|
+
* conversation with large pastes that clears 0.6 easily. Landing anywhere in
|
|
100
|
+
* [0.6, 0.86) left the latch stuck SHUT FOREVER -- fill only grows from there,
|
|
101
|
+
* so it could never fall back under the mark, and the run compacted exactly
|
|
102
|
+
* once before marching into the wall it was supposed to prevent. Silently: a
|
|
103
|
+
* gate that never fires looks identical to a conversation that never got big.
|
|
104
|
+
*
|
|
105
|
+
* So the question is not "how full is the window" but "has it grown enough
|
|
106
|
+
* since the last compaction to have something new to collapse". Growth of
|
|
107
|
+
* REARM_ON_REGROWTH past that floor means yes, whatever the absolute level.
|
|
108
|
+
* The drained-window rule is kept alongside it: it is the cheapest possible
|
|
109
|
+
* signal and stays correct. Between the marks, with nothing new accumulated,
|
|
110
|
+
* the gate still does nothing -- which remains the right answer, because a
|
|
111
|
+
* second compaction so soon has almost nothing left to remove and would charge
|
|
112
|
+
* a full prefix rewrite to discover that. */
|
|
113
|
+
export function compactionDecision(fill, messages, gate) {
|
|
114
|
+
if (!gate.armed) {
|
|
115
|
+
const drained = fill < REARM_BELOW_FILL;
|
|
116
|
+
const regrown = gate.floor !== null && fill >= gate.floor + REARM_ON_REGROWTH;
|
|
117
|
+
return { compact: false, next: { ...gate, armed: drained || regrown } };
|
|
118
|
+
}
|
|
119
|
+
if (!shouldCompact(fill, messages))
|
|
120
|
+
return { compact: false, next: gate };
|
|
121
|
+
return { compact: true, next: afterCompaction() };
|
|
122
|
+
}
|
|
123
|
+
export async function compactHistory(messages, opts) {
|
|
124
|
+
// A trailing assistant message that has asked for tools but not yet been
|
|
125
|
+
// answered is held back from the collapse: the loop appends its `tool`
|
|
126
|
+
// results next, and those results need this parent to exist or the provider
|
|
127
|
+
// rejects the request with a fatal 400.
|
|
128
|
+
const tail = messages.length ? messages[messages.length - 1] : undefined;
|
|
129
|
+
const pending = tail && isUnansweredToolCall(tail) ? tail : null;
|
|
130
|
+
const body = pending ? messages.slice(0, -1) : messages;
|
|
131
|
+
const system = body.filter((m) => m.role === 'system');
|
|
132
|
+
const collapsed = body.filter((m) => m.role !== 'system' && m.role !== 'user');
|
|
133
|
+
const kept = [];
|
|
134
|
+
let answered = null;
|
|
135
|
+
for (const m of body) {
|
|
136
|
+
/* Skip THIS MODULE'S OWN messages, both as anchor candidates and as
|
|
137
|
+
* things to re-anchor. An anchor is an assistant message with string
|
|
138
|
+
* content, so a naive walk picks up the previous compaction's anchors and
|
|
139
|
+
* anchors those — a real run produced
|
|
140
|
+
* "[anchor] answering: [anchor] answering: [anchor] answering: ...".
|
|
141
|
+
* These are bookkeeping about the conversation, not part of it. */
|
|
142
|
+
if (isOwnMarker(m))
|
|
143
|
+
continue;
|
|
144
|
+
if (m.role === 'assistant' && typeof m.content === 'string')
|
|
145
|
+
answered = m;
|
|
146
|
+
if (m.role !== 'user')
|
|
147
|
+
continue;
|
|
148
|
+
if (opts.anchor) {
|
|
149
|
+
kept.push({ role: 'assistant', content: `${ANCHOR_MARKER}${await opts.anchor(m, answered)}` });
|
|
150
|
+
}
|
|
151
|
+
kept.push(await offloadIfOversized(m, opts));
|
|
152
|
+
}
|
|
153
|
+
const summary = await opts.summarize(collapsed);
|
|
154
|
+
return {
|
|
155
|
+
messages: [
|
|
156
|
+
...system,
|
|
157
|
+
{ role: 'assistant', content: `${CHECKPOINT_MARKER}${summary}` },
|
|
158
|
+
...kept,
|
|
159
|
+
...(pending ? [pending] : []),
|
|
160
|
+
],
|
|
161
|
+
checkpoint: summary,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** A user turn's DECISION text is small and stays verbatim; a pasted log is
|
|
165
|
+
* bulk and gets rung 1's treatment. Anything that is not oversized plain text
|
|
166
|
+
* is returned untouched. */
|
|
167
|
+
async function offloadIfOversized(m, opts) {
|
|
168
|
+
const limit = opts.offloadOverBytes;
|
|
169
|
+
if (!opts.offload || limit === undefined)
|
|
170
|
+
return m;
|
|
171
|
+
if (typeof m.content !== 'string' || m.content.length <= limit)
|
|
172
|
+
return m;
|
|
173
|
+
return { ...m, content: await opts.offload(m.content) };
|
|
174
|
+
}
|
|
175
|
+
/** An anchor or checkpoint a compaction wrote — bookkeeping ABOUT the
|
|
176
|
+
* conversation rather than part of it. Exported because a caller that has to
|
|
177
|
+
* tell "what this run added" from "what a rewrite inserted" cannot do it by
|
|
178
|
+
* position: a rewrite puts fresh objects into the array at arbitrary places. */
|
|
179
|
+
export const isCompactionMarker = isOwnMarker;
|
|
180
|
+
/** An anchor or checkpoint this module wrote on an earlier pass. Dropped and
|
|
181
|
+
* rewritten each time rather than accumulated: the newest checkpoint already
|
|
182
|
+
* subsumes what the older ones recorded. */
|
|
183
|
+
function isOwnMarker(m) {
|
|
184
|
+
if (m.role !== 'assistant' || typeof m.content !== 'string')
|
|
185
|
+
return false;
|
|
186
|
+
return m.content.startsWith(ANCHOR_MARKER) || m.content.startsWith(CHECKPOINT_MARKER);
|
|
187
|
+
}
|
|
188
|
+
function isUnansweredToolCall(m) {
|
|
189
|
+
return m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length > 0;
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=compactHistory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compactHistory.js","sourceRoot":"","sources":["../../core/compactHistory.ts"],"names":[],"mappings":"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"}
|
package/dist/core/loop.d.ts
CHANGED
|
@@ -40,6 +40,17 @@ export interface RunAgentArgs {
|
|
|
40
40
|
describePermissionDenial?: (toolName: string) => string;
|
|
41
41
|
/** Context-compression config (rung-1 offload). Omit to disable. */
|
|
42
42
|
compression?: CompressionConfig;
|
|
43
|
+
/** Rung-2 history compaction. Omit and history is never compacted — the run
|
|
44
|
+
* then behaves exactly as it did before this existed, which is to say it
|
|
45
|
+
* runs until the provider rejects an oversized request with a fatal 400.
|
|
46
|
+
* `summarize` is a host concern because it costs a model call; the host also
|
|
47
|
+
* decides where `onCheckpoint` persists the body (a file the user can edit,
|
|
48
|
+
* not an invisible blob). */
|
|
49
|
+
compaction?: {
|
|
50
|
+
summarize: (collapsed: ChatCompletionMessageParam[]) => Promise<string>;
|
|
51
|
+
anchor?: (user: ChatCompletionMessageParam, answered: ChatCompletionMessageParam | null) => Promise<string>;
|
|
52
|
+
onCheckpoint?: (checkpoint: string) => void | Promise<void>;
|
|
53
|
+
};
|
|
43
54
|
/** Intra-run self-healing budget (stream-retry + tool-steer). Omit for defaults. */
|
|
44
55
|
recovery?: Partial<RecoveryConfig>;
|
|
45
56
|
/** Optional output-token ceiling forwarded to the model on every turn (reasoning
|
|
@@ -73,6 +84,20 @@ export interface RunAgentResult {
|
|
|
73
84
|
text: string;
|
|
74
85
|
usage: Usage;
|
|
75
86
|
stopReason: string;
|
|
87
|
+
/** How many messages at the TAIL of `messages` this run added.
|
|
88
|
+
*
|
|
89
|
+
* A caller stitching the new messages onto its own transcript cannot compute
|
|
90
|
+
* this from what it sent: rung-2 compaction rewrites `messages` in place and
|
|
91
|
+
* the rewrite is SHORTER than the input -- that is the point of it -- so an
|
|
92
|
+
* absolute offset into the result runs off the end and the caller silently
|
|
93
|
+
* keeps nothing. The browser runtime did exactly that, and dropped the reply
|
|
94
|
+
* of every run that compacted.
|
|
95
|
+
*
|
|
96
|
+
* Counted from the tail by object identity, so no future `messages.push` can
|
|
97
|
+
* forget to maintain it. A run whose earlier messages were collapsed into a
|
|
98
|
+
* checkpoint reports only what survives: those messages are genuinely gone
|
|
99
|
+
* from the array, and the seam in the transcript is what stands in for them. */
|
|
100
|
+
appended: number;
|
|
76
101
|
}
|
|
77
102
|
/** The most tools one request may carry.
|
|
78
103
|
*
|
package/dist/core/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../core/loop.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,0BAA0B,EAAsB,MAAM,mCAAmC,CAAC;AACxG,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,EAA4B,MAAM,mBAAmB,CAAC;AAE7G,OAAO,EACY,KAAK,cAAc,EAErC,MAAM,YAAY,CAAC;AAMpB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAA0D,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../core/loop.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,0BAA0B,EAAsB,MAAM,mCAAmC,CAAC;AACxG,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,EAA4B,MAAM,mBAAmB,CAAC;AAE7G,OAAO,EACY,KAAK,cAAc,EAErC,MAAM,YAAY,CAAC;AAMpB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAA0D,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAKhH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd;kDAC8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,0BAA0B,EAAE,CAAC;IACvC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB;;;kFAG8E;IAC9E,aAAa,CAAC,EAAE,SAAS,EAAE,CAAC;IAC5B;oFACgF;IAChF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;;;;;;;;6EAQyE;IACzE,wBAAwB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IACxD,oEAAoE;IACpE,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC;;;;;iCAK6B;IAC7B,UAAU,CAAC,EAAE;QACX,SAAS,EAAE,CAAC,SAAS,EAAE,0BAA0B,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;QACxE,MAAM,CAAC,EAAE,CACP,IAAI,EAAE,0BAA0B,EAChC,QAAQ,EAAE,0BAA0B,GAAG,IAAI,KACxC,OAAO,CAAC,MAAM,CAAC,CAAC;QACrB,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KAC7D,CAAC;IACF,oFAAoF;IACpF,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IACnC;0FACsF;IACtF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;yEAOqE;IACrE,aAAa,CAAC,EAAE,MAAM,0BAA0B,EAAE,CAAC;IACnD;;4DAEwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,OAAO,CAAC;IACpC;;;0EAGsE;IACtE,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACpD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,0BAA0B,EAAE,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;oFAYgF;IAChF,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;gEAWgE;AAChE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAugB1E"}
|
package/dist/core/loop.js
CHANGED
|
@@ -6,6 +6,7 @@ import { checkFeature } from '../guards.js';
|
|
|
6
6
|
import { STANDARD_POLICY } from '../guardrail/builtins.js';
|
|
7
7
|
import { toolAddress } from '../tools/namespaces.js';
|
|
8
8
|
import { effectiveThreshold, formatOffloadRef, isOffloadPointer } from '../compression.js';
|
|
9
|
+
import { compactHistory, compactionDecision, estimateFill, isCompactionMarker } from './compactHistory.js';
|
|
9
10
|
import { fillToolsMarker } from '../prompts/toolsMarker.js';
|
|
10
11
|
import { createToolLoader, serverIndex } from '../tools/toolSearch.js';
|
|
11
12
|
/** The most tools one request may carry.
|
|
@@ -29,6 +30,10 @@ export async function runAgent(args) {
|
|
|
29
30
|
const ctxWindow = compression?.contextWindow;
|
|
30
31
|
const compressionStrategy = compression?.strategy ?? 'eager';
|
|
31
32
|
let fill = 0; // window fill ratio; drives the adaptive offload threshold
|
|
33
|
+
/* Compaction is a LATCH, not a per-turn predicate -- see compactionDecision.
|
|
34
|
+
* Armed at the start of a run; disarmed by a compaction; re-armed once the
|
|
35
|
+
* window has drained, or has refilled past where that compaction left it. */
|
|
36
|
+
let compactGate = { armed: true, floor: null };
|
|
32
37
|
// 'deferred' strategy only: results offloaded THIS turn, awaiting the ONE
|
|
33
38
|
// streamCompletion call that sends them in full (the turn right after
|
|
34
39
|
// they're produced — the model that asked for the result reads it). As soon
|
|
@@ -54,6 +59,24 @@ export async function runAgent(args) {
|
|
|
54
59
|
signal.addEventListener('abort', () => { turnAc?.abort(); });
|
|
55
60
|
args.registerInterrupt?.(() => { interrupted = true; turnAc?.abort(); });
|
|
56
61
|
const messages = [...args.messages];
|
|
62
|
+
/* The messages that came IN, by identity. Everything this run appends is a
|
|
63
|
+
* fresh object, and everything that survives a compaction from the input is
|
|
64
|
+
* the same object, so walking back from the tail until an input message
|
|
65
|
+
* appears is an exact count of what this run added -- maintained by nothing,
|
|
66
|
+
* and therefore impossible for a later `push` site to get wrong. */
|
|
67
|
+
const incoming = new Set(args.messages);
|
|
68
|
+
const appendedFromTail = () => {
|
|
69
|
+
let n = 0;
|
|
70
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
71
|
+
const m = messages[i];
|
|
72
|
+
// A compaction's own checkpoint/anchor, and the system prompt unshifted
|
|
73
|
+
// below, are fresh objects too -- and neither is something this run said.
|
|
74
|
+
if (incoming.has(m) || m.role === 'system' || isCompactionMarker(m))
|
|
75
|
+
break;
|
|
76
|
+
n++;
|
|
77
|
+
}
|
|
78
|
+
return n;
|
|
79
|
+
};
|
|
57
80
|
/* DEFERRED TOOLS: sent only once found. The loader owns the loaded set;
|
|
58
81
|
* `activeTools` is what one request may carry right now -- the eager tools,
|
|
59
82
|
* `tool_search`, then the loaded ones, capped. Eager first so a full loaded
|
|
@@ -138,6 +161,75 @@ export async function runAgent(args) {
|
|
|
138
161
|
emit({ type: 'injected', content: typeof m.content === 'string' ? m.content : '' });
|
|
139
162
|
}
|
|
140
163
|
}
|
|
164
|
+
/* Rung-2 history compaction, BEFORE the request is built so the compacted
|
|
165
|
+
* array is what goes on the wire. `fill` is last turn's prompt/window
|
|
166
|
+
* ratio; shouldCompact refuses below the threshold and refuses again when
|
|
167
|
+
* there is nothing collapsible, because a compaction that removes nothing
|
|
168
|
+
* still rewrites the cached prefix and re-bills everything after it.
|
|
169
|
+
* Rebuilt in place: callers (and `res.messages`) hold this array. */
|
|
170
|
+
/* On turn 1 there is no usage reading yet, so `fill` is 0 and the gate
|
|
171
|
+
* could not fire before the first request -- which is precisely how a run
|
|
172
|
+
* resuming an already-oversized conversation put it on the wire and took
|
|
173
|
+
* the fatal 400. Fall back to reading the window off the messages. Only as
|
|
174
|
+
* a fallback: a real reading is always better than an estimate that cannot
|
|
175
|
+
* see the tool schemas. */
|
|
176
|
+
const gateFill = fill || estimateFill(messages, ctxWindow);
|
|
177
|
+
const gate = compactionDecision(gateFill, messages, compactGate);
|
|
178
|
+
compactGate = gate.next;
|
|
179
|
+
if (args.compaction && gate.compact) {
|
|
180
|
+
const before = messages.length;
|
|
181
|
+
/* A summarizer failure is the same shape as a persist failure: the window
|
|
182
|
+
* is full, the model call meant to compress it did not come back, and the
|
|
183
|
+
* honest response is to carry on UNCOMPACTED rather than end the run on
|
|
184
|
+
* the recovery path. The turn then proceeds with the history it had,
|
|
185
|
+
* which is exactly what would have happened without compaction at all. */
|
|
186
|
+
try {
|
|
187
|
+
const out = await compactHistory(messages, {
|
|
188
|
+
summarize: args.compaction.summarize,
|
|
189
|
+
anchor: args.compaction.anchor,
|
|
190
|
+
offloadOverBytes: compression && compression.mode !== 'off'
|
|
191
|
+
? effectiveThreshold(compression.mode, gateFill)
|
|
192
|
+
: undefined,
|
|
193
|
+
offload: toolCtx.recall
|
|
194
|
+
// Recorded under 'user' rather than a tool name: the RecallStore is
|
|
195
|
+
// indifferent to who authored the bulk, but the transcript should not
|
|
196
|
+
// claim a tool produced what a person pasted.
|
|
197
|
+
? async (content) => formatOffloadRef(await toolCtx.recall.record('user', content))
|
|
198
|
+
: undefined,
|
|
199
|
+
});
|
|
200
|
+
messages.splice(0, messages.length, ...out.messages);
|
|
201
|
+
/* The 'deferred' strategy holds ABSOLUTE indices into `messages`, to
|
|
202
|
+
* rewrite those entries once the model has seen them in full. The splice
|
|
203
|
+
* above just invalidated every one of them, and the rewrite pass read
|
|
204
|
+
* `.role` off the resulting undefined -- a crash in the interactive
|
|
205
|
+
* CLI's shipping configuration ('deferred' + compaction). Nothing is
|
|
206
|
+
* lost by dropping them: the tool results they point at were collapsed
|
|
207
|
+
* into the checkpoint and are not in `messages` any more. */
|
|
208
|
+
deferredOffloads.length = 0;
|
|
209
|
+
/* The compaction has ALREADY happened by this point. Failing to RECORD it
|
|
210
|
+
* must not take the run with it -- checkpointStore.append deliberately
|
|
211
|
+
* rejects rather than risk clobbering an existing CHECKPOINT.md, and a
|
|
212
|
+
* read-only workspace rejects too. Dying here would lose the whole
|
|
213
|
+
* conversation in order to save nothing. */
|
|
214
|
+
try {
|
|
215
|
+
await args.compaction.onCheckpoint?.(out.checkpoint);
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
alog('checkpoint persist failed', e instanceof Error ? e.message : String(e));
|
|
219
|
+
}
|
|
220
|
+
emit({ type: 'checkpoint', collapsed: before - messages.length, checkpoint: out.checkpoint });
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
/* Compaction failed (most likely the summarizer's own model call). The
|
|
224
|
+
* turn proceeds with the history it had -- exactly what would have
|
|
225
|
+
* happened had compaction never been configured. Re-arm so a later turn
|
|
226
|
+
* may try again rather than giving up on the run for good. */
|
|
227
|
+
/* The floor is left as it was: it describes where the last SUCCESSFUL
|
|
228
|
+
* compaction landed, and a failed attempt moved nothing. */
|
|
229
|
+
compactGate = { ...compactGate, armed: true };
|
|
230
|
+
alog('compaction failed', e instanceof Error ? e.message : String(e));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
141
233
|
// Turn-budget guardrail (gstate.turn = completed turns).
|
|
142
234
|
gstate.turn = turn - 1;
|
|
143
235
|
const turnGate = evaluateTurnStart(policy, gstate);
|
|
@@ -263,6 +355,12 @@ export async function runAgent(args) {
|
|
|
263
355
|
usage.completionTokens += streamed.usage.completionTokens;
|
|
264
356
|
if (ctxWindow && ctxWindow > 0 && streamed.usage.promptTokens) {
|
|
265
357
|
fill = Math.min(1, streamed.usage.promptTokens / ctxWindow);
|
|
358
|
+
/* The first real reading after a compaction is how far down that
|
|
359
|
+
* compaction actually got, and re-arming is judged against it from here
|
|
360
|
+
* on. Measured, never estimated: it is compared with later readings of
|
|
361
|
+
* the same kind. */
|
|
362
|
+
if (!compactGate.armed && compactGate.floor === null)
|
|
363
|
+
compactGate = { armed: false, floor: fill };
|
|
266
364
|
}
|
|
267
365
|
emit({ type: 'usage', usage: streamed.usage });
|
|
268
366
|
// Commit the assistant message to history (with tool_calls if any).
|
|
@@ -454,7 +552,7 @@ export async function runAgent(args) {
|
|
|
454
552
|
}
|
|
455
553
|
alog('done', stopReason, 'tokens', usage);
|
|
456
554
|
emit({ type: 'done', reason: stopReason });
|
|
457
|
-
return { messages, text, usage, stopReason };
|
|
555
|
+
return { messages, text, usage, stopReason, appended: appendedFromTail() };
|
|
458
556
|
}
|
|
459
557
|
/** 'deferred' strategy: compact any ALREADY-oversized tool result already
|
|
460
558
|
* sitting in the incoming history — i.e. produced by an earlier `runAgent()`
|