@corenel/harness 0.7.5 → 0.9.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 +206 -0
- package/dist/core/compactHistory.js.map +1 -0
- package/dist/core/contextPressure.d.ts +45 -0
- package/dist/core/contextPressure.d.ts.map +1 -0
- package/dist/core/contextPressure.js +69 -0
- package/dist/core/contextPressure.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 +176 -13
- package/dist/core/loop.js.map +1 -1
- package/dist/core/pricing.d.ts +30 -1
- package/dist/core/pricing.d.ts.map +1 -1
- package/dist/core/pricing.js +50 -21
- package/dist/core/pricing.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/guardrail/builtins.d.ts.map +1 -1
- package/dist/guardrail/builtins.js +9 -3
- package/dist/guardrail/builtins.js.map +1 -1
- package/dist/guardrail/validate.d.ts.map +1 -1
- package/dist/guardrail/validate.js +6 -2
- package/dist/guardrail/validate.js.map +1 -1
- package/dist/prompts/host.d.ts +23 -0
- package/dist/prompts/host.d.ts.map +1 -1
- package/dist/prompts/host.js.map +1 -1
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +43 -2
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/library/defaults.generated.js +5 -5
- package/dist/prompts/library/defaults.generated.js.map +1 -1
- package/dist/providers/contextWindow.d.ts +23 -4
- package/dist/providers/contextWindow.d.ts.map +1 -1
- package/dist/providers/contextWindow.js +25 -0
- package/dist/providers/contextWindow.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/skillSource.d.ts +36 -0
- package/dist/skillSource.d.ts.map +1 -0
- package/dist/skillSource.js +105 -0
- package/dist/skillSource.js.map +1 -0
- package/dist/skills.d.ts +38 -2
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +64 -9
- package/dist/skills.js.map +1 -1
- package/dist/tools/builtins.d.ts.map +1 -1
- package/dist/tools/builtins.js +50 -2
- 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 +2 -2
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;AAGpF,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,CA8C/B;AAeD;;;gFAGgF;AAChF,eAAO,MAAM,kBAAkB,oBAAc,CAAC;AAY9C;;4CAE4C;AAC5C,iBAAS,WAAW,CAAC,CAAC,EAAE,0BAA0B,GAAG,OAAO,CAG3D"}
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
import { isContextNote } from './contextPressure.js';
|
|
13
|
+
/** The marker every checkpoint message starts with, so a later pass can
|
|
14
|
+
* recognize one it already wrote instead of collapsing it again. */
|
|
15
|
+
export const CHECKPOINT_MARKER = '[checkpoint] ';
|
|
16
|
+
/** The marker every anchor message starts with. An anchor is context ABOUT the
|
|
17
|
+
* user message that follows it, not something the assistant said. */
|
|
18
|
+
export const ANCHOR_MARKER = '[anchor] ';
|
|
19
|
+
/** Window fill ratio at which history compaction becomes worth its cost.
|
|
20
|
+
* High on purpose: see the one-shot reasoning at the top of this file. */
|
|
21
|
+
export const COMPACT_AT_FILL = 0.86;
|
|
22
|
+
/** Whether compacting now is worth rewriting the cached prefix. Two gates: the
|
|
23
|
+
* window is genuinely close to full, AND there is bulk to remove. A compaction
|
|
24
|
+
* that reduces nothing still invalidates the whole prefix, so "nothing to
|
|
25
|
+
* collapse" is a refusal, not a no-op. */
|
|
26
|
+
export function shouldCompact(fill, messages) {
|
|
27
|
+
if (fill < COMPACT_AT_FILL)
|
|
28
|
+
return false;
|
|
29
|
+
return messages.some((m) => m.role !== 'system' && m.role !== 'user');
|
|
30
|
+
}
|
|
31
|
+
/** Fill must fall back below this before compaction re-arms. */
|
|
32
|
+
export const REARM_BELOW_FILL = 0.6;
|
|
33
|
+
/** How much the window must refill, ABOVE where the last compaction left it,
|
|
34
|
+
* for another compaction to be worth its cost. */
|
|
35
|
+
export const REARM_ON_REGROWTH = 0.1;
|
|
36
|
+
/** The gate's state the moment a compaction has run: shut, and waiting for the
|
|
37
|
+
* next usage reading to tell it how far down that compaction got. */
|
|
38
|
+
export function afterCompaction() {
|
|
39
|
+
return { armed: false, floor: null };
|
|
40
|
+
}
|
|
41
|
+
/* ~4 characters per token, matching estimateFactTokens in memory/tier.ts and
|
|
42
|
+
* the chars/4 fallback in @corenel/ui-kit. A BPE count is not worth a
|
|
43
|
+
* dependency for a number whose whole job is to be approximately right. */
|
|
44
|
+
function messageChars(m) {
|
|
45
|
+
let n = 0;
|
|
46
|
+
const content = m.content;
|
|
47
|
+
if (typeof content === 'string')
|
|
48
|
+
n += content.length;
|
|
49
|
+
else if (Array.isArray(content)) {
|
|
50
|
+
for (const part of content) {
|
|
51
|
+
const text = part.text;
|
|
52
|
+
if (typeof text === 'string')
|
|
53
|
+
n += text.length;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/* Tool-call arguments ARE the bulk of an agent conversation and live beside
|
|
57
|
+
* `content`, not in it. Leaving them out would make this read near zero on
|
|
58
|
+
* exactly the histories it exists to catch. */
|
|
59
|
+
const calls = m.tool_calls;
|
|
60
|
+
if (Array.isArray(calls)) {
|
|
61
|
+
for (const c of calls)
|
|
62
|
+
n += (c.function?.name?.length ?? 0) + (c.function?.arguments?.length ?? 0);
|
|
63
|
+
}
|
|
64
|
+
return n;
|
|
65
|
+
}
|
|
66
|
+
/** Window fill ratio read from the messages themselves.
|
|
67
|
+
*
|
|
68
|
+
* For the one turn where no usage reading exists: `fill` comes from the
|
|
69
|
+
* PREVIOUS response, so it is 0 on turn 1 and the gate could never fire before
|
|
70
|
+
* the first request. A run resuming an already-oversized conversation
|
|
71
|
+
* therefore put it on the wire uncompacted and took the fatal 400 immediately
|
|
72
|
+
* -- the case that needs rung 2 most was the case it could not run in.
|
|
73
|
+
*
|
|
74
|
+
* Reads low: the request also carries tool schemas, which are not visible
|
|
75
|
+
* here. That is the right direction to be wrong in, since the cost of reading
|
|
76
|
+
* high is a compaction nobody needed. */
|
|
77
|
+
export function estimateFill(messages, contextWindow) {
|
|
78
|
+
if (!contextWindow || contextWindow <= 0)
|
|
79
|
+
return 0;
|
|
80
|
+
let chars = 0;
|
|
81
|
+
for (const m of messages)
|
|
82
|
+
chars += messageChars(m);
|
|
83
|
+
return Math.min(1, chars / 4 / contextWindow);
|
|
84
|
+
}
|
|
85
|
+
/* The gate, with hysteresis.
|
|
86
|
+
*
|
|
87
|
+
* `shouldCompact` alone says "the window is full and there is bulk" — which
|
|
88
|
+
* stays TRUE on the turn after a compaction, and the one after that. A real
|
|
89
|
+
* run against a live model compacted over and over for exactly that reason,
|
|
90
|
+
* paying a full cached-prefix rewrite each time: the trickle failure this
|
|
91
|
+
* module's own header argues against, reintroduced by the missing edge.
|
|
92
|
+
*
|
|
93
|
+
* So the gate is a latch, not a predicate. It fires once at COMPACT_AT_FILL,
|
|
94
|
+
* disarms, and re-arms only once another compaction would be worth its cost.
|
|
95
|
+
*
|
|
96
|
+
* RE-ARMING ON PROGRESS, NOT ON A MARK. The first version re-armed only below
|
|
97
|
+
* REARM_BELOW_FILL, which assumed a compaction always drains past that mark.
|
|
98
|
+
* Nothing guarantees it: this codebase keeps EVERY user message verbatim, so a
|
|
99
|
+
* compaction's floor is system + all user turns + the checkpoint, and in a long
|
|
100
|
+
* conversation with large pastes that clears 0.6 easily. Landing anywhere in
|
|
101
|
+
* [0.6, 0.86) left the latch stuck SHUT FOREVER -- fill only grows from there,
|
|
102
|
+
* so it could never fall back under the mark, and the run compacted exactly
|
|
103
|
+
* once before marching into the wall it was supposed to prevent. Silently: a
|
|
104
|
+
* gate that never fires looks identical to a conversation that never got big.
|
|
105
|
+
*
|
|
106
|
+
* So the question is not "how full is the window" but "has it grown enough
|
|
107
|
+
* since the last compaction to have something new to collapse". Growth of
|
|
108
|
+
* REARM_ON_REGROWTH past that floor means yes, whatever the absolute level.
|
|
109
|
+
* The drained-window rule is kept alongside it: it is the cheapest possible
|
|
110
|
+
* signal and stays correct. Between the marks, with nothing new accumulated,
|
|
111
|
+
* the gate still does nothing -- which remains the right answer, because a
|
|
112
|
+
* second compaction so soon has almost nothing left to remove and would charge
|
|
113
|
+
* a full prefix rewrite to discover that. */
|
|
114
|
+
export function compactionDecision(fill, messages, gate) {
|
|
115
|
+
if (!gate.armed) {
|
|
116
|
+
const drained = fill < REARM_BELOW_FILL;
|
|
117
|
+
const regrown = gate.floor !== null && fill >= gate.floor + REARM_ON_REGROWTH;
|
|
118
|
+
return { compact: false, next: { ...gate, armed: drained || regrown } };
|
|
119
|
+
}
|
|
120
|
+
if (!shouldCompact(fill, messages))
|
|
121
|
+
return { compact: false, next: gate };
|
|
122
|
+
return { compact: true, next: afterCompaction() };
|
|
123
|
+
}
|
|
124
|
+
export async function compactHistory(messages, opts) {
|
|
125
|
+
// A trailing assistant message that has asked for tools but not yet been
|
|
126
|
+
// answered is held back from the collapse: the loop appends its `tool`
|
|
127
|
+
// results next, and those results need this parent to exist or the provider
|
|
128
|
+
// rejects the request with a fatal 400.
|
|
129
|
+
const tail = messages.length ? messages[messages.length - 1] : undefined;
|
|
130
|
+
const pending = tail && isUnansweredToolCall(tail) ? tail : null;
|
|
131
|
+
const body = pending ? messages.slice(0, -1) : messages;
|
|
132
|
+
const system = body.filter((m) => m.role === 'system');
|
|
133
|
+
/* A stale pressure notice is DROPPED, not summarized. "Context is 80% full"
|
|
134
|
+
* is a fact about the window this compaction is in the middle of emptying,
|
|
135
|
+
* and letting it into the summarizer's input would write it into the
|
|
136
|
+
* checkpoint as though it were something that happened in the conversation.
|
|
137
|
+
* The agent gets a fresh one on the next crossing if it earns it. */
|
|
138
|
+
const collapsed = body.filter((m) => m.role !== 'system' && m.role !== 'user' && !isContextNote(m));
|
|
139
|
+
const kept = [];
|
|
140
|
+
let answered = null;
|
|
141
|
+
for (const m of body) {
|
|
142
|
+
/* Skip THIS MODULE'S OWN messages, both as anchor candidates and as
|
|
143
|
+
* things to re-anchor. An anchor is an assistant message with string
|
|
144
|
+
* content, so a naive walk picks up the previous compaction's anchors and
|
|
145
|
+
* anchors those — a real run produced
|
|
146
|
+
* "[anchor] answering: [anchor] answering: [anchor] answering: ...".
|
|
147
|
+
* These are bookkeeping about the conversation, not part of it. */
|
|
148
|
+
if (isOwnMarker(m) || isContextNote(m))
|
|
149
|
+
continue;
|
|
150
|
+
if (m.role === 'assistant' && typeof m.content === 'string')
|
|
151
|
+
answered = m;
|
|
152
|
+
if (m.role !== 'user')
|
|
153
|
+
continue;
|
|
154
|
+
if (opts.anchor) {
|
|
155
|
+
kept.push({ role: 'assistant', content: `${ANCHOR_MARKER}${await opts.anchor(m, answered)}` });
|
|
156
|
+
}
|
|
157
|
+
kept.push(await offloadIfOversized(m, opts));
|
|
158
|
+
}
|
|
159
|
+
const summary = await opts.summarize(collapsed);
|
|
160
|
+
return {
|
|
161
|
+
messages: [
|
|
162
|
+
...system,
|
|
163
|
+
{ role: 'assistant', content: `${CHECKPOINT_MARKER}${summary}` },
|
|
164
|
+
...kept,
|
|
165
|
+
...(pending ? [pending] : []),
|
|
166
|
+
],
|
|
167
|
+
checkpoint: summary,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** A user turn's DECISION text is small and stays verbatim; a pasted log is
|
|
171
|
+
* bulk and gets rung 1's treatment. Anything that is not oversized plain text
|
|
172
|
+
* is returned untouched. */
|
|
173
|
+
async function offloadIfOversized(m, opts) {
|
|
174
|
+
const limit = opts.offloadOverBytes;
|
|
175
|
+
if (!opts.offload || limit === undefined)
|
|
176
|
+
return m;
|
|
177
|
+
if (typeof m.content !== 'string' || m.content.length <= limit)
|
|
178
|
+
return m;
|
|
179
|
+
return { ...m, content: await opts.offload(m.content) };
|
|
180
|
+
}
|
|
181
|
+
/** An anchor or checkpoint a compaction wrote — bookkeeping ABOUT the
|
|
182
|
+
* conversation rather than part of it. Exported because a caller that has to
|
|
183
|
+
* tell "what this run added" from "what a rewrite inserted" cannot do it by
|
|
184
|
+
* position: a rewrite puts fresh objects into the array at arbitrary places. */
|
|
185
|
+
export const isCompactionMarker = isOwnMarker;
|
|
186
|
+
/* A context-pressure notice (core/contextPressure.ts) is bookkeeping too, and
|
|
187
|
+
* a STALE one: it says the window was 80% full, which a compaction has just
|
|
188
|
+
* made false. So the collapse above drops it -- but it is deliberately NOT
|
|
189
|
+
* folded into `isCompactionMarker`. That predicate makes the loop's
|
|
190
|
+
* `appendedFromTail` walk STOP, and a pressure notice sits in the middle of a
|
|
191
|
+
* run's own appended messages rather than at the head of the kept region:
|
|
192
|
+
* conflating them silently truncates every `appended` count taken after a
|
|
193
|
+
* notice fired, and the host then persists a partial tail. One predicate, two
|
|
194
|
+
* very different jobs -- loop.contextPressure.test.ts pins the difference. */
|
|
195
|
+
/** An anchor or checkpoint this module wrote on an earlier pass. Dropped and
|
|
196
|
+
* rewritten each time rather than accumulated: the newest checkpoint already
|
|
197
|
+
* subsumes what the older ones recorded. */
|
|
198
|
+
function isOwnMarker(m) {
|
|
199
|
+
if (m.role !== 'assistant' || typeof m.content !== 'string')
|
|
200
|
+
return false;
|
|
201
|
+
return m.content.startsWith(ANCHOR_MARKER) || m.content.startsWith(CHECKPOINT_MARKER);
|
|
202
|
+
}
|
|
203
|
+
function isUnansweredToolCall(m) {
|
|
204
|
+
return m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length > 0;
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=compactHistory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compactHistory.js","sourceRoot":"","sources":["../../core/compactHistory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
|
|
2
|
+
/** Marks a note this module appended. Same shape as CHECKPOINT_MARKER/
|
|
3
|
+
* ANCHOR_MARKER so the compactor can recognise and drop it. */
|
|
4
|
+
export declare const CONTEXT_MARKER = "[context] ";
|
|
5
|
+
/** Fill ratios worth interrupting for, ascending.
|
|
6
|
+
*
|
|
7
|
+
* 0.6 is where behavior should start bending (read narrower, summarize).
|
|
8
|
+
* 0.8 is where new exploration should stop. 0.9 is the last useful warning --
|
|
9
|
+
* COMPACT_AT_FILL is 0.86, so by 0.9 either compaction is not configured or it
|
|
10
|
+
* ran and did not get far enough, and both mean answer now. */
|
|
11
|
+
export declare const PRESSURE_BANDS: readonly [0.6, 0.8, 0.9];
|
|
12
|
+
export type PressureBand = (typeof PRESSURE_BANDS)[number];
|
|
13
|
+
/** How far fill must fall BELOW a band before that band counts as un-crossed
|
|
14
|
+
* and may announce again. Without it, a run hovering at 0.79/0.81 re-announces
|
|
15
|
+
* on every other turn -- the notice becomes noise and costs tokens each time. */
|
|
16
|
+
export declare const PRESSURE_HYSTERESIS = 0.05;
|
|
17
|
+
/** The highest band already announced; 0 before any. */
|
|
18
|
+
export interface PressureGate {
|
|
19
|
+
crossed: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const initialPressureGate: PressureGate;
|
|
22
|
+
/**
|
|
23
|
+
* Decide whether this turn should announce context pressure.
|
|
24
|
+
*
|
|
25
|
+
* `band` is non-null at most once per crossing. After a compaction drains the
|
|
26
|
+
* window, the gate falls back to whatever band the new fill sits in, so
|
|
27
|
+
* climbing into 80% a second time is news a second time -- which is right: it
|
|
28
|
+
* is a different 80%, reached after the history was already collapsed once.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pressureDecision(fill: number, gate: PressureGate): {
|
|
31
|
+
band: PressureBand | null;
|
|
32
|
+
next: PressureGate;
|
|
33
|
+
};
|
|
34
|
+
/** What the agent is told at each band. Each one names a CHANGE IN BEHAVIOR,
|
|
35
|
+
* not just a number -- a percentage with no instruction attached is something
|
|
36
|
+
* to acknowledge and then ignore. */
|
|
37
|
+
export declare function pressureMessage(band: PressureBand, fill: number): string;
|
|
38
|
+
/** The static line the loop adds to the system prompt once per run. Empty when
|
|
39
|
+
* the host did not resolve a window -- an invented number is worse than none. */
|
|
40
|
+
export declare function contextWindowLine(contextWindow: number | undefined): string;
|
|
41
|
+
/** True for a note this module appended. */
|
|
42
|
+
export declare function isContextNote(m: ChatCompletionMessageParam): boolean;
|
|
43
|
+
/** The history message carrying a pressure notice. */
|
|
44
|
+
export declare function contextNote(band: PressureBand, fill: number): ChatCompletionMessageParam;
|
|
45
|
+
//# sourceMappingURL=contextPressure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contextPressure.d.ts","sourceRoot":"","sources":["../../core/contextPressure.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAEpF;+DAC+D;AAC/D,eAAO,MAAM,cAAc,eAAe,CAAC;AAE3C;;;;;+DAK+D;AAC/D,eAAO,MAAM,cAAc,0BAA2B,CAAC;AACvD,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D;;iFAEiF;AACjF,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAAG,OAAO,EAAE,MAAM,CAAA;CAAE;AAEjD,eAAO,MAAM,mBAAmB,EAAE,YAA6B,CAAC;AAQhE;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG;IAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,CAOpH;AAED;;qCAEqC;AACrC,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAUxE;AAED;iFACiF;AACjF,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAG3E;AAED,4CAA4C;AAC5C,wBAAgB,aAAa,CAAC,CAAC,EAAE,0BAA0B,GAAG,OAAO,CAEpE;AAED,sDAAsD;AACtD,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,0BAA0B,CAExF"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Marks a note this module appended. Same shape as CHECKPOINT_MARKER/
|
|
2
|
+
* ANCHOR_MARKER so the compactor can recognise and drop it. */
|
|
3
|
+
export const CONTEXT_MARKER = '[context] ';
|
|
4
|
+
/** Fill ratios worth interrupting for, ascending.
|
|
5
|
+
*
|
|
6
|
+
* 0.6 is where behavior should start bending (read narrower, summarize).
|
|
7
|
+
* 0.8 is where new exploration should stop. 0.9 is the last useful warning --
|
|
8
|
+
* COMPACT_AT_FILL is 0.86, so by 0.9 either compaction is not configured or it
|
|
9
|
+
* ran and did not get far enough, and both mean answer now. */
|
|
10
|
+
export const PRESSURE_BANDS = [0.6, 0.8, 0.9];
|
|
11
|
+
/** How far fill must fall BELOW a band before that band counts as un-crossed
|
|
12
|
+
* and may announce again. Without it, a run hovering at 0.79/0.81 re-announces
|
|
13
|
+
* on every other turn -- the notice becomes noise and costs tokens each time. */
|
|
14
|
+
export const PRESSURE_HYSTERESIS = 0.05;
|
|
15
|
+
export const initialPressureGate = { crossed: 0 };
|
|
16
|
+
function highestBandAtOrBelow(fill, slack = 0) {
|
|
17
|
+
let best = 0;
|
|
18
|
+
for (const b of PRESSURE_BANDS)
|
|
19
|
+
if (fill >= b - slack)
|
|
20
|
+
best = b;
|
|
21
|
+
return best;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decide whether this turn should announce context pressure.
|
|
25
|
+
*
|
|
26
|
+
* `band` is non-null at most once per crossing. After a compaction drains the
|
|
27
|
+
* window, the gate falls back to whatever band the new fill sits in, so
|
|
28
|
+
* climbing into 80% a second time is news a second time -- which is right: it
|
|
29
|
+
* is a different 80%, reached after the history was already collapsed once.
|
|
30
|
+
*/
|
|
31
|
+
export function pressureDecision(fill, gate) {
|
|
32
|
+
const current = highestBandAtOrBelow(fill);
|
|
33
|
+
if (current > gate.crossed)
|
|
34
|
+
return { band: current, next: { crossed: current } };
|
|
35
|
+
// Not a new crossing. Hold the gate where it is unless fill has fallen a
|
|
36
|
+
// clear margin below it, in which case drop to the band it really sits in.
|
|
37
|
+
const held = highestBandAtOrBelow(fill, PRESSURE_HYSTERESIS);
|
|
38
|
+
return { band: null, next: { crossed: Math.min(gate.crossed, held) } };
|
|
39
|
+
}
|
|
40
|
+
/** What the agent is told at each band. Each one names a CHANGE IN BEHAVIOR,
|
|
41
|
+
* not just a number -- a percentage with no instruction attached is something
|
|
42
|
+
* to acknowledge and then ignore. */
|
|
43
|
+
export function pressureMessage(band, fill) {
|
|
44
|
+
const pct = Math.round(fill * 100);
|
|
45
|
+
switch (band) {
|
|
46
|
+
case 0.6:
|
|
47
|
+
return `${CONTEXT_MARKER}Context is ${pct}% full. Read files in ranges rather than whole, summarize tool output rather than quoting it back, and stop re-reading things already in this conversation.`;
|
|
48
|
+
case 0.8:
|
|
49
|
+
return `${CONTEXT_MARKER}Context is ${pct}% full. Finish the task in progress and report it; start no new exploration, and open nothing large. If the work does not fit, say what is left rather than beginning it.`;
|
|
50
|
+
case 0.9:
|
|
51
|
+
return `${CONTEXT_MARKER}Context is ${pct}% full. Stop gathering and answer now with what you already have, naming what you did not get to. Further reading will be lost.`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The static line the loop adds to the system prompt once per run. Empty when
|
|
55
|
+
* the host did not resolve a window -- an invented number is worse than none. */
|
|
56
|
+
export function contextWindowLine(contextWindow) {
|
|
57
|
+
if (!contextWindow || contextWindow <= 0)
|
|
58
|
+
return '';
|
|
59
|
+
return `Your context window is ${contextWindow.toLocaleString('en-US')} tokens. You will be told when it is filling up.`;
|
|
60
|
+
}
|
|
61
|
+
/** True for a note this module appended. */
|
|
62
|
+
export function isContextNote(m) {
|
|
63
|
+
return m.role === 'assistant' && typeof m.content === 'string' && m.content.startsWith(CONTEXT_MARKER);
|
|
64
|
+
}
|
|
65
|
+
/** The history message carrying a pressure notice. */
|
|
66
|
+
export function contextNote(band, fill) {
|
|
67
|
+
return { role: 'assistant', content: pressureMessage(band, fill) };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=contextPressure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contextPressure.js","sourceRoot":"","sources":["../../core/contextPressure.ts"],"names":[],"mappings":"AAgCA;+DAC+D;AAC/D,MAAM,CAAC,MAAM,cAAc,GAAG,YAAY,CAAC;AAE3C;;;;;+DAK+D;AAC/D,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAC;AAGvD;;iFAEiF;AACjF,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAKxC,MAAM,CAAC,MAAM,mBAAmB,GAAiB,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AAEhE,SAAS,oBAAoB,CAAC,IAAY,EAAE,KAAK,GAAG,CAAC;IACnD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,cAAc;QAAE,IAAI,IAAI,IAAI,CAAC,GAAG,KAAK;YAAE,IAAI,GAAG,CAAC,CAAC;IAChE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,IAAkB;IAC/D,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,OAAuB,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;IACjG,yEAAyE;IACzE,2EAA2E;IAC3E,MAAM,IAAI,GAAG,oBAAoB,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;IAC7D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;AACzE,CAAC;AAED;;qCAEqC;AACrC,MAAM,UAAU,eAAe,CAAC,IAAkB,EAAE,IAAY;IAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;IACnC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,GAAG;YACN,OAAO,GAAG,cAAc,cAAc,GAAG,6JAA6J,CAAC;QACzM,KAAK,GAAG;YACN,OAAO,GAAG,cAAc,cAAc,GAAG,2KAA2K,CAAC;QACvN,KAAK,GAAG;YACN,OAAO,GAAG,cAAc,cAAc,GAAG,iIAAiI,CAAC;IAC/K,CAAC;AACH,CAAC;AAED;iFACiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,aAAiC;IACjE,IAAI,CAAC,aAAa,IAAI,aAAa,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACpD,OAAO,0BAA0B,aAAa,CAAC,cAAc,CAAC,OAAO,CAAC,kDAAkD,CAAC;AAC3H,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,aAAa,CAAC,CAA6B;IACzD,OAAO,CAAC,CAAC,IAAI,KAAK,WAAW,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AACzG,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,WAAW,CAAC,IAAkB,EAAE,IAAY;IAC1D,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;AACrE,CAAC"}
|
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;AAMhH,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,CAykB1E"}
|