@graphorin/agent 0.6.0 → 0.7.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 (110) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +31 -11
  3. package/dist/errors/index.d.ts +17 -4
  4. package/dist/errors/index.d.ts.map +1 -1
  5. package/dist/errors/index.js +19 -3
  6. package/dist/errors/index.js.map +1 -1
  7. package/dist/factory.d.ts.map +1 -1
  8. package/dist/factory.js +153 -1930
  9. package/dist/factory.js.map +1 -1
  10. package/dist/fanout/index.d.ts +13 -1
  11. package/dist/fanout/index.d.ts.map +1 -1
  12. package/dist/fanout/index.js +13 -4
  13. package/dist/fanout/index.js.map +1 -1
  14. package/dist/index.d.ts +7 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +11 -9
  17. package/dist/index.js.map +1 -1
  18. package/dist/lateral-leak/index.js +1 -1
  19. package/dist/package.js +6 -0
  20. package/dist/package.js.map +1 -0
  21. package/dist/run-state/index.d.ts +32 -6
  22. package/dist/run-state/index.d.ts.map +1 -1
  23. package/dist/run-state/index.js +46 -22
  24. package/dist/run-state/index.js.map +1 -1
  25. package/dist/runtime/agent-surface.js +122 -0
  26. package/dist/runtime/agent-surface.js.map +1 -0
  27. package/dist/runtime/agent-to-tool.d.ts +51 -0
  28. package/dist/runtime/agent-to-tool.d.ts.map +1 -0
  29. package/dist/runtime/agent-to-tool.js +145 -0
  30. package/dist/runtime/agent-to-tool.js.map +1 -0
  31. package/dist/runtime/approvals.js +0 -0
  32. package/dist/runtime/approvals.js.map +1 -0
  33. package/dist/runtime/dispatch.js +108 -0
  34. package/dist/runtime/dispatch.js.map +1 -0
  35. package/dist/runtime/executor-wiring.js +128 -0
  36. package/dist/runtime/executor-wiring.js.map +1 -0
  37. package/dist/runtime/fallback-chain.js +139 -0
  38. package/dist/runtime/fallback-chain.js.map +1 -0
  39. package/dist/runtime/handoff.js +307 -0
  40. package/dist/runtime/handoff.js.map +1 -0
  41. package/dist/runtime/messages.d.ts +22 -0
  42. package/dist/runtime/messages.d.ts.map +1 -0
  43. package/dist/runtime/messages.js +204 -0
  44. package/dist/runtime/messages.js.map +1 -0
  45. package/dist/runtime/provider-events.js +117 -0
  46. package/dist/runtime/provider-events.js.map +1 -0
  47. package/dist/runtime/run-compaction.js +210 -0
  48. package/dist/runtime/run-compaction.js.map +1 -0
  49. package/dist/runtime/run-finish.js +48 -0
  50. package/dist/runtime/run-finish.js.map +1 -0
  51. package/dist/runtime/run-gates.js +336 -0
  52. package/dist/runtime/run-gates.js.map +1 -0
  53. package/dist/runtime/run-init.js +81 -0
  54. package/dist/runtime/run-init.js.map +1 -0
  55. package/dist/runtime/run-input.js +46 -0
  56. package/dist/runtime/run-input.js.map +1 -0
  57. package/dist/runtime/step-catalogue.js +173 -0
  58. package/dist/runtime/step-catalogue.js.map +1 -0
  59. package/dist/runtime/tool-call-walk.js +189 -0
  60. package/dist/runtime/tool-call-walk.js.map +1 -0
  61. package/dist/runtime/tool-wiring.js +159 -0
  62. package/dist/runtime/tool-wiring.js.map +1 -0
  63. package/dist/tooling/adapters.js +1 -1
  64. package/dist/tooling/dataflow.js +1 -1
  65. package/dist/tooling/policy.js +2 -0
  66. package/dist/tooling/policy.js.map +1 -1
  67. package/dist/types.d.ts +63 -13
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +20 -20
  70. package/src/errors/index.ts +320 -0
  71. package/src/evaluator-optimizer/index.ts +212 -0
  72. package/src/factory.ts +957 -0
  73. package/src/fallback/index.ts +108 -0
  74. package/src/fanout/index.ts +523 -0
  75. package/src/filters/index.ts +347 -0
  76. package/src/index.ts +180 -0
  77. package/src/internal/ids.ts +46 -0
  78. package/src/internal/usage-accumulator.ts +90 -0
  79. package/src/lateral-leak/causality-monitor.ts +221 -0
  80. package/src/lateral-leak/index.ts +35 -0
  81. package/src/lateral-leak/merge-guard.ts +151 -0
  82. package/src/lateral-leak/protocol-guard.ts +222 -0
  83. package/src/preferred-model/index.ts +210 -0
  84. package/src/progress/index.ts +238 -0
  85. package/src/run-state/index.ts +607 -0
  86. package/src/runtime/agent-surface.ts +218 -0
  87. package/src/runtime/agent-to-tool.ts +323 -0
  88. package/src/runtime/approvals.ts +0 -0
  89. package/src/runtime/dispatch.ts +183 -0
  90. package/src/runtime/executor-wiring.ts +331 -0
  91. package/src/runtime/fallback-chain.ts +250 -0
  92. package/src/runtime/handoff.ts +428 -0
  93. package/src/runtime/messages.ts +309 -0
  94. package/src/runtime/provider-events.ts +175 -0
  95. package/src/runtime/run-compaction.ts +288 -0
  96. package/src/runtime/run-finish.ts +93 -0
  97. package/src/runtime/run-gates.ts +419 -0
  98. package/src/runtime/run-init.ts +169 -0
  99. package/src/runtime/run-input.ts +102 -0
  100. package/src/runtime/step-catalogue.ts +338 -0
  101. package/src/runtime/tool-call-walk.ts +301 -0
  102. package/src/runtime/tool-wiring.ts +218 -0
  103. package/src/testing/replay-provider.ts +121 -0
  104. package/src/tooling/adapters.ts +403 -0
  105. package/src/tooling/catalogue.ts +36 -0
  106. package/src/tooling/dataflow.ts +171 -0
  107. package/src/tooling/plan.ts +123 -0
  108. package/src/tooling/policy.ts +67 -0
  109. package/src/tooling/registry-build.ts +191 -0
  110. package/src/types.ts +696 -0
