@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,336 @@
1
+ import { buildAssistantMessage } from "./messages.js";
2
+ import { createHash } from "node:crypto";
3
+ import { composeGuardrails } from "@graphorin/security/guardrails";
4
+
5
+ //#region src/runtime/run-gates.ts
6
+ /**
7
+ * The run-level gates the agent loop applies to its inputs and outputs:
8
+ * input / output guardrail screening (AG-2 / SDF-4), the structured
9
+ * output contract (AG-3: the JSON-only instruction, fence stripping,
10
+ * parse + validation), the lateral-leak commit gate on outgoing
11
+ * assistant content (RB-55 / AG-10), the verifier gate on terminal
12
+ * responses (C3), and the shared cancellation path (AG-6). Extracted
13
+ * verbatim from `factory.ts` (issue #23).
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+ const sha256Hex = (input) => createHash("sha256").update(input, "utf8").digest("hex");
18
+ /**
19
+ * Replacement content committed in place of assistant commentary the
20
+ * causality monitor blocked (AG-10). Worded to not match any built-in
21
+ * denial pattern, so the notice itself never re-triggers the monitor.
22
+ */
23
+ const LATERAL_LEAK_BLOCKED_NOTICE = "[graphorin] assistant commentary withheld by the lateral-leak defense.";
24
+ /**
25
+ * Strip a single Markdown code fence around a JSON payload (AG-3).
26
+ * Models often wrap structured output in ```json fences even when told
27
+ * not to. ReDoS-safe: the info string is matched with `[^\n]*`.
28
+ */
29
+ function stripJsonFence(text) {
30
+ const trimmed = text.trim();
31
+ return /^```[^\n]*\n([\s\S]*?)\n?```$/.exec(trimmed)?.[1] ?? trimmed;
32
+ }
33
+ /**
34
+ * The AG-3 fallback instruction: one trailing system message that
35
+ * pins JSON-only output and embeds the wire schema / description.
36
+ * This is the documented structured-output contract for adapters that
37
+ * do not yet consume `ProviderRequest.outputType` natively (PS-24).
38
+ */
39
+ function buildStructuredInstruction(spec) {
40
+ const parts = ["Respond with a single valid JSON value only - no prose, no Markdown code fences."];
41
+ if (spec.description !== void 0 && spec.description.length > 0) parts.push(spec.description);
42
+ if (spec.jsonSchema !== void 0) parts.push(`The JSON MUST conform to this JSON Schema:\n${JSON.stringify(spec.jsonSchema)}`);
43
+ return parts.join("\n");
44
+ }
45
+ /**
46
+ * AG-6: shared cancellation path for the loop-top abort check, a
47
+ * mid-stream provider abort, and the abort-during-suspend seam (W-038).
48
+ * Yields `agent.cancelling`, applies the `onPendingApprovals` policy,
49
+ * and returns `true` when the run was finalized as 'failed' (the 'fail'
50
+ * policy WITH live approvals - the caller must `return finalize(...)`);
51
+ * otherwise it sets `state.status = 'aborted'` and returns `false`.
52
+ *
53
+ * Policy semantics (W-038):
54
+ * - `'deny'`: drain every pending approval AND commit a matching tool
55
+ * message per drained toolCallId - the transcript must not keep a
56
+ * dangling `tool_use` real providers reject on a later resume.
57
+ * - `'hold'`: keep `pendingApprovals` on the aborted state; resume is
58
+ * possible only through an explicit directive.
59
+ * - `'fail'`: fail the run ONLY when approvals are actually pending -
60
+ * an abort with an empty queue is a plain 'aborted', per the
61
+ * documented contract.
62
+ */
63
+ async function* emitCancellation(env) {
64
+ const { state, messages, getPendingAbort } = env;
65
+ yield {
66
+ type: "agent.cancelling",
67
+ runId: state.id,
68
+ drain: getPendingAbort()?.drain ?? false,
69
+ onPendingApprovals: getPendingAbort()?.onPendingApprovals ?? "deny"
70
+ };
71
+ const policy = getPendingAbort()?.onPendingApprovals ?? "deny";
72
+ if (policy === "deny") {
73
+ const drained = state.pendingApprovals.splice(0, state.pendingApprovals.length);
74
+ for (const approval of drained) {
75
+ yield {
76
+ type: "tool.approval.denied",
77
+ toolCallId: approval.toolCallId,
78
+ reason: "auto-denied: agent.abort()"
79
+ };
80
+ const text = "Error: tool approval denied: auto-denied on abort";
81
+ messages.push({
82
+ role: "tool",
83
+ toolCallId: approval.toolCallId,
84
+ content: text
85
+ });
86
+ state.messages.push({
87
+ role: "tool",
88
+ toolCallId: approval.toolCallId,
89
+ content: text
90
+ });
91
+ }
92
+ } else if (policy === "fail" && state.pendingApprovals.length > 0) {
93
+ state.status = "failed";
94
+ state.error = {
95
+ message: "aborted with pending approvals",
96
+ code: "run-aborted"
97
+ };
98
+ yield {
99
+ type: "agent.error",
100
+ error: {
101
+ message: "aborted with pending approvals",
102
+ code: "run-aborted"
103
+ }
104
+ };
105
+ return true;
106
+ }
107
+ state.status = "aborted";
108
+ return false;
109
+ }
110
+ /**
111
+ * AG-2 / SDF-4: input guardrails screen each fresh-run seed user
112
+ * message (string content) BEFORE the first provider call, using the
113
+ * canonical `@graphorin/security` composer. 'block' fails the run
114
+ * without reaching the model; 'rewrite' replaces the content in both
115
+ * the working buffer and the persisted RunState; 'warn' logs and
116
+ * continues. Resumed runs skip the pass - their seed was screened
117
+ * when first submitted.
118
+ *
119
+ * Returns `true` when a guardrail blocked the run (the failure is
120
+ * already recorded on `state` and streamed - the loop must finish).
121
+ */
122
+ async function* screenInputGuardrails(env, inputGuards) {
123
+ const { state, messages, sessionId, agentId } = env;
124
+ for (let i = 0; i < messages.length; i++) {
125
+ const msg = messages[i];
126
+ if (msg === void 0 || msg.role !== "user" || typeof msg.content !== "string") continue;
127
+ const composed = await composeGuardrails(inputGuards, msg.content, {
128
+ stage: "input",
129
+ runId: state.id,
130
+ sessionId,
131
+ agentId
132
+ });
133
+ if (!composed.ok) {
134
+ yield {
135
+ type: "guardrail.tripped",
136
+ guardrailName: composed.name,
137
+ phase: "input",
138
+ reason: composed.message
139
+ };
140
+ const message = `input guardrail '${composed.name}' blocked the run: ${composed.message}`;
141
+ yield {
142
+ type: "agent.error",
143
+ error: {
144
+ message,
145
+ code: "guardrail-blocked"
146
+ }
147
+ };
148
+ state.status = "failed";
149
+ state.error = {
150
+ message,
151
+ code: "guardrail-blocked"
152
+ };
153
+ return true;
154
+ }
155
+ if (composed.value !== msg.content) {
156
+ const rewritten = {
157
+ ...msg,
158
+ content: composed.value
159
+ };
160
+ const stateIdx = state.messages.indexOf(msg);
161
+ messages[i] = rewritten;
162
+ if (stateIdx !== -1) state.messages[stateIdx] = rewritten;
163
+ }
164
+ }
165
+ return false;
166
+ }
167
+ /**
168
+ * Lateral-leak (RB-55 / AG-10): scan the outgoing assistant
169
+ * content BEFORE it is appended, so a 'block' decision keeps
170
+ * the laundered commentary out of the durable history - and
171
+ * therefore out of every subsequent provider request. The
172
+ * deltas already streamed; what 'block' protects is the
173
+ * persistent buffer and the run's final output.
174
+ *
175
+ * Commits the (possibly replaced) assistant message to both buffers,
176
+ * records the C6 taint span, and emits `agent.lateral-leak.detected`
177
+ * when the monitor tripped. Returns `leakBlocked`.
178
+ */
179
+ async function* commitAssistantMessage(env, textBuffer, stepReasoningParts, finalCalls, reasoningPolicy) {
180
+ const { state, messages, sessionId, agentId, causalityMonitor, toolDataFlowGuard } = env;
181
+ const leakCheck = causalityMonitor !== void 0 && textBuffer.length > 0 ? causalityMonitor.checkMessage(textBuffer) : void 0;
182
+ const leakBlocked = leakCheck?.leakDetected === true && leakCheck.decision === "block";
183
+ const assistant = buildAssistantMessage(leakBlocked ? LATERAL_LEAK_BLOCKED_NOTICE : textBuffer, stepReasoningParts, finalCalls, agentId, reasoningPolicy);
184
+ messages.push(assistant);
185
+ state.messages.push(assistant);
186
+ if (toolDataFlowGuard !== void 0 && textBuffer.length > 0 && !leakBlocked) toolDataFlowGuard.recordAssistant(state.id, textBuffer);
187
+ if (leakCheck?.leakDetected === true) {
188
+ const sha = sha256Hex(textBuffer);
189
+ yield {
190
+ type: "agent.lateral-leak.detected",
191
+ runId: state.id,
192
+ sessionId,
193
+ agentId,
194
+ vector: leakCheck.vector,
195
+ severity: leakCheck.severity,
196
+ causalityChain: leakCheck.causalityChain,
197
+ messageContentSha256: sha,
198
+ ...leakCheck.matchedPattern !== void 0 ? { matchedPattern: leakCheck.matchedPattern } : {},
199
+ decision: leakCheck.decision,
200
+ detectedAtIso: (/* @__PURE__ */ new Date()).toISOString()
201
+ };
202
+ }
203
+ return leakBlocked;
204
+ }
205
+ /**
206
+ * C3: verifier seam - deterministic checks run on EVERY terminal
207
+ * response (so the outcome is always observable via
208
+ * verifier.result events). Failures feed structured feedback back
209
+ * as a user message and the loop continues, but only while
210
+ * continuation rounds remain (maxVerifierRounds, default 1);
211
+ * exhausted rounds complete with the last output. A verifier that
212
+ * throws is treated as passed (a buggy verifier must not fail the
213
+ * run).
214
+ */
215
+ async function* runVerifierGate(env, finalSnapshot, stepNumber, verifierRoundsUsed) {
216
+ const { config, state, messages } = env;
217
+ if (config.verifiers !== void 0 && config.verifiers.length > 0) {
218
+ const feedbacks = [];
219
+ for (const verifier of config.verifiers) {
220
+ let result;
221
+ try {
222
+ result = await verifier.verify({
223
+ output: String(finalSnapshot.output ?? ""),
224
+ state,
225
+ stepNumber
226
+ });
227
+ } catch {
228
+ result = { ok: true };
229
+ }
230
+ yield {
231
+ type: "verifier.result",
232
+ verifierId: verifier.id,
233
+ ok: result.ok,
234
+ ...result.ok ? {} : { feedback: result.feedback },
235
+ stepNumber
236
+ };
237
+ if (!result.ok) feedbacks.push(`[verifier:${verifier.id}] ${result.feedback}`);
238
+ }
239
+ if (feedbacks.length > 0 && verifierRoundsUsed < (config.maxVerifierRounds ?? 1)) {
240
+ const feedbackMessage = {
241
+ role: "user",
242
+ content: `Your response failed ${feedbacks.length} verification check(s). Address the feedback and answer again:\n${feedbacks.join("\n")}`
243
+ };
244
+ messages.push(feedbackMessage);
245
+ state.messages.push(feedbackMessage);
246
+ return {
247
+ continueRun: true,
248
+ verifierRoundsUsed: verifierRoundsUsed + 1
249
+ };
250
+ }
251
+ }
252
+ return {
253
+ continueRun: false,
254
+ verifierRoundsUsed
255
+ };
256
+ }
257
+ /**
258
+ * The terminal output phases of a run, in their frozen order: the AG-24
259
+ * stop-condition cut check, the AG-3 structured-output parse +
260
+ * validation (before output guardrails, so they screen the PARSED
261
+ * value), and the AG-2 / SDF-4 output guardrail screen. Mutates
262
+ * `state` / `finalSnapshot` exactly as the inline blocks did.
263
+ */
264
+ async function* finalizeRunOutput(env, finalSnapshot) {
265
+ const { config, state, sessionId, agentId, stopWhen } = env;
266
+ if (state.status === "running") {
267
+ const message = `run stopped by stop condition: ${stopWhen.description}`;
268
+ state.status = "failed";
269
+ state.error = {
270
+ message,
271
+ code: "stop-condition"
272
+ };
273
+ yield {
274
+ type: "agent.error",
275
+ error: {
276
+ message,
277
+ code: "stop-condition"
278
+ }
279
+ };
280
+ }
281
+ if (state.status === "completed" && config.outputType?.kind === "structured") {
282
+ const raw = String(finalSnapshot.output ?? "");
283
+ try {
284
+ const parsed = JSON.parse(stripJsonFence(raw));
285
+ finalSnapshot.output = config.outputType.schema !== void 0 ? config.outputType.schema.parse(parsed) : parsed;
286
+ } catch (cause) {
287
+ const message = `structured output validation failed: ${cause instanceof Error ? cause.message : String(cause)}`;
288
+ yield {
289
+ type: "agent.error",
290
+ error: {
291
+ message,
292
+ code: "output-validation-failed"
293
+ }
294
+ };
295
+ state.status = "failed";
296
+ state.error = {
297
+ message,
298
+ code: "output-validation-failed"
299
+ };
300
+ }
301
+ }
302
+ const outputGuards = config.guardrails?.output;
303
+ if (state.status === "completed" && outputGuards !== void 0 && outputGuards.length > 0) {
304
+ const composed = await composeGuardrails(outputGuards, finalSnapshot.output, {
305
+ stage: "output",
306
+ runId: state.id,
307
+ sessionId,
308
+ agentId
309
+ });
310
+ if (!composed.ok) {
311
+ yield {
312
+ type: "guardrail.tripped",
313
+ guardrailName: composed.name,
314
+ phase: "output",
315
+ reason: composed.message
316
+ };
317
+ const message = `output guardrail '${composed.name}' blocked the run: ${composed.message}`;
318
+ yield {
319
+ type: "agent.error",
320
+ error: {
321
+ message,
322
+ code: "guardrail-blocked"
323
+ }
324
+ };
325
+ state.status = "failed";
326
+ state.error = {
327
+ message,
328
+ code: "guardrail-blocked"
329
+ };
330
+ } else if (composed.value !== finalSnapshot.output) finalSnapshot.output = composed.value;
331
+ }
332
+ }
333
+
334
+ //#endregion
335
+ export { buildStructuredInstruction, commitAssistantMessage, emitCancellation, finalizeRunOutput, runVerifierGate, screenInputGuardrails };
336
+ //# sourceMappingURL=run-gates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-gates.js","names":["rewritten: Message","assistant: AssistantMessage","feedbacks: string[]","result: VerifierResult","feedbackMessage: Message","parsed: unknown"],"sources":["../../src/runtime/run-gates.ts"],"sourcesContent":["/**\n * The run-level gates the agent loop applies to its inputs and outputs:\n * input / output guardrail screening (AG-2 / SDF-4), the structured\n * output contract (AG-3: the JSON-only instruction, fence stripping,\n * parse + validation), the lateral-leak commit gate on outgoing\n * assistant content (RB-55 / AG-10), the verifier gate on terminal\n * responses (C3), and the shared cancellation path (AG-6). Extracted\n * verbatim from `factory.ts` (issue #23).\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport type {\n AgentEvent,\n AssistantMessage,\n Message,\n ReasoningContent,\n ReasoningRetention,\n RunState,\n ToolCall,\n} from '@graphorin/core';\nimport { composeGuardrails, type InputGuardrail } from '@graphorin/security/guardrails';\nimport type { CausalityMonitor } from '../lateral-leak/causality-monitor.js';\nimport type { buildDataFlowGuard } from '../tooling/dataflow.js';\nimport type { AbortOptions, AgentConfig, VerifierResult } from '../types.js';\nimport { buildAssistantMessage } from './messages.js';\nimport type { InternalRunSnapshot, MutableRunState } from './run-input.js';\n\nconst sha256Hex = (input: string): string =>\n createHash('sha256').update(input, 'utf8').digest('hex');\n\n/**\n * Replacement content committed in place of assistant commentary the\n * causality monitor blocked (AG-10). Worded to not match any built-in\n * denial pattern, so the notice itself never re-triggers the monitor.\n */\nexport const LATERAL_LEAK_BLOCKED_NOTICE =\n '[graphorin] assistant commentary withheld by the lateral-leak defense.';\n\n/**\n * Strip a single Markdown code fence around a JSON payload (AG-3).\n * Models often wrap structured output in ```json fences even when told\n * not to. ReDoS-safe: the info string is matched with `[^\\n]*`.\n */\nexport function stripJsonFence(text: string): string {\n const trimmed = text.trim();\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n?```$/.exec(trimmed);\n return match?.[1] ?? trimmed;\n}\n\n/**\n * The AG-3 fallback instruction: one trailing system message that\n * pins JSON-only output and embeds the wire schema / description.\n * This is the documented structured-output contract for adapters that\n * do not yet consume `ProviderRequest.outputType` natively (PS-24).\n */\nexport function buildStructuredInstruction(spec: {\n readonly description?: string;\n readonly jsonSchema?: Readonly<Record<string, unknown>>;\n}): string {\n const parts = [\n 'Respond with a single valid JSON value only - no prose, no Markdown code fences.',\n ];\n if (spec.description !== undefined && spec.description.length > 0) {\n parts.push(spec.description);\n }\n if (spec.jsonSchema !== undefined) {\n parts.push(`The JSON MUST conform to this JSON Schema:\\n${JSON.stringify(spec.jsonSchema)}`);\n }\n return parts.join('\\n');\n}\n\nexport { sha256Hex };\n\n/** The run-scoped context the cancellation path operates on. */\nexport interface CancellationEnv {\n readonly state: MutableRunState & RunState;\n readonly messages: Message[];\n readonly getPendingAbort: () => AbortOptions | undefined;\n}\n\n/**\n * AG-6: shared cancellation path for the loop-top abort check, a\n * mid-stream provider abort, and the abort-during-suspend seam (W-038).\n * Yields `agent.cancelling`, applies the `onPendingApprovals` policy,\n * and returns `true` when the run was finalized as 'failed' (the 'fail'\n * policy WITH live approvals - the caller must `return finalize(...)`);\n * otherwise it sets `state.status = 'aborted'` and returns `false`.\n *\n * Policy semantics (W-038):\n * - `'deny'`: drain every pending approval AND commit a matching tool\n * message per drained toolCallId - the transcript must not keep a\n * dangling `tool_use` real providers reject on a later resume.\n * - `'hold'`: keep `pendingApprovals` on the aborted state; resume is\n * possible only through an explicit directive.\n * - `'fail'`: fail the run ONLY when approvals are actually pending -\n * an abort with an empty queue is a plain 'aborted', per the\n * documented contract.\n */\nexport async function* emitCancellation<TOutput>(\n env: CancellationEnv,\n): AsyncGenerator<AgentEvent<TOutput>, boolean, void> {\n const { state, messages, getPendingAbort } = env;\n yield {\n type: 'agent.cancelling',\n runId: state.id,\n drain: getPendingAbort()?.drain ?? false,\n onPendingApprovals: getPendingAbort()?.onPendingApprovals ?? 'deny',\n };\n const policy = getPendingAbort()?.onPendingApprovals ?? 'deny';\n if (policy === 'deny') {\n const drained = state.pendingApprovals.splice(0, state.pendingApprovals.length);\n for (const approval of drained) {\n yield {\n type: 'tool.approval.denied',\n toolCallId: approval.toolCallId,\n reason: 'auto-denied: agent.abort()',\n };\n const text = 'Error: tool approval denied: auto-denied on abort';\n messages.push({ role: 'tool', toolCallId: approval.toolCallId, content: text });\n state.messages.push({ role: 'tool', toolCallId: approval.toolCallId, content: text });\n }\n } else if (policy === 'fail' && state.pendingApprovals.length > 0) {\n state.status = 'failed';\n state.error = { message: 'aborted with pending approvals', code: 'run-aborted' };\n yield {\n type: 'agent.error',\n error: { message: 'aborted with pending approvals', code: 'run-aborted' },\n };\n return true;\n }\n state.status = 'aborted';\n return false;\n}\n\n/** The run-scoped context the input-guardrail screen operates on. */\nexport interface GuardrailScreenEnv {\n readonly state: MutableRunState & RunState;\n readonly messages: Message[];\n readonly sessionId: string;\n readonly agentId: string;\n}\n\n/**\n * AG-2 / SDF-4: input guardrails screen each fresh-run seed user\n * message (string content) BEFORE the first provider call, using the\n * canonical `@graphorin/security` composer. 'block' fails the run\n * without reaching the model; 'rewrite' replaces the content in both\n * the working buffer and the persisted RunState; 'warn' logs and\n * continues. Resumed runs skip the pass - their seed was screened\n * when first submitted.\n *\n * Returns `true` when a guardrail blocked the run (the failure is\n * already recorded on `state` and streamed - the loop must finish).\n */\nexport async function* screenInputGuardrails<TOutput>(\n env: GuardrailScreenEnv,\n inputGuards: ReadonlyArray<InputGuardrail<string>>,\n): AsyncGenerator<AgentEvent<TOutput>, boolean, void> {\n const { state, messages, sessionId, agentId } = env;\n for (let i = 0; i < messages.length; i++) {\n const msg = messages[i];\n if (msg === undefined || msg.role !== 'user' || typeof msg.content !== 'string') continue;\n const composed = await composeGuardrails(inputGuards, msg.content, {\n stage: 'input',\n runId: state.id,\n sessionId,\n agentId,\n });\n if (!composed.ok) {\n yield {\n type: 'guardrail.tripped',\n guardrailName: composed.name,\n phase: 'input',\n reason: composed.message,\n };\n const message = `input guardrail '${composed.name}' blocked the run: ${composed.message}`;\n yield { type: 'agent.error', error: { message, code: 'guardrail-blocked' } };\n state.status = 'failed';\n state.error = { message, code: 'guardrail-blocked' };\n return true;\n }\n if (composed.value !== msg.content) {\n const rewritten: Message = { ...msg, content: composed.value };\n const stateIdx = state.messages.indexOf(msg);\n messages[i] = rewritten;\n if (stateIdx !== -1) state.messages[stateIdx] = rewritten;\n }\n }\n return false;\n}\n\n/** The run-scoped context the assistant-commit gate operates on. */\nexport interface AssistantCommitEnv {\n readonly state: MutableRunState & RunState;\n readonly messages: Message[];\n readonly sessionId: string;\n readonly agentId: string;\n readonly causalityMonitor: CausalityMonitor | undefined;\n readonly toolDataFlowGuard: ReturnType<typeof buildDataFlowGuard> | undefined;\n}\n\n/**\n * Lateral-leak (RB-55 / AG-10): scan the outgoing assistant\n * content BEFORE it is appended, so a 'block' decision keeps\n * the laundered commentary out of the durable history - and\n * therefore out of every subsequent provider request. The\n * deltas already streamed; what 'block' protects is the\n * persistent buffer and the run's final output.\n *\n * Commits the (possibly replaced) assistant message to both buffers,\n * records the C6 taint span, and emits `agent.lateral-leak.detected`\n * when the monitor tripped. Returns `leakBlocked`.\n */\nexport async function* commitAssistantMessage<TOutput>(\n env: AssistantCommitEnv,\n textBuffer: string,\n stepReasoningParts: ReadonlyArray<ReasoningContent>,\n finalCalls: ReadonlyArray<ToolCall>,\n reasoningPolicy: ReasoningRetention,\n): AsyncGenerator<AgentEvent<TOutput>, boolean, void> {\n const { state, messages, sessionId, agentId, causalityMonitor, toolDataFlowGuard } = env;\n const leakCheck =\n causalityMonitor !== undefined && textBuffer.length > 0\n ? causalityMonitor.checkMessage(textBuffer)\n : undefined;\n const leakBlocked = leakCheck?.leakDetected === true && leakCheck.decision === 'block';\n\n const assistant: AssistantMessage = buildAssistantMessage(\n leakBlocked ? LATERAL_LEAK_BLOCKED_NOTICE : textBuffer,\n stepReasoningParts,\n finalCalls,\n agentId,\n reasoningPolicy,\n );\n messages.push(assistant);\n state.messages.push(assistant);\n\n // C6: once the run is tainted, the model's own TEXT output is\n // derived from untrusted context - record it so a later sink call\n // copying the model's phrasing still trips the verbatim probe\n // (no-op on untainted runs). Tool-call args are deliberately NOT\n // recorded: the sink gate inspects those same args next, and\n // recording them first would self-match every post-taint call,\n // collapsing the precise verbatim signal into the coarse one.\n if (toolDataFlowGuard !== undefined && textBuffer.length > 0 && !leakBlocked) {\n toolDataFlowGuard.recordAssistant(state.id, textBuffer);\n }\n\n if (leakCheck?.leakDetected === true) {\n const sha = sha256Hex(textBuffer);\n yield {\n type: 'agent.lateral-leak.detected',\n runId: state.id,\n sessionId,\n agentId,\n vector: leakCheck.vector,\n severity: leakCheck.severity,\n causalityChain: leakCheck.causalityChain,\n messageContentSha256: sha,\n ...(leakCheck.matchedPattern !== undefined\n ? { matchedPattern: leakCheck.matchedPattern }\n : {}),\n decision: leakCheck.decision,\n detectedAtIso: new Date().toISOString(),\n };\n }\n return leakBlocked;\n}\n\n/** The run-scoped context the verifier gate operates on. */\nexport interface VerifierGateEnv<TDeps, TOutput> {\n readonly config: Pick<AgentConfig<TDeps, TOutput>, 'verifiers' | 'maxVerifierRounds'>;\n readonly state: MutableRunState & RunState;\n readonly messages: Message[];\n}\n\n/** Outcome of one verifier gate pass (read by the run loop). */\nexport interface VerifierGateResult {\n /** `true`: feedback was fed back - the loop takes another step. */\n readonly continueRun: boolean;\n /** The continuation rounds consumed so far (threaded back to the loop). */\n readonly verifierRoundsUsed: number;\n}\n\n/**\n * C3: verifier seam - deterministic checks run on EVERY terminal\n * response (so the outcome is always observable via\n * verifier.result events). Failures feed structured feedback back\n * as a user message and the loop continues, but only while\n * continuation rounds remain (maxVerifierRounds, default 1);\n * exhausted rounds complete with the last output. A verifier that\n * throws is treated as passed (a buggy verifier must not fail the\n * run).\n */\nexport async function* runVerifierGate<TDeps, TOutput>(\n env: VerifierGateEnv<TDeps, TOutput>,\n finalSnapshot: InternalRunSnapshot<TOutput>,\n stepNumber: number,\n verifierRoundsUsed: number,\n): AsyncGenerator<AgentEvent<TOutput>, VerifierGateResult, void> {\n const { config, state, messages } = env;\n if (config.verifiers !== undefined && config.verifiers.length > 0) {\n const feedbacks: string[] = [];\n for (const verifier of config.verifiers) {\n let result: VerifierResult;\n try {\n result = await verifier.verify({\n output: String(finalSnapshot.output ?? ''),\n state,\n stepNumber,\n });\n } catch {\n result = { ok: true };\n }\n yield {\n type: 'verifier.result',\n verifierId: verifier.id,\n ok: result.ok,\n ...(result.ok ? {} : { feedback: result.feedback }),\n stepNumber,\n };\n if (!result.ok) feedbacks.push(`[verifier:${verifier.id}] ${result.feedback}`);\n }\n if (feedbacks.length > 0 && verifierRoundsUsed < (config.maxVerifierRounds ?? 1)) {\n const feedbackMessage: Message = {\n role: 'user',\n content: `Your response failed ${feedbacks.length} verification check(s). Address the feedback and answer again:\\n${feedbacks.join('\\n')}`,\n };\n messages.push(feedbackMessage);\n state.messages.push(feedbackMessage);\n return { continueRun: true, verifierRoundsUsed: verifierRoundsUsed + 1 };\n }\n }\n return { continueRun: false, verifierRoundsUsed };\n}\n\n/** The run-scoped context the terminal output phases operate on. */\nexport interface RunOutputEnv<TDeps, TOutput> {\n readonly config: AgentConfig<TDeps, TOutput>;\n readonly state: MutableRunState & RunState;\n readonly sessionId: string;\n readonly agentId: string;\n readonly stopWhen: { readonly description: string };\n}\n\n/**\n * The terminal output phases of a run, in their frozen order: the AG-24\n * stop-condition cut check, the AG-3 structured-output parse +\n * validation (before output guardrails, so they screen the PARSED\n * value), and the AG-2 / SDF-4 output guardrail screen. Mutates\n * `state` / `finalSnapshot` exactly as the inline blocks did.\n */\nexport async function* finalizeRunOutput<TDeps, TOutput>(\n env: RunOutputEnv<TDeps, TOutput>,\n finalSnapshot: InternalRunSnapshot<TOutput>,\n): AsyncGenerator<AgentEvent<TOutput>, void, void> {\n const { config, state, sessionId, agentId, stopWhen } = env;\n if (state.status === 'running') {\n // AG-24: the loop exited via the stop condition (default\n // isStepCount(50)) with work still pending - that is a CUT run,\n // not a completion. Surface it as a typed failure so consumers\n // can tell it apart from a clean finish.\n const message = `run stopped by stop condition: ${stopWhen.description}`;\n state.status = 'failed';\n state.error = { message, code: 'stop-condition' };\n yield { type: 'agent.error', error: { message, code: 'stop-condition' } };\n }\n // AG-3: structured output is parsed + validated on the completed\n // path - a failure is a typed run failure (`output-validation-failed`),\n // never a silent text-cast. Runs BEFORE output guardrails so they\n // screen the PARSED value.\n if (state.status === 'completed' && config.outputType?.kind === 'structured') {\n const raw = String(finalSnapshot.output ?? '');\n try {\n const parsed: unknown = JSON.parse(stripJsonFence(raw));\n finalSnapshot.output = (\n config.outputType.schema !== undefined ? config.outputType.schema.parse(parsed) : parsed\n ) as TOutput;\n } catch (cause) {\n const message = `structured output validation failed: ${\n cause instanceof Error ? cause.message : String(cause)\n }`;\n yield { type: 'agent.error', error: { message, code: 'output-validation-failed' } };\n state.status = 'failed';\n state.error = { message, code: 'output-validation-failed' };\n }\n }\n\n // AG-2 / SDF-4: output guardrails screen the final output on the\n // completed path before `agent.end`. 'block' fails the run;\n // 'rewrite' replaces the durable result (`result.output`) - the\n // text deltas were already streamed, so the rewrite governs what\n // is persisted/returned, not the live token stream.\n const outputGuards = config.guardrails?.output;\n if (state.status === 'completed' && outputGuards !== undefined && outputGuards.length > 0) {\n const composed = await composeGuardrails(outputGuards, finalSnapshot.output, {\n stage: 'output',\n runId: state.id,\n sessionId,\n agentId,\n });\n if (!composed.ok) {\n yield {\n type: 'guardrail.tripped',\n guardrailName: composed.name,\n phase: 'output',\n reason: composed.message,\n };\n const message = `output guardrail '${composed.name}' blocked the run: ${composed.message}`;\n yield { type: 'agent.error', error: { message, code: 'guardrail-blocked' } };\n state.status = 'failed';\n state.error = { message, code: 'guardrail-blocked' };\n } else if (composed.value !== finalSnapshot.output) {\n finalSnapshot.output = composed.value;\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import { newId } from "../internal/ids.js";
2
+ import { createInitialRunState } from "../run-state/index.js";
3
+ import { lastUserText } from "./messages.js";
4
+
5
+ //#region src/runtime/run-init.ts
6
+ /**
7
+ * Inject the agent's system prompt at the top of the buffer
8
+ * exactly once per run, before any seed messages - then mirror the
9
+ * assembled messages into RunState so the JSONL session export and
10
+ * any downstream consumers see what the agent saw.
11
+ */
12
+ async function seedInitialMessages(env, messages, seed) {
13
+ const { config, options, memory, agentId, sessionId, userId } = env;
14
+ const { tracer, signal, usageAcc, state } = env;
15
+ const instructionsRaw = config.instructions;
16
+ let instructionsText;
17
+ if (typeof instructionsRaw === "string") instructionsText = instructionsRaw;
18
+ else instructionsText = await instructionsRaw({
19
+ runId: state.id,
20
+ sessionId,
21
+ ...userId !== void 0 ? { userId } : {},
22
+ agentId,
23
+ deps: options.deps ?? config.deps,
24
+ tracer,
25
+ signal,
26
+ usage: usageAcc,
27
+ stepNumber: 0,
28
+ messages,
29
+ state
30
+ });
31
+ let systemPrompt = instructionsText;
32
+ if (config.autoAssembleContext === true && memory !== void 0) {
33
+ const lastUser = lastUserText(seed);
34
+ systemPrompt = (await memory.contextEngine.assemble(memory, {
35
+ scope: {
36
+ userId: userId ?? agentId,
37
+ sessionId,
38
+ agentId
39
+ },
40
+ agentId,
41
+ sessionId,
42
+ runId: state.id,
43
+ ...instructionsText.length > 0 ? { agentInstructions: instructionsText } : {},
44
+ ...lastUser !== void 0 ? { lastUserMessage: lastUser } : {}
45
+ })).systemMessage.content;
46
+ }
47
+ if (systemPrompt.length > 0) messages.push({
48
+ role: "system",
49
+ content: systemPrompt
50
+ });
51
+ messages.push(...seed);
52
+ for (const m of messages) state.messages.push(m);
53
+ }
54
+ /**
55
+ * Bootstrap the run's state: create (or adopt) the {@link RunState},
56
+ * rehydrate the run-scoped security state (AG-19: the persisted coarse
57
+ * taint summary re-seeds the enforce-mode sink gate), and restore the
58
+ * `tool_search` promotion set (TL-7 / AG-19, with the C1
59
+ * `'run-boundary'` snapshot).
60
+ */
61
+ function initializeRunState(env, resumed) {
62
+ const { config, agentId, sessionId, userId, toolDataFlowGuard } = env;
63
+ const state = resumed ? resumed : createInitialRunState({
64
+ id: newId("run"),
65
+ agentId,
66
+ sessionId,
67
+ ...userId !== void 0 ? { userId } : {}
68
+ });
69
+ if (resumed && state.taintSummary !== void 0) toolDataFlowGuard?.seedLedger(state.id, state.taintSummary);
70
+ const promotedDeferred = /* @__PURE__ */ new Set();
71
+ if (resumed && state.promotedTools !== void 0) for (const name of state.promotedTools) promotedDeferred.add(name);
72
+ return {
73
+ state,
74
+ promotedDeferred,
75
+ runStartPromotions: config.toolPromotion === "run-boundary" ? new Set(promotedDeferred) : void 0
76
+ };
77
+ }
78
+
79
+ //#endregion
80
+ export { initializeRunState, seedInitialMessages };
81
+ //# sourceMappingURL=run-init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-init.js","names":["instructionsText: string"],"sources":["../../src/runtime/run-init.ts"],"sourcesContent":["/**\n * Fresh-run message-buffer initialization: resolve the agent's\n * `instructions` (string or per-run function form, AG-8), optionally\n * assemble the memory-aware 6-layer system prompt via the context\n * engine (CE-1, opt-in), and seed the buffer + RunState mirror.\n * Extracted verbatim from `factory.ts` (issue #23).\n *\n * @packageDocumentation\n */\n\nimport type { Message, RunContext, RunState } from '@graphorin/core';\nimport type { Memory } from '@graphorin/memory';\nimport { newId } from '../internal/ids.js';\nimport { createInitialRunState } from '../run-state/index.js';\nimport type { buildDataFlowGuard } from '../tooling/dataflow.js';\nimport type { AgentCallOptions, AgentConfig } from '../types.js';\nimport { lastUserText } from './messages.js';\nimport type { MutableRunState } from './run-input.js';\n\n/** What the fresh-run seeding pass needs from the run loop's scope. */\nexport interface RunInitEnv<TDeps, TOutput> {\n readonly config: AgentConfig<TDeps, TOutput>;\n readonly options: AgentCallOptions<TDeps>;\n readonly memory: Memory | undefined;\n readonly agentId: string;\n readonly sessionId: string;\n readonly userId: string | undefined;\n readonly tracer: RunContext<TDeps>['tracer'];\n readonly signal: AbortSignal;\n readonly usageAcc: RunContext<TDeps>['usage'];\n readonly state: MutableRunState & RunState;\n}\n\n/**\n * Inject the agent's system prompt at the top of the buffer\n * exactly once per run, before any seed messages - then mirror the\n * assembled messages into RunState so the JSONL session export and\n * any downstream consumers see what the agent saw.\n */\nexport async function seedInitialMessages<TDeps, TOutput>(\n env: RunInitEnv<TDeps, TOutput>,\n messages: Message[],\n seed: Message[],\n): Promise<void> {\n const { config, options, memory, agentId, sessionId, userId } = env;\n const { tracer, signal, usageAcc, state } = env;\n const instructionsRaw = config.instructions;\n // AG-8: resolve the function form of `instructions` (sync or async). It is\n // resolved ONCE per run (the per-run contract documented on `AgentConfig`),\n // against a RunContext snapshot at step 0; the result is pinned as the\n // run's system-prompt prefix. A function that previously returned nothing\n // observable now actually seeds the system message.\n let instructionsText: string;\n if (typeof instructionsRaw === 'string') {\n instructionsText = instructionsRaw;\n } else {\n const instructionsCtx: RunContext<TDeps> = {\n runId: state.id,\n sessionId,\n ...(userId !== undefined ? { userId } : {}),\n agentId,\n deps: (options.deps ?? config.deps) as TDeps,\n tracer,\n signal,\n usage: usageAcc,\n stepNumber: 0,\n messages,\n state,\n };\n instructionsText = await instructionsRaw(instructionsCtx);\n }\n let systemPrompt = instructionsText;\n if (config.autoAssembleContext === true && memory !== undefined) {\n // CE-1 (opt-in): build the memory-aware 6-layer system prompt via the\n // context engine. The instructions become Layer 2; the engine prepends\n // the memory base and appends working blocks, procedural rules, skill\n // cards, the metadata counts, and (when `factsAutoRecall` is configured)\n // auto-recalled facts. Default-off keeps the explicit memory-tools\n // pattern, so the system prompt is `instructions` alone.\n const lastUser = lastUserText(seed);\n const assembled = await memory.contextEngine.assemble(memory, {\n scope: { userId: userId ?? agentId, sessionId, agentId },\n agentId,\n sessionId,\n runId: state.id,\n ...(instructionsText.length > 0 ? { agentInstructions: instructionsText } : {}),\n ...(lastUser !== undefined ? { lastUserMessage: lastUser } : {}),\n });\n systemPrompt = assembled.systemMessage.content;\n }\n if (systemPrompt.length > 0) {\n messages.push({ role: 'system', content: systemPrompt });\n }\n messages.push(...seed);\n // Mirror the assembled messages into RunState so the JSONL\n // session export and any downstream consumers see what the\n // agent saw.\n for (const m of messages) state.messages.push(m);\n}\n\n/** What the run-state bootstrap needs from the run loop's scope. */\nexport interface RunStateInitEnv<TDeps, TOutput> {\n readonly config: Pick<AgentConfig<TDeps, TOutput>, 'toolPromotion'>;\n readonly agentId: string;\n readonly sessionId: string;\n readonly userId: string | undefined;\n readonly toolDataFlowGuard: ReturnType<typeof buildDataFlowGuard> | undefined;\n}\n\n/** The run-scoped state the bootstrap hands back to the run loop. */\nexport interface InitializedRunState {\n readonly state: MutableRunState & RunState;\n readonly promotedDeferred: Set<string>;\n readonly runStartPromotions: Set<string> | undefined;\n}\n\n/**\n * Bootstrap the run's state: create (or adopt) the {@link RunState},\n * rehydrate the run-scoped security state (AG-19: the persisted coarse\n * taint summary re-seeds the enforce-mode sink gate), and restore the\n * `tool_search` promotion set (TL-7 / AG-19, with the C1\n * `'run-boundary'` snapshot).\n */\nexport function initializeRunState<TDeps, TOutput>(\n env: RunStateInitEnv<TDeps, TOutput>,\n resumed: RunState | undefined,\n): InitializedRunState {\n const { config, agentId, sessionId, userId, toolDataFlowGuard } = env;\n const baseState: RunState = resumed\n ? resumed\n : createInitialRunState({\n id: newId('run'),\n agentId,\n sessionId,\n ...(userId !== undefined ? { userId } : {}),\n });\n // Mutable view (the public RunState is `readonly` but the runtime\n // owns the lifecycle; cast to a writable shape here).\n const state = baseState as RunState as unknown as MutableRunState & RunState;\n\n // AG-19: rehydrate the run-scoped security state BEFORE any tool runs this\n // resume. Seeding the data-flow ledger with the persisted coarse taint\n // summary keeps an enforce-mode sink gated across the suspend/resume\n // boundary (the promoted-tool set is restored below, once it exists).\n if (resumed && state.taintSummary !== undefined) {\n toolDataFlowGuard?.seedLedger(state.id, state.taintSummary);\n }\n\n // WI-05: deferred tools promoted by a `tool_search` call this run.\n // Membership grows as the model discovers tools and gates which\n // deferred entries the per-step catalogue advertises. TL-7/AG-19:\n // persisted onto `RunState.promotedTools` at every exit and\n // rehydrated here, so a resumed run keeps its discoveries.\n const promotedDeferred = new Set<string>();\n // AG-19: restore deferred tools promoted by `tool_search` before the suspend\n // so they remain in the per-step catalogue after a resume.\n if (resumed && state.promotedTools !== undefined) {\n for (const name of state.promotedTools) promotedDeferred.add(name);\n }\n // C1: under `toolPromotion: 'run-boundary'` the advertised catalogue is\n // frozen to the promotions known at run start (incl. those restored\n // above), keeping the provider prompt cache byte-stable for the whole\n // run. New promotions still land in `promotedDeferred` (and persist),\n // taking effect on the next run / resume.\n const runStartPromotions =\n config.toolPromotion === 'run-boundary' ? new Set(promotedDeferred) : undefined;\n\n return { state, promotedDeferred, runStartPromotions };\n}\n"],"mappings":";;;;;;;;;;;AAuCA,eAAsB,oBACpB,KACA,UACA,MACe;CACf,MAAM,EAAE,QAAQ,SAAS,QAAQ,SAAS,WAAW,WAAW;CAChE,MAAM,EAAE,QAAQ,QAAQ,UAAU,UAAU;CAC5C,MAAM,kBAAkB,OAAO;CAM/B,IAAIA;AACJ,KAAI,OAAO,oBAAoB,SAC7B,oBAAmB;KAenB,oBAAmB,MAAM,gBAbkB;EACzC,OAAO,MAAM;EACb;EACA,GAAI,WAAW,SAAY,EAAE,QAAQ,GAAG,EAAE;EAC1C;EACA,MAAO,QAAQ,QAAQ,OAAO;EAC9B;EACA;EACA,OAAO;EACP,YAAY;EACZ;EACA;EACD,CACwD;CAE3D,IAAI,eAAe;AACnB,KAAI,OAAO,wBAAwB,QAAQ,WAAW,QAAW;EAO/D,MAAM,WAAW,aAAa,KAAK;AASnC,kBARkB,MAAM,OAAO,cAAc,SAAS,QAAQ;GAC5D,OAAO;IAAE,QAAQ,UAAU;IAAS;IAAW;IAAS;GACxD;GACA;GACA,OAAO,MAAM;GACb,GAAI,iBAAiB,SAAS,IAAI,EAAE,mBAAmB,kBAAkB,GAAG,EAAE;GAC9E,GAAI,aAAa,SAAY,EAAE,iBAAiB,UAAU,GAAG,EAAE;GAChE,CAAC,EACuB,cAAc;;AAEzC,KAAI,aAAa,SAAS,EACxB,UAAS,KAAK;EAAE,MAAM;EAAU,SAAS;EAAc,CAAC;AAE1D,UAAS,KAAK,GAAG,KAAK;AAItB,MAAK,MAAM,KAAK,SAAU,OAAM,SAAS,KAAK,EAAE;;;;;;;;;AA0BlD,SAAgB,mBACd,KACA,SACqB;CACrB,MAAM,EAAE,QAAQ,SAAS,WAAW,QAAQ,sBAAsB;CAWlE,MAAM,QAVsB,UACxB,UACA,sBAAsB;EACpB,IAAI,MAAM,MAAM;EAChB;EACA;EACA,GAAI,WAAW,SAAY,EAAE,QAAQ,GAAG,EAAE;EAC3C,CAAC;AASN,KAAI,WAAW,MAAM,iBAAiB,OACpC,oBAAmB,WAAW,MAAM,IAAI,MAAM,aAAa;CAQ7D,MAAM,mCAAmB,IAAI,KAAa;AAG1C,KAAI,WAAW,MAAM,kBAAkB,OACrC,MAAK,MAAM,QAAQ,MAAM,cAAe,kBAAiB,IAAI,KAAK;AAUpE,QAAO;EAAE;EAAO;EAAkB,oBAFhC,OAAO,kBAAkB,iBAAiB,IAAI,IAAI,iBAAiB,GAAG;EAElB"}
@@ -0,0 +1,46 @@
1
+ import { InvalidAgentConfigError, InvalidPreferredModelError } from "../errors/index.js";
2
+
3
+ //#region src/runtime/run-input.ts
4
+ function isModelHintLike(value) {
5
+ return value === "fast" || value === "balanced" || value === "smart";
6
+ }
7
+ function isModelSpecLike(value) {
8
+ if (typeof value !== "object" || value === null) return false;
9
+ const v = value;
10
+ if (typeof v.modelId === "string" && typeof v.name === "string") return true;
11
+ if (typeof v.provider === "object" && v.provider !== null && typeof v.model === "string") return true;
12
+ return false;
13
+ }
14
+ function validatePreferredModel(value) {
15
+ if (value === void 0) return;
16
+ if (isModelHintLike(value)) return;
17
+ if (isModelSpecLike(value)) return;
18
+ throw new InvalidPreferredModelError(value);
19
+ }
20
+ function isMessageObject(value) {
21
+ if (typeof value !== "object" || value === null) return false;
22
+ const role = value.role;
23
+ return role === "system" || role === "user" || role === "assistant" || role === "tool";
24
+ }
25
+ function isRunStateObject(value) {
26
+ if (typeof value !== "object" || value === null) return false;
27
+ const v = value;
28
+ return typeof v.id === "string" && typeof v.agentId === "string" && Array.isArray(v.messages) && Array.isArray(v.steps);
29
+ }
30
+ function asMessages(input) {
31
+ if (typeof input === "string") return { seed: [{
32
+ role: "user",
33
+ content: input
34
+ }] };
35
+ if (Array.isArray(input)) return { seed: [...input] };
36
+ if (isMessageObject(input)) return { seed: [input] };
37
+ if (isRunStateObject(input)) return {
38
+ seed: [],
39
+ resumed: input
40
+ };
41
+ throw new InvalidAgentConfigError(`unrecognized AgentInput shape`);
42
+ }
43
+
44
+ //#endregion
45
+ export { asMessages, isModelHintLike, isModelSpecLike, validatePreferredModel };
46
+ //# sourceMappingURL=run-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-input.js","names":[],"sources":["../../src/runtime/run-input.ts"],"sourcesContent":["/**\n * Run-input normalization and configuration validation guards for the\n * agent runtime: the `AgentInput | RunState` -> seed-message mapping,\n * the preferred-model / model-spec structural checks, and the runtime's\n * internal mutable views of `RunState`. Extracted verbatim from\n * `factory.ts` (issue #23).\n *\n * @packageDocumentation\n */\n\nimport type {\n HandoffRecord,\n Message,\n RunState,\n RunStep,\n ToolApproval,\n Usage,\n} from '@graphorin/core';\nimport { InvalidAgentConfigError, InvalidPreferredModelError } from '../errors/index.js';\nimport type { AgentInput } from '../types.js';\n\n/**\n * Internal mutable view of {@link RunState}. The public type marks\n * most fields `readonly` to guard against accidental mutation by\n * consumers; the runtime owns the lifecycle and writes through\n * this view.\n */\nexport interface MutableRunState {\n status: RunState['status'];\n currentAgentId: string;\n readonly steps: RunStep[];\n readonly messages: Message[];\n readonly pendingApprovals: ToolApproval[];\n readonly handoffs: HandoffRecord[];\n usage: Usage;\n error?: RunState['error'];\n finishedAt?: string;\n usageByModel?: RunState['usageByModel'];\n /** W-001: parked sub-agent runs (see {@link RunState.pendingSubRuns}). */\n pendingSubRuns?: RunState['pendingSubRuns'];\n}\n\nexport interface InternalRunSnapshot<TOutput> {\n output: TOutput;\n}\n\nexport function isModelHintLike(value: unknown): boolean {\n return value === 'fast' || value === 'balanced' || value === 'smart';\n}\n\nexport function isModelSpecLike(value: unknown): boolean {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.modelId === 'string' && typeof v.name === 'string') return true;\n if (typeof v.provider === 'object' && v.provider !== null && typeof v.model === 'string') {\n return true;\n }\n return false;\n}\n\nexport function validatePreferredModel(value: unknown): void {\n if (value === undefined) return;\n if (isModelHintLike(value)) return;\n if (isModelSpecLike(value)) return;\n throw new InvalidPreferredModelError(value);\n}\n\nexport function isMessageObject(value: unknown): value is Message {\n if (typeof value !== 'object' || value === null) return false;\n const role = (value as { role?: unknown }).role;\n return role === 'system' || role === 'user' || role === 'assistant' || role === 'tool';\n}\n\nexport function isRunStateObject(value: unknown): value is RunState {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.id === 'string' &&\n typeof v.agentId === 'string' &&\n Array.isArray(v.messages) &&\n Array.isArray(v.steps)\n );\n}\n\nexport function asMessages(input: AgentInput | RunState): {\n readonly seed: Message[];\n readonly resumed?: RunState;\n} {\n if (typeof input === 'string') {\n return { seed: [{ role: 'user', content: input }] };\n }\n if (Array.isArray(input)) {\n return { seed: [...input] as Message[] };\n }\n if (isMessageObject(input)) {\n return { seed: [input] };\n }\n if (isRunStateObject(input)) {\n return { seed: [], resumed: input };\n }\n throw new InvalidAgentConfigError(`unrecognized AgentInput shape`);\n}\n"],"mappings":";;;AA8CA,SAAgB,gBAAgB,OAAyB;AACvD,QAAO,UAAU,UAAU,UAAU,cAAc,UAAU;;AAG/D,SAAgB,gBAAgB,OAAyB;AACvD,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;CACxD,MAAM,IAAI;AACV,KAAI,OAAO,EAAE,YAAY,YAAY,OAAO,EAAE,SAAS,SAAU,QAAO;AACxE,KAAI,OAAO,EAAE,aAAa,YAAY,EAAE,aAAa,QAAQ,OAAO,EAAE,UAAU,SAC9E,QAAO;AAET,QAAO;;AAGT,SAAgB,uBAAuB,OAAsB;AAC3D,KAAI,UAAU,OAAW;AACzB,KAAI,gBAAgB,MAAM,CAAE;AAC5B,KAAI,gBAAgB,MAAM,CAAE;AAC5B,OAAM,IAAI,2BAA2B,MAAM;;AAG7C,SAAgB,gBAAgB,OAAkC;AAChE,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;CACxD,MAAM,OAAQ,MAA6B;AAC3C,QAAO,SAAS,YAAY,SAAS,UAAU,SAAS,eAAe,SAAS;;AAGlF,SAAgB,iBAAiB,OAAmC;AAClE,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;CACxD,MAAM,IAAI;AACV,QACE,OAAO,EAAE,OAAO,YAChB,OAAO,EAAE,YAAY,YACrB,MAAM,QAAQ,EAAE,SAAS,IACzB,MAAM,QAAQ,EAAE,MAAM;;AAI1B,SAAgB,WAAW,OAGzB;AACA,KAAI,OAAO,UAAU,SACnB,QAAO,EAAE,MAAM,CAAC;EAAE,MAAM;EAAQ,SAAS;EAAO,CAAC,EAAE;AAErD,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,EAAE,MAAM,CAAC,GAAG,MAAM,EAAe;AAE1C,KAAI,gBAAgB,MAAM,CACxB,QAAO,EAAE,MAAM,CAAC,MAAM,EAAE;AAE1B,KAAI,iBAAiB,MAAM,CACzB,QAAO;EAAE,MAAM,EAAE;EAAE,SAAS;EAAO;AAErC,OAAM,IAAI,wBAAwB,gCAAgC"}