@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.
Files changed (67) hide show
  1. package/dist/conversation.d.ts +15 -0
  2. package/dist/conversation.d.ts.map +1 -1
  3. package/dist/conversation.js +13 -1
  4. package/dist/conversation.js.map +1 -1
  5. package/dist/core/compactHistory.d.ts +84 -0
  6. package/dist/core/compactHistory.d.ts.map +1 -0
  7. package/dist/core/compactHistory.js +206 -0
  8. package/dist/core/compactHistory.js.map +1 -0
  9. package/dist/core/contextPressure.d.ts +45 -0
  10. package/dist/core/contextPressure.d.ts.map +1 -0
  11. package/dist/core/contextPressure.js +69 -0
  12. package/dist/core/contextPressure.js.map +1 -0
  13. package/dist/core/loop.d.ts +25 -0
  14. package/dist/core/loop.d.ts.map +1 -1
  15. package/dist/core/loop.js +176 -13
  16. package/dist/core/loop.js.map +1 -1
  17. package/dist/core/pricing.d.ts +30 -1
  18. package/dist/core/pricing.d.ts.map +1 -1
  19. package/dist/core/pricing.js +50 -21
  20. package/dist/core/pricing.js.map +1 -1
  21. package/dist/core/summarize.d.ts +10 -0
  22. package/dist/core/summarize.d.ts.map +1 -0
  23. package/dist/core/summarize.js +66 -0
  24. package/dist/core/summarize.js.map +1 -0
  25. package/dist/guardrail/builtins.d.ts.map +1 -1
  26. package/dist/guardrail/builtins.js +9 -3
  27. package/dist/guardrail/builtins.js.map +1 -1
  28. package/dist/guardrail/validate.d.ts.map +1 -1
  29. package/dist/guardrail/validate.js +6 -2
  30. package/dist/guardrail/validate.js.map +1 -1
  31. package/dist/prompts/host.d.ts +23 -0
  32. package/dist/prompts/host.d.ts.map +1 -1
  33. package/dist/prompts/host.js.map +1 -1
  34. package/dist/prompts/index.d.ts +14 -0
  35. package/dist/prompts/index.d.ts.map +1 -1
  36. package/dist/prompts/index.js +43 -2
  37. package/dist/prompts/index.js.map +1 -1
  38. package/dist/prompts/library/defaults.generated.js +5 -5
  39. package/dist/prompts/library/defaults.generated.js.map +1 -1
  40. package/dist/providers/contextWindow.d.ts +23 -4
  41. package/dist/providers/contextWindow.d.ts.map +1 -1
  42. package/dist/providers/contextWindow.js +25 -0
  43. package/dist/providers/contextWindow.js.map +1 -1
  44. package/dist/sessions/checkpointStore.d.ts +12 -0
  45. package/dist/sessions/checkpointStore.d.ts.map +1 -0
  46. package/dist/sessions/checkpointStore.js +48 -0
  47. package/dist/sessions/checkpointStore.js.map +1 -0
  48. package/dist/sessions/fileSessionStore.d.ts +4 -0
  49. package/dist/sessions/fileSessionStore.d.ts.map +1 -1
  50. package/dist/sessions/fileSessionStore.js +28 -2
  51. package/dist/sessions/fileSessionStore.js.map +1 -1
  52. package/dist/skillSource.d.ts +36 -0
  53. package/dist/skillSource.d.ts.map +1 -0
  54. package/dist/skillSource.js +105 -0
  55. package/dist/skillSource.js.map +1 -0
  56. package/dist/skills.d.ts +38 -2
  57. package/dist/skills.d.ts.map +1 -1
  58. package/dist/skills.js +64 -9
  59. package/dist/skills.js.map +1 -1
  60. package/dist/tools/builtins.d.ts.map +1 -1
  61. package/dist/tools/builtins.js +50 -2
  62. package/dist/tools/builtins.js.map +1 -1
  63. package/dist/tools/project.d.ts +11 -0
  64. package/dist/tools/project.d.ts.map +1 -0
  65. package/dist/tools/project.js +133 -0
  66. package/dist/tools/project.js.map +1 -0
  67. package/package.json +2 -2
@@ -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;AAEpF,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;CACnB"}
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"}
@@ -1,2 +1,14 @@
1
- export {};
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
@@ -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"}
@@ -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
  *
@@ -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;AAIhH,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,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;CACpB;AAED;;;;;;;;;;;gEAWgE;AAChE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAwa1E"}
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"}