@@ -0,0 +1,117 @@
1
+ //#region src/runtime/provider-events.ts
2
+ /**
3
+ * AG-21: classify a **thrown** provider error into a {@link ProviderErrorKind}
4
+ * so the fallback chain can act on it, instead of flattening every exception to
5
+ * `'unknown'` (which is always fallback-ineligible). Structural - reads the
6
+ * `kind` carried by `@graphorin/provider`'s `GraphorinProviderError` subclasses
7
+ * without importing them, keeping the agent decoupled from the provider package.
8
+ */
9
+ /** Canonical `ProviderErrorKind` values honoured off a thrown error's `errorKind`. */
10
+ const CANONICAL_PROVIDER_ERROR_KINDS = new Set([
11
+ "rate-limit",
12
+ "capacity",
13
+ "context-length",
14
+ "transient",
15
+ "invalid-request",
16
+ "unauthorized",
17
+ "content-filter"
18
+ ]);
19
+ function classifyThrownProviderErrorKind(cause) {
20
+ if (typeof cause === "object" && cause !== null) {
21
+ const errorKind = cause.errorKind;
22
+ if (typeof errorKind === "string" && CANONICAL_PROVIDER_ERROR_KINDS.has(errorKind)) return errorKind;
23
+ switch (cause.kind) {
24
+ case "rate-limit-exceeded": return "rate-limit";
25
+ }
26
+ }
27
+ return "unknown";
28
+ }
29
+ function specToProvider(spec) {
30
+ if ("provider" in spec) return spec.provider;
31
+ return spec;
32
+ }
33
+ function handleProviderEvent(ev, state) {
34
+ switch (ev.type) {
35
+ case "stream-start": return {};
36
+ case "reasoning-delta":
37
+ state.reasoningBuffer += ev.delta;
38
+ return { emit: {
39
+ type: "reasoning.delta",
40
+ delta: ev.delta
41
+ } };
42
+ case "reasoning-end": {
43
+ const text = state.reasoningBuffer;
44
+ state.reasoningBuffer = "";
45
+ state.reasoningParts.push({
46
+ type: "reasoning",
47
+ text,
48
+ ...ev.meta !== void 0 ? { meta: ev.meta } : {}
49
+ });
50
+ return {};
51
+ }
52
+ case "text-delta":
53
+ state.textBuffer += ev.delta;
54
+ return { emit: {
55
+ type: "text.delta",
56
+ delta: ev.delta
57
+ } };
58
+ case "tool-call-start":
59
+ state.calls.set(ev.toolCallId, {
60
+ toolCallId: ev.toolCallId,
61
+ toolName: ev.toolName,
62
+ argsBuffer: ""
63
+ });
64
+ return { emit: {
65
+ type: "tool.call.start",
66
+ toolCallId: ev.toolCallId,
67
+ toolName: ev.toolName,
68
+ args: void 0
69
+ } };
70
+ case "tool-call-input-delta": {
71
+ const acc = state.calls.get(ev.toolCallId);
72
+ if (acc !== void 0) acc.argsBuffer += ev.argsDelta;
73
+ return { emit: {
74
+ type: "tool.call.delta",
75
+ toolCallId: ev.toolCallId,
76
+ argsDelta: ev.argsDelta
77
+ } };
78
+ }
79
+ case "tool-call-end": {
80
+ const acc = state.calls.get(ev.toolCallId);
81
+ if (acc === void 0) {
82
+ process.stderr.write(`[graphorin/agent] dropped tool-call-end '${ev.toolCallId}' with no matching tool-call-start.\n`);
83
+ return {};
84
+ }
85
+ state.finalCalls.push({
86
+ toolCallId: ev.toolCallId,
87
+ toolName: acc.toolName,
88
+ args: ev.finalArgs
89
+ });
90
+ return { emit: {
91
+ type: "tool.call.end",
92
+ toolCallId: ev.toolCallId,
93
+ finalArgs: ev.finalArgs
94
+ } };
95
+ }
96
+ case "file": return { emit: {
97
+ type: "file.generated",
98
+ mimeType: ev.mimeType,
99
+ data: ev.data
100
+ } };
101
+ case "source": return { emit: {
102
+ type: "source.cited",
103
+ uri: ev.uri,
104
+ ...ev.title !== void 0 ? { title: ev.title } : {}
105
+ } };
106
+ case "finish": return {
107
+ usage: ev.usage,
108
+ finished: true
109
+ };
110
+ case "error": return { providerError: ev.error };
111
+ default: return {};
112
+ }
113
+ }
114
+
115
+ //#endregion
116
+ export { classifyThrownProviderErrorKind, handleProviderEvent, specToProvider };
117
+ //# sourceMappingURL=provider-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-events.js","names":["CANONICAL_PROVIDER_ERROR_KINDS: ReadonlySet<string>"],"sources":["../../src/runtime/provider-events.ts"],"sourcesContent":["/**\n * Provider stream bridging for the agent runtime: the per-step\n * accumulation of `ProviderEvent`s into agent-visible events / final\n * tool calls, thrown-provider-error classification for the fallback\n * chain, and the `ModelSpec` -> `Provider` projection. Extracted\n * verbatim from `factory.ts` (issue #23).\n *\n * @packageDocumentation\n */\n\nimport type {\n AgentEvent,\n ModelSpec,\n Provider,\n ProviderError,\n ProviderErrorKind,\n ProviderEvent,\n ReasoningContent,\n ToolCall,\n Usage,\n} from '@graphorin/core';\n\n/**\n * AG-21: classify a **thrown** provider error into a {@link ProviderErrorKind}\n * so the fallback chain can act on it, instead of flattening every exception to\n * `'unknown'` (which is always fallback-ineligible). Structural - reads the\n * `kind` carried by `@graphorin/provider`'s `GraphorinProviderError` subclasses\n * without importing them, keeping the agent decoupled from the provider package.\n */\n/** Canonical `ProviderErrorKind` values honoured off a thrown error's `errorKind`. */\nconst CANONICAL_PROVIDER_ERROR_KINDS: ReadonlySet<string> = new Set([\n 'rate-limit',\n 'capacity',\n 'context-length',\n 'transient',\n 'invalid-request',\n 'unauthorized',\n 'content-filter',\n]);\n\nexport function classifyThrownProviderErrorKind(cause: unknown): ProviderErrorKind {\n if (typeof cause === 'object' && cause !== null) {\n // B1: `ProviderHttpError` carries the canonical mapped kind on\n // `errorKind` (its `kind` stays the stable 'provider-http'\n // discriminant) - honour it so a thrown 429 / context overflow is\n // classified like the structured-event equivalent.\n const errorKind = (cause as { readonly errorKind?: unknown }).errorKind;\n if (typeof errorKind === 'string' && CANONICAL_PROVIDER_ERROR_KINDS.has(errorKind)) {\n return errorKind as ProviderErrorKind;\n }\n switch ((cause as { readonly kind?: unknown }).kind) {\n case 'rate-limit-exceeded':\n return 'rate-limit';\n }\n }\n return 'unknown';\n}\n\nexport function specToProvider(spec: ModelSpec): Provider {\n if ('provider' in spec) return spec.provider as Provider;\n return spec as Provider;\n}\n\nexport interface ToolCallAccumulator {\n readonly toolCallId: string;\n toolName: string;\n argsBuffer: string;\n}\n\nexport interface ProviderEventOutcome {\n readonly emit?: AgentEvent;\n readonly providerError?: ProviderError;\n readonly usage?: Usage;\n readonly finished?: boolean;\n}\n\nexport interface ProviderEventCollector {\n textBuffer: string;\n reasoningBuffer: string;\n reasoningParts: ReasoningContent[];\n calls: Map<string, ToolCallAccumulator>;\n finalCalls: ToolCall[];\n}\n\nexport function handleProviderEvent(\n ev: ProviderEvent,\n state: ProviderEventCollector,\n): ProviderEventOutcome {\n switch (ev.type) {\n case 'stream-start':\n return {};\n case 'reasoning-delta':\n state.reasoningBuffer += ev.delta;\n return { emit: { type: 'reasoning.delta', delta: ev.delta } };\n case 'reasoning-end': {\n // W-024: close the current reasoning block - flush the buffered\n // deltas into a per-block ReasoningContent carrying the adapter's\n // opaque round-trip meta (Anthropic thinking signature / redacted\n // data). A redacted block has no deltas: its part is meta-only\n // with empty text, which the retention pipeline still round-trips.\n const text = state.reasoningBuffer;\n state.reasoningBuffer = '';\n state.reasoningParts.push({\n type: 'reasoning',\n text,\n ...(ev.meta !== undefined ? { meta: ev.meta } : {}),\n });\n return {};\n }\n case 'text-delta':\n state.textBuffer += ev.delta;\n return { emit: { type: 'text.delta', delta: ev.delta } };\n case 'tool-call-start':\n state.calls.set(ev.toolCallId, {\n toolCallId: ev.toolCallId,\n toolName: ev.toolName,\n argsBuffer: '',\n });\n return {\n emit: {\n type: 'tool.call.start',\n toolCallId: ev.toolCallId,\n toolName: ev.toolName,\n args: undefined,\n },\n };\n case 'tool-call-input-delta': {\n const acc = state.calls.get(ev.toolCallId);\n if (acc !== undefined) acc.argsBuffer += ev.argsDelta;\n return {\n emit: { type: 'tool.call.delta', toolCallId: ev.toolCallId, argsDelta: ev.argsDelta },\n };\n }\n case 'tool-call-end': {\n const acc = state.calls.get(ev.toolCallId);\n if (acc === undefined) {\n // AG-26: an end without a matching start has no tool name - the\n // old path dispatched it as the unknown tool ''. Drop it loudly.\n process.stderr.write(\n `[graphorin/agent] dropped tool-call-end '${ev.toolCallId}' with no matching tool-call-start.\\n`,\n );\n return {};\n }\n state.finalCalls.push({\n toolCallId: ev.toolCallId,\n toolName: acc.toolName,\n args: ev.finalArgs,\n });\n return {\n emit: { type: 'tool.call.end', toolCallId: ev.toolCallId, finalArgs: ev.finalArgs },\n };\n }\n // AG-26: provider-generated files / citations are consumer-observable\n // events instead of silently vanishing.\n case 'file':\n return { emit: { type: 'file.generated', mimeType: ev.mimeType, data: ev.data } };\n case 'source':\n return {\n emit: {\n type: 'source.cited',\n uri: ev.uri,\n ...(ev.title !== undefined ? { title: ev.title } : {}),\n },\n };\n case 'finish':\n return { usage: ev.usage, finished: true };\n case 'error':\n return { providerError: ev.error };\n default: {\n const _exhaustive: never = ev;\n void _exhaustive;\n return {};\n }\n }\n}\n"],"mappings":";;;;;;;;;AA8BA,MAAMA,iCAAsD,IAAI,IAAI;CAClE;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAgB,gCAAgC,OAAmC;AACjF,KAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EAK/C,MAAM,YAAa,MAA2C;AAC9D,MAAI,OAAO,cAAc,YAAY,+BAA+B,IAAI,UAAU,CAChF,QAAO;AAET,UAAS,MAAsC,MAA/C;GACE,KAAK,sBACH,QAAO;;;AAGb,QAAO;;AAGT,SAAgB,eAAe,MAA2B;AACxD,KAAI,cAAc,KAAM,QAAO,KAAK;AACpC,QAAO;;AAwBT,SAAgB,oBACd,IACA,OACsB;AACtB,SAAQ,GAAG,MAAX;EACE,KAAK,eACH,QAAO,EAAE;EACX,KAAK;AACH,SAAM,mBAAmB,GAAG;AAC5B,UAAO,EAAE,MAAM;IAAE,MAAM;IAAmB,OAAO,GAAG;IAAO,EAAE;EAC/D,KAAK,iBAAiB;GAMpB,MAAM,OAAO,MAAM;AACnB,SAAM,kBAAkB;AACxB,SAAM,eAAe,KAAK;IACxB,MAAM;IACN;IACA,GAAI,GAAG,SAAS,SAAY,EAAE,MAAM,GAAG,MAAM,GAAG,EAAE;IACnD,CAAC;AACF,UAAO,EAAE;;EAEX,KAAK;AACH,SAAM,cAAc,GAAG;AACvB,UAAO,EAAE,MAAM;IAAE,MAAM;IAAc,OAAO,GAAG;IAAO,EAAE;EAC1D,KAAK;AACH,SAAM,MAAM,IAAI,GAAG,YAAY;IAC7B,YAAY,GAAG;IACf,UAAU,GAAG;IACb,YAAY;IACb,CAAC;AACF,UAAO,EACL,MAAM;IACJ,MAAM;IACN,YAAY,GAAG;IACf,UAAU,GAAG;IACb,MAAM;IACP,EACF;EACH,KAAK,yBAAyB;GAC5B,MAAM,MAAM,MAAM,MAAM,IAAI,GAAG,WAAW;AAC1C,OAAI,QAAQ,OAAW,KAAI,cAAc,GAAG;AAC5C,UAAO,EACL,MAAM;IAAE,MAAM;IAAmB,YAAY,GAAG;IAAY,WAAW,GAAG;IAAW,EACtF;;EAEH,KAAK,iBAAiB;GACpB,MAAM,MAAM,MAAM,MAAM,IAAI,GAAG,WAAW;AAC1C,OAAI,QAAQ,QAAW;AAGrB,YAAQ,OAAO,MACb,4CAA4C,GAAG,WAAW,uCAC3D;AACD,WAAO,EAAE;;AAEX,SAAM,WAAW,KAAK;IACpB,YAAY,GAAG;IACf,UAAU,IAAI;IACd,MAAM,GAAG;IACV,CAAC;AACF,UAAO,EACL,MAAM;IAAE,MAAM;IAAiB,YAAY,GAAG;IAAY,WAAW,GAAG;IAAW,EACpF;;EAIH,KAAK,OACH,QAAO,EAAE,MAAM;GAAE,MAAM;GAAkB,UAAU,GAAG;GAAU,MAAM,GAAG;GAAM,EAAE;EACnF,KAAK,SACH,QAAO,EACL,MAAM;GACJ,MAAM;GACN,KAAK,GAAG;GACR,GAAI,GAAG,UAAU,SAAY,EAAE,OAAO,GAAG,OAAO,GAAG,EAAE;GACtD,EACF;EACH,KAAK,SACH,QAAO;GAAE,OAAO,GAAG;GAAO,UAAU;GAAM;EAC5C,KAAK,QACH,QAAO,EAAE,eAAe,GAAG,OAAO;EACpC,QAGE,QAAO,EAAE"}
@@ -0,0 +1,210 @@
1
+ //#region src/runtime/run-compaction.ts
2
+ const noopCompactionResult = (skippedReason) => ({
3
+ beforeTokens: 0,
4
+ afterTokens: 0,
5
+ summaryTokens: 0,
6
+ durationMs: 0,
7
+ hooksFiredCount: 0,
8
+ summary: "",
9
+ applied: false,
10
+ skippedReason
11
+ });
12
+ /**
13
+ * Auto-compaction trigger (WI-09 / P1-1). Before assembling each
14
+ * provider request, ask the memory {@link ContextEngine} whether the
15
+ * in-flight buffer has crossed its per-provider threshold
16
+ * (`shouldCompact`); when it has, summarise the older turns
17
+ * (`compactNow`, `source: 'auto-trigger'`), splice the result back in
18
+ * - preserving the byte-stable system prefix and the most-recent
19
+ * turns verbatim - and emit `context.compacted`. The compaction is
20
+ * configured on the memory facade (`createMemory({ contextEngine })`,
21
+ * RB-46); there is no parallel agent-level knob.
22
+ *
23
+ * No-op when no memory is wired, when compaction is disabled or below
24
+ * threshold (the engine returns `false`), or for `secret`-tier runs
25
+ * (secret history is never shipped to the summarizer - a less-trusted
26
+ * external sink; per-result handle references land in WI-10). Best
27
+ * effort: a misconfigured engine (e.g. no summarizer) is swallowed and
28
+ * the run proceeds uncompacted rather than aborting mid-flight.
29
+ *
30
+ * Operator-requested compactions (`agent.compact()`, CE-3/AG-13) are
31
+ * serviced here too, FIRST - the queue carries the `compact()` promise
32
+ * resolvers, and manual requests bypass the trigger evaluation.
33
+ */
34
+ async function* maybeAutoCompact(env) {
35
+ const { config, memory, state, messages, sessionId, agentId, systemPrefixLength } = env;
36
+ const { pendingManualCompacts } = env;
37
+ while (pendingManualCompacts.length > 0) {
38
+ const pending = pendingManualCompacts.shift();
39
+ if (pending !== void 0) yield* serviceManualCompact(env, pending);
40
+ }
41
+ const mem = memory;
42
+ if (mem === void 0) return;
43
+ if (config.sensitivity === "secret") return;
44
+ const engine = mem.contextEngine;
45
+ if (!await engine.shouldCompact(messages, { compactableFromIndex: systemPrefixLength }).catch(() => false)) return;
46
+ const startedAt = Date.now();
47
+ const envelope = await engine.compactNow({
48
+ scope: {
49
+ userId: state.userId ?? agentId,
50
+ sessionId,
51
+ agentId
52
+ },
53
+ runId: state.id,
54
+ sessionId,
55
+ agentId,
56
+ source: "auto-trigger",
57
+ messages: messages.slice(systemPrefixLength),
58
+ prefixMessages: messages.slice(0, systemPrefixLength),
59
+ memory: mem
60
+ }).catch(() => void 0);
61
+ if (envelope === void 0) return;
62
+ if (envelope.result.droppedMessageIndices.length === 0) return;
63
+ spliceCompacted(env, envelope);
64
+ yield {
65
+ type: "context.compacted",
66
+ runId: state.id,
67
+ sessionId,
68
+ agentId,
69
+ beforeTokens: envelope.result.beforeTokens,
70
+ afterTokens: envelope.result.afterTokens,
71
+ summaryTokens: envelope.result.summaryTokens,
72
+ durationMs: Date.now() - startedAt,
73
+ source: "auto-trigger",
74
+ hooksFiredCount: envelope.result.hooksFiredCount
75
+ };
76
+ }
77
+ /**
78
+ * context-engine-06: last-resort tier at hard context overflow. When
79
+ * a provider rejects the request as over-window, force ONE aggressive
80
+ * compaction (`preserveRecentTurns: 2`, trigger evaluation bypassed)
81
+ * and let the caller retry the same provider - the fallback chain's
82
+ * members usually share the same window, so without this the run just
83
+ * dies. Returns `true` when the buffer actually shrank (retry is
84
+ * worthwhile); `false` when memory is not wired, the run is
85
+ * secret-tier, compaction trimmed nothing, or the engine threw.
86
+ */
87
+ async function* tryEmergencyCompact(env) {
88
+ const { config, memory, state, messages, sessionId, agentId, systemPrefixLength } = env;
89
+ const mem = memory;
90
+ if (mem === void 0 || config.sensitivity === "secret") return false;
91
+ const startedAt = Date.now();
92
+ const envelope = await mem.contextEngine.compactNow({
93
+ scope: {
94
+ userId: state.userId ?? agentId,
95
+ sessionId,
96
+ agentId
97
+ },
98
+ runId: state.id,
99
+ sessionId,
100
+ agentId,
101
+ source: "auto-trigger",
102
+ messages: messages.slice(systemPrefixLength),
103
+ prefixMessages: messages.slice(0, systemPrefixLength),
104
+ memory: mem,
105
+ preserveRecentTurns: 2
106
+ }).catch(() => void 0);
107
+ if (envelope === void 0 || envelope.result.droppedMessageIndices.length === 0) return false;
108
+ spliceCompacted(env, envelope);
109
+ yield {
110
+ type: "context.compacted",
111
+ runId: state.id,
112
+ sessionId,
113
+ agentId,
114
+ beforeTokens: envelope.result.beforeTokens,
115
+ afterTokens: envelope.result.afterTokens,
116
+ summaryTokens: envelope.result.summaryTokens,
117
+ durationMs: Date.now() - startedAt,
118
+ source: "auto-trigger",
119
+ hooksFiredCount: envelope.result.hooksFiredCount
120
+ };
121
+ return true;
122
+ }
123
+ /**
124
+ * Prefix-pinned splice shared by the auto + manual compaction paths
125
+ * (CE-3): stable system prefix + [summary, ...recent turns], with the
126
+ * post-compaction hooks' text Context Essentials re-anchored as a
127
+ * trailing system message so they survive the trim (RB-46). Mutates
128
+ * BOTH the live loop buffer and `state.messages`.
129
+ */
130
+ function spliceCompacted(env, envelope) {
131
+ const { state, messages, systemPrefixLength } = env;
132
+ const rebuilt = [...messages.slice(0, systemPrefixLength), ...envelope.result.trimmedMessages];
133
+ const essentials = envelope.extraContent.map((part) => typeof part === "object" && part !== null && "text" in part ? String(part.text) : "").filter((text) => text.length > 0).join("\n\n");
134
+ if (essentials.length > 0) rebuilt.push({
135
+ role: "system",
136
+ content: essentials
137
+ });
138
+ messages.splice(0, messages.length, ...rebuilt);
139
+ state.messages.splice(0, state.messages.length, ...rebuilt);
140
+ }
141
+ /**
142
+ * Service one `agent.compact()` request inside the loop (CE-3/AG-13):
143
+ * same prefix-pinned splice as the auto path, `source: 'manual'` (or
144
+ * the caller's `'pre-step'`), `preserveRecentTurns` forwarded as a
145
+ * per-call strategy override. An engine failure rejects the caller's
146
+ * promise but never aborts the live run; a summarize that trims
147
+ * nothing resolves `applied: false` without an event.
148
+ */
149
+ async function* serviceManualCompact(env, pending) {
150
+ const { memory, state, messages, sessionId, agentId, systemPrefixLength } = env;
151
+ const mem = memory;
152
+ if (mem === void 0) {
153
+ pending.resolve(noopCompactionResult("no-memory"));
154
+ return;
155
+ }
156
+ const source = pending.options?.source ?? "manual";
157
+ const startedAt = Date.now();
158
+ let envelope;
159
+ try {
160
+ envelope = await mem.contextEngine.compactNow({
161
+ scope: {
162
+ userId: state.userId ?? agentId,
163
+ sessionId,
164
+ agentId
165
+ },
166
+ runId: state.id,
167
+ sessionId,
168
+ agentId,
169
+ source,
170
+ messages: messages.slice(systemPrefixLength),
171
+ prefixMessages: messages.slice(0, systemPrefixLength),
172
+ memory: mem,
173
+ ...pending.options?.preserveRecentTurns !== void 0 ? { preserveRecentTurns: pending.options.preserveRecentTurns } : {}
174
+ });
175
+ } catch (cause) {
176
+ pending.reject(cause);
177
+ return;
178
+ }
179
+ const { result } = envelope;
180
+ const applied = result.droppedMessageIndices.length > 0;
181
+ if (applied) {
182
+ spliceCompacted(env, envelope);
183
+ yield {
184
+ type: "context.compacted",
185
+ runId: state.id,
186
+ sessionId,
187
+ agentId,
188
+ beforeTokens: result.beforeTokens,
189
+ afterTokens: result.afterTokens,
190
+ summaryTokens: result.summaryTokens,
191
+ durationMs: Date.now() - startedAt,
192
+ source,
193
+ hooksFiredCount: result.hooksFiredCount
194
+ };
195
+ }
196
+ pending.resolve({
197
+ beforeTokens: result.beforeTokens,
198
+ afterTokens: result.afterTokens,
199
+ summaryTokens: result.summaryTokens,
200
+ durationMs: Date.now() - startedAt,
201
+ hooksFiredCount: result.hooksFiredCount,
202
+ summary: result.summary ?? "",
203
+ applied,
204
+ ...applied ? {} : { skippedReason: "nothing-to-trim" }
205
+ });
206
+ }
207
+
208
+ //#endregion
209
+ export { maybeAutoCompact, noopCompactionResult, tryEmergencyCompact };
210
+ //# sourceMappingURL=run-compaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-compaction.js","names":["rebuilt: Message[]","envelope: CompactionEnvelope"],"sources":["../../src/runtime/run-compaction.ts"],"sourcesContent":["/**\n * Compaction lifecycle for the agent run loop (WI-09 / P1-1, CE-3 /\n * AG-13, context-engine-06): the auto-compaction trigger evaluated\n * before each provider request, the last-resort emergency compaction at\n * hard context overflow, the servicing of `agent.compact()` manual\n * requests, and the prefix-pinned splice shared by all three paths.\n * Extracted verbatim from `factory.ts` (issue #23); the former run-loop\n * closures now take an explicit {@link CompactionRunEnv}.\n *\n * @packageDocumentation\n */\n\nimport type { AgentEvent, Message, RunState, Sensitivity } from '@graphorin/core';\nimport type { Memory } from '@graphorin/memory';\nimport type { CompactionApiResult, CompactOptions } from '../types.js';\nimport type { MutableRunState } from './run-input.js';\n\n/** Return envelope of `ContextEngine.compactNow` (shared splice paths, CE-3). */\nexport type CompactionEnvelope = Awaited<ReturnType<Memory['contextEngine']['compactNow']>>;\n\n/**\n * Manual-compaction requests enqueued by `agent.compact()` (CE-3 /\n * AG-13). The run loop owns the live message buffer, so the splice\n * must happen inside the loop: `maybeAutoCompact` services the queue\n * at the next step boundary, and `finishRun` settles leftovers as\n * explicit no-ops.\n */\nexport interface PendingManualCompact {\n readonly options: CompactOptions | undefined;\n readonly resolve: (result: CompactionApiResult) => void;\n readonly reject: (cause: unknown) => void;\n}\n\nexport const noopCompactionResult = (\n skippedReason: NonNullable<CompactionApiResult['skippedReason']>,\n): CompactionApiResult => ({\n beforeTokens: 0,\n afterTokens: 0,\n summaryTokens: 0,\n durationMs: 0,\n hooksFiredCount: 0,\n summary: '',\n applied: false,\n skippedReason,\n});\n\n/**\n * The run-scoped context the compaction paths operate on. Field names\n * mirror the run-loop locals the former closures captured; the live\n * message buffer and the pending-manual queue are shared references.\n */\nexport interface CompactionRunEnv {\n readonly config: { readonly sensitivity?: Sensitivity };\n readonly memory: Memory | undefined;\n readonly state: MutableRunState & RunState;\n readonly messages: Message[];\n readonly sessionId: string;\n readonly agentId: string;\n readonly systemPrefixLength: number;\n readonly pendingManualCompacts: PendingManualCompact[];\n}\n\n/**\n * Auto-compaction trigger (WI-09 / P1-1). Before assembling each\n * provider request, ask the memory {@link ContextEngine} whether the\n * in-flight buffer has crossed its per-provider threshold\n * (`shouldCompact`); when it has, summarise the older turns\n * (`compactNow`, `source: 'auto-trigger'`), splice the result back in\n * - preserving the byte-stable system prefix and the most-recent\n * turns verbatim - and emit `context.compacted`. The compaction is\n * configured on the memory facade (`createMemory({ contextEngine })`,\n * RB-46); there is no parallel agent-level knob.\n *\n * No-op when no memory is wired, when compaction is disabled or below\n * threshold (the engine returns `false`), or for `secret`-tier runs\n * (secret history is never shipped to the summarizer - a less-trusted\n * external sink; per-result handle references land in WI-10). Best\n * effort: a misconfigured engine (e.g. no summarizer) is swallowed and\n * the run proceeds uncompacted rather than aborting mid-flight.\n *\n * Operator-requested compactions (`agent.compact()`, CE-3/AG-13) are\n * serviced here too, FIRST - the queue carries the `compact()` promise\n * resolvers, and manual requests bypass the trigger evaluation.\n */\nexport async function* maybeAutoCompact<TOutput>(\n env: CompactionRunEnv,\n): AsyncGenerator<AgentEvent<TOutput>, void, void> {\n const { config, memory, state, messages, sessionId, agentId, systemPrefixLength } = env;\n const { pendingManualCompacts } = env;\n while (pendingManualCompacts.length > 0) {\n const pending = pendingManualCompacts.shift();\n if (pending !== undefined) yield* serviceManualCompact<TOutput>(env, pending);\n }\n const mem = memory;\n if (mem === undefined) return;\n // Sensitivity gate (WI-09 step 2): drop, never re-route, secret-tier\n // content. Auto-compaction is an LLM summarizer call, so a secret\n // run is left un-compacted here.\n if (config.sensitivity === 'secret') return;\n const engine = mem.contextEngine;\n // context-engine-04: trigger, reclaim floor, and anti-thrash guard\n // share one basis - the engine sees the full buffer for the trigger\n // total, learns where the pinned (uncompactable) prefix ends, and\n // receives the prefix messages so the guard arms against the FULL\n // post-splice context instead of the sliced body.\n const triggered = await engine\n .shouldCompact(messages, { compactableFromIndex: systemPrefixLength })\n .catch(() => false);\n if (!triggered) return;\n\n const startedAt = Date.now();\n const envelope = await engine\n .compactNow({\n scope: { userId: state.userId ?? agentId, sessionId, agentId },\n runId: state.id,\n sessionId,\n agentId,\n source: 'auto-trigger',\n messages: messages.slice(systemPrefixLength),\n prefixMessages: messages.slice(0, systemPrefixLength),\n memory: mem,\n })\n // No summarizer configured (or the strategy threw) - proceed with\n // the un-compacted buffer rather than failing a live run.\n .catch(() => undefined);\n if (envelope === undefined) return;\n // Nothing was old enough to trim (body ≤ preserve-recent) - skip the\n // splice + event so `context.compacted` only fires on real work.\n if (envelope.result.droppedMessageIndices.length === 0) return;\n\n spliceCompacted(env, envelope);\n yield {\n type: 'context.compacted',\n runId: state.id,\n sessionId,\n agentId,\n beforeTokens: envelope.result.beforeTokens,\n afterTokens: envelope.result.afterTokens,\n summaryTokens: envelope.result.summaryTokens,\n durationMs: Date.now() - startedAt,\n source: 'auto-trigger',\n hooksFiredCount: envelope.result.hooksFiredCount,\n };\n}\n\n/**\n * context-engine-06: last-resort tier at hard context overflow. When\n * a provider rejects the request as over-window, force ONE aggressive\n * compaction (`preserveRecentTurns: 2`, trigger evaluation bypassed)\n * and let the caller retry the same provider - the fallback chain's\n * members usually share the same window, so without this the run just\n * dies. Returns `true` when the buffer actually shrank (retry is\n * worthwhile); `false` when memory is not wired, the run is\n * secret-tier, compaction trimmed nothing, or the engine threw.\n */\nexport async function* tryEmergencyCompact<TOutput>(\n env: CompactionRunEnv,\n): AsyncGenerator<AgentEvent<TOutput>, boolean, void> {\n const { config, memory, state, messages, sessionId, agentId, systemPrefixLength } = env;\n const mem = memory;\n if (mem === undefined || config.sensitivity === 'secret') return false;\n const startedAt = Date.now();\n const envelope = await mem.contextEngine\n .compactNow({\n scope: { userId: state.userId ?? agentId, sessionId, agentId },\n runId: state.id,\n sessionId,\n agentId,\n source: 'auto-trigger',\n messages: messages.slice(systemPrefixLength),\n prefixMessages: messages.slice(0, systemPrefixLength),\n memory: mem,\n preserveRecentTurns: 2,\n })\n .catch(() => undefined);\n if (envelope === undefined || envelope.result.droppedMessageIndices.length === 0) {\n return false;\n }\n spliceCompacted(env, envelope);\n yield {\n type: 'context.compacted',\n runId: state.id,\n sessionId,\n agentId,\n beforeTokens: envelope.result.beforeTokens,\n afterTokens: envelope.result.afterTokens,\n summaryTokens: envelope.result.summaryTokens,\n durationMs: Date.now() - startedAt,\n source: 'auto-trigger',\n hooksFiredCount: envelope.result.hooksFiredCount,\n };\n return true;\n}\n\n/**\n * Prefix-pinned splice shared by the auto + manual compaction paths\n * (CE-3): stable system prefix + [summary, ...recent turns], with the\n * post-compaction hooks' text Context Essentials re-anchored as a\n * trailing system message so they survive the trim (RB-46). Mutates\n * BOTH the live loop buffer and `state.messages`.\n */\nfunction spliceCompacted(env: CompactionRunEnv, envelope: CompactionEnvelope): void {\n const { state, messages, systemPrefixLength } = env;\n const prefix = messages.slice(0, systemPrefixLength);\n const rebuilt: Message[] = [...prefix, ...envelope.result.trimmedMessages];\n const essentials = envelope.extraContent\n .map((part) =>\n typeof part === 'object' && part !== null && 'text' in part\n ? String((part as { readonly text: unknown }).text)\n : '',\n )\n .filter((text) => text.length > 0)\n .join('\\n\\n');\n if (essentials.length > 0) {\n rebuilt.push({ role: 'system', content: essentials });\n }\n messages.splice(0, messages.length, ...rebuilt);\n state.messages.splice(0, state.messages.length, ...rebuilt);\n}\n\n/**\n * Service one `agent.compact()` request inside the loop (CE-3/AG-13):\n * same prefix-pinned splice as the auto path, `source: 'manual'` (or\n * the caller's `'pre-step'`), `preserveRecentTurns` forwarded as a\n * per-call strategy override. An engine failure rejects the caller's\n * promise but never aborts the live run; a summarize that trims\n * nothing resolves `applied: false` without an event.\n */\nasync function* serviceManualCompact<TOutput>(\n env: CompactionRunEnv,\n pending: PendingManualCompact,\n): AsyncGenerator<AgentEvent<TOutput>, void, void> {\n const { memory, state, messages, sessionId, agentId, systemPrefixLength } = env;\n const mem = memory;\n if (mem === undefined) {\n pending.resolve(noopCompactionResult('no-memory'));\n return;\n }\n const source = pending.options?.source ?? 'manual';\n const startedAt = Date.now();\n let envelope: CompactionEnvelope;\n try {\n envelope = await mem.contextEngine.compactNow({\n scope: { userId: state.userId ?? agentId, sessionId, agentId },\n runId: state.id,\n sessionId,\n agentId,\n source,\n messages: messages.slice(systemPrefixLength),\n // context-engine-04: same accounting basis as the auto path.\n prefixMessages: messages.slice(0, systemPrefixLength),\n memory: mem,\n ...(pending.options?.preserveRecentTurns !== undefined\n ? { preserveRecentTurns: pending.options.preserveRecentTurns }\n : {}),\n });\n } catch (cause) {\n pending.reject(cause);\n return;\n }\n const { result } = envelope;\n const applied = result.droppedMessageIndices.length > 0;\n if (applied) {\n spliceCompacted(env, envelope);\n yield {\n type: 'context.compacted',\n runId: state.id,\n sessionId,\n agentId,\n beforeTokens: result.beforeTokens,\n afterTokens: result.afterTokens,\n summaryTokens: result.summaryTokens,\n durationMs: Date.now() - startedAt,\n source,\n hooksFiredCount: result.hooksFiredCount,\n };\n }\n pending.resolve({\n beforeTokens: result.beforeTokens,\n afterTokens: result.afterTokens,\n summaryTokens: result.summaryTokens,\n durationMs: Date.now() - startedAt,\n hooksFiredCount: result.hooksFiredCount,\n summary: result.summary ?? '',\n applied,\n ...(applied ? {} : { skippedReason: 'nothing-to-trim' as const }),\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { noopCompactionResult } from "./run-compaction.js";
2
+
3
+ //#region src/runtime/run-finish.ts
4
+ /**
5
+ * Build the factory's `finishRunBase` over the shared manual-compact
6
+ * queue and spill writer.
7
+ */
8
+ function createRunFinisher(deps) {
9
+ const { pendingManualCompacts, spillWriter, checkpointStore, checkpointPolicy } = deps;
10
+ /**
11
+ * Terminal wrapper around {@link finalize}: every exit path of the run
12
+ * loop - completed, failed, aborted, suspended - ends the stream with
13
+ * an `agent.end` event carrying the final {@link AgentResult} (AG-20).
14
+ *
15
+ * The parameter is `MutableRunState & RunState`: the single caller
16
+ * (factory `finishRun`) is typed exactly so, and the intersection lets
17
+ * {@link finalize} assemble `AgentResult.state` without a cast (W-047).
18
+ */
19
+ async function* finishRunBase(state, snapshot) {
20
+ while (pendingManualCompacts.length > 0) pendingManualCompacts.shift()?.resolve(noopCompactionResult("no-active-run"));
21
+ const result = finalize(state, snapshot);
22
+ if (result.status === "completed" || result.status === "failed") {
23
+ await spillWriter.clear?.(result.state.id).catch(() => {});
24
+ if (checkpointPolicy === "delete-on-terminal" && checkpointStore !== void 0) await checkpointStore.deleteThread(result.state.id).catch(() => {});
25
+ }
26
+ yield {
27
+ type: "agent.end",
28
+ runId: result.state.id,
29
+ result
30
+ };
31
+ return result;
32
+ }
33
+ function finalize(state, snapshot) {
34
+ state.finishedAt = state.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
35
+ return {
36
+ output: snapshot.output,
37
+ usage: state.usage,
38
+ status: state.status,
39
+ ...state.error !== void 0 ? { error: state.error } : {},
40
+ state
41
+ };
42
+ }
43
+ return finishRunBase;
44
+ }
45
+
46
+ //#endregion
47
+ export { createRunFinisher };
48
+ //# sourceMappingURL=run-finish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-finish.js","names":[],"sources":["../../src/runtime/run-finish.ts"],"sourcesContent":["/**\n * The terminal path of every agent run: settle leftover manual-compact\n * requests (CE-3 / AG-13), assemble the final {@link AgentResult}\n * (AG-9), clear terminal-run spill artifacts (TL-10), and end the\n * stream with `agent.end` (AG-20). Extracted verbatim from\n * `factory.ts` (issue #23); the former factory closures now hang off\n * {@link createRunFinisher}'s deps.\n *\n * @packageDocumentation\n */\n\nimport type { AgentEvent, AgentResult, CheckpointStore, RunState } from '@graphorin/core';\nimport type { createDefaultSpillWriter } from '@graphorin/tools/result';\nimport { noopCompactionResult, type PendingManualCompact } from './run-compaction.js';\nimport type { InternalRunSnapshot, MutableRunState } from './run-input.js';\n\n/** What the terminal path needs from the agent factory scope. */\nexport interface RunFinisherDeps {\n readonly pendingManualCompacts: PendingManualCompact[];\n readonly spillWriter: ReturnType<typeof createDefaultSpillWriter>;\n /** W-005: thread erasure on terminal runs (`'delete-on-terminal'`). */\n readonly checkpointStore?: CheckpointStore | undefined;\n readonly checkpointPolicy?: 'keep' | 'delete-on-terminal' | undefined;\n}\n\n/**\n * Build the factory's `finishRunBase` over the shared manual-compact\n * queue and spill writer.\n */\nexport function createRunFinisher<TOutput>(\n deps: RunFinisherDeps,\n): (\n state: MutableRunState & RunState,\n snapshot: InternalRunSnapshot<TOutput>,\n) => AsyncGenerator<AgentEvent<TOutput>, AgentResult<TOutput>, void> {\n const { pendingManualCompacts, spillWriter, checkpointStore, checkpointPolicy } = deps;\n\n /**\n * Terminal wrapper around {@link finalize}: every exit path of the run\n * loop - completed, failed, aborted, suspended - ends the stream with\n * an `agent.end` event carrying the final {@link AgentResult} (AG-20).\n *\n * The parameter is `MutableRunState & RunState`: the single caller\n * (factory `finishRun`) is typed exactly so, and the intersection lets\n * {@link finalize} assemble `AgentResult.state` without a cast (W-047).\n */\n async function* finishRunBase(\n state: MutableRunState & RunState,\n snapshot: InternalRunSnapshot<TOutput>,\n ): AsyncGenerator<AgentEvent<TOutput>, AgentResult<TOutput>, void> {\n // CE-3/AG-13: settle manual-compact requests the loop never serviced -\n // the run is over, there is no live buffer left to splice.\n while (pendingManualCompacts.length > 0) {\n pendingManualCompacts.shift()?.resolve(noopCompactionResult('no-active-run'));\n }\n const result = finalize(state, snapshot);\n // TL-10: spill artifacts are run-scoped scratch - drop them once the\n // run is terminal. `awaiting_approval` and `aborted` runs keep\n // theirs (handles must survive resume); orphans fall to the writer's\n // startup TTL sweep.\n if (result.status === 'completed' || result.status === 'failed') {\n await spillWriter.clear?.(result.state.id).catch(() => {});\n // W-005: opt-in checkpoint hygiene mirrors the spill lifecycle -\n // same statuses, best-effort, and NEVER for awaiting_approval /\n // aborted runs (their thread is the resume state). Default 'keep'\n // preserves post-hoc debugging and process-restart resume.\n if (checkpointPolicy === 'delete-on-terminal' && checkpointStore !== undefined) {\n await checkpointStore.deleteThread(result.state.id).catch(() => {});\n }\n }\n yield { type: 'agent.end', runId: result.state.id, result };\n return result;\n }\n\n function finalize(\n state: MutableRunState & RunState,\n snapshot: InternalRunSnapshot<TOutput>,\n ): AgentResult<TOutput> {\n state.finishedAt = state.finishedAt ?? new Date().toISOString();\n // AG-9: the result carries the terminal status, the failure (when\n // any), and the final RunState - a suspended run is resumable from\n // the result alone, no checkpointStore required.\n return {\n output: snapshot.output,\n usage: state.usage,\n status: state.status,\n ...(state.error !== undefined ? { error: state.error } : {}),\n state,\n };\n }\n\n return finishRunBase;\n}\n"],"mappings":";;;;;;;AA6BA,SAAgB,kBACd,MAImE;CACnE,MAAM,EAAE,uBAAuB,aAAa,iBAAiB,qBAAqB;;;;;;;;;;CAWlF,gBAAgB,cACd,OACA,UACiE;AAGjE,SAAO,sBAAsB,SAAS,EACpC,uBAAsB,OAAO,EAAE,QAAQ,qBAAqB,gBAAgB,CAAC;EAE/E,MAAM,SAAS,SAAS,OAAO,SAAS;AAKxC,MAAI,OAAO,WAAW,eAAe,OAAO,WAAW,UAAU;AAC/D,SAAM,YAAY,QAAQ,OAAO,MAAM,GAAG,CAAC,YAAY,GAAG;AAK1D,OAAI,qBAAqB,wBAAwB,oBAAoB,OACnE,OAAM,gBAAgB,aAAa,OAAO,MAAM,GAAG,CAAC,YAAY,GAAG;;AAGvE,QAAM;GAAE,MAAM;GAAa,OAAO,OAAO,MAAM;GAAI;GAAQ;AAC3D,SAAO;;CAGT,SAAS,SACP,OACA,UACsB;AACtB,QAAM,aAAa,MAAM,+BAAc,IAAI,MAAM,EAAC,aAAa;AAI/D,SAAO;GACL,QAAQ,SAAS;GACjB,OAAO,MAAM;GACb,QAAQ,MAAM;GACd,GAAI,MAAM,UAAU,SAAY,EAAE,OAAO,MAAM,OAAO,GAAG,EAAE;GAC3D;GACD;;AAGH,QAAO"}