@kindgi/agents 0.0.0-bootstrap.0 → 0.1.1

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 (240) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +131 -2
  3. package/dist/agent-turn-flow.d.ts +57 -0
  4. package/dist/agent-turn-flow.d.ts.map +1 -0
  5. package/dist/agent-turn-flow.js +172 -0
  6. package/dist/agent-turn-flow.js.map +1 -0
  7. package/dist/conversation-binding.d.ts +111 -0
  8. package/dist/conversation-binding.d.ts.map +1 -0
  9. package/dist/conversation-binding.js +4 -0
  10. package/dist/conversation-binding.js.map +1 -0
  11. package/dist/define.d.ts +180 -0
  12. package/dist/define.d.ts.map +1 -0
  13. package/dist/define.js +361 -0
  14. package/dist/define.js.map +1 -0
  15. package/dist/errors.d.ts +62 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +4 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/guardrails-gate.d.ts +169 -0
  20. package/dist/guardrails-gate.d.ts.map +1 -0
  21. package/dist/guardrails-gate.js +202 -0
  22. package/dist/guardrails-gate.js.map +1 -0
  23. package/dist/handlers/budget-check.d.ts +22 -0
  24. package/dist/handlers/budget-check.d.ts.map +1 -0
  25. package/dist/handlers/budget-check.js +109 -0
  26. package/dist/handlers/budget-check.js.map +1 -0
  27. package/dist/handlers/build-initial-messages.d.ts +17 -0
  28. package/dist/handlers/build-initial-messages.d.ts.map +1 -0
  29. package/dist/handlers/build-initial-messages.js +86 -0
  30. package/dist/handlers/build-initial-messages.js.map +1 -0
  31. package/dist/handlers/compose-result.d.ts +10 -0
  32. package/dist/handlers/compose-result.d.ts.map +1 -0
  33. package/dist/handlers/compose-result.js +79 -0
  34. package/dist/handlers/compose-result.js.map +1 -0
  35. package/dist/handlers/constants.d.ts +8 -0
  36. package/dist/handlers/constants.d.ts.map +1 -0
  37. package/dist/handlers/constants.js +10 -0
  38. package/dist/handlers/constants.js.map +1 -0
  39. package/dist/handlers/context.d.ts +210 -0
  40. package/dist/handlers/context.d.ts.map +1 -0
  41. package/dist/handlers/context.js +4 -0
  42. package/dist/handlers/context.js.map +1 -0
  43. package/dist/handlers/dispatch-tools.d.ts +15 -0
  44. package/dist/handlers/dispatch-tools.d.ts.map +1 -0
  45. package/dist/handlers/dispatch-tools.js +511 -0
  46. package/dist/handlers/dispatch-tools.js.map +1 -0
  47. package/dist/handlers/errors.d.ts +133 -0
  48. package/dist/handlers/errors.d.ts.map +1 -0
  49. package/dist/handlers/errors.js +134 -0
  50. package/dist/handlers/errors.js.map +1 -0
  51. package/dist/handlers/evaluate-guardrails.d.ts +14 -0
  52. package/dist/handlers/evaluate-guardrails.d.ts.map +1 -0
  53. package/dist/handlers/evaluate-guardrails.js +128 -0
  54. package/dist/handlers/evaluate-guardrails.js.map +1 -0
  55. package/dist/handlers/final-iteration.d.ts +7 -0
  56. package/dist/handlers/final-iteration.d.ts.map +1 -0
  57. package/dist/handlers/final-iteration.js +26 -0
  58. package/dist/handlers/final-iteration.js.map +1 -0
  59. package/dist/handlers/index.d.ts +5 -0
  60. package/dist/handlers/index.d.ts.map +1 -0
  61. package/dist/handlers/index.js +41 -0
  62. package/dist/handlers/index.js.map +1 -0
  63. package/dist/handlers/model-call.d.ts +13 -0
  64. package/dist/handlers/model-call.d.ts.map +1 -0
  65. package/dist/handlers/model-call.js +136 -0
  66. package/dist/handlers/model-call.js.map +1 -0
  67. package/dist/handlers/persist-final-message.d.ts +14 -0
  68. package/dist/handlers/persist-final-message.d.ts.map +1 -0
  69. package/dist/handlers/persist-final-message.js +54 -0
  70. package/dist/handlers/persist-final-message.js.map +1 -0
  71. package/dist/handlers/persist-provenance.d.ts +12 -0
  72. package/dist/handlers/persist-provenance.d.ts.map +1 -0
  73. package/dist/handlers/persist-provenance.js +34 -0
  74. package/dist/handlers/persist-provenance.js.map +1 -0
  75. package/dist/handlers/persist-user-message.d.ts +15 -0
  76. package/dist/handlers/persist-user-message.d.ts.map +1 -0
  77. package/dist/handlers/persist-user-message.js +59 -0
  78. package/dist/handlers/persist-user-message.js.map +1 -0
  79. package/dist/handlers/public-types.d.ts +201 -0
  80. package/dist/handlers/public-types.d.ts.map +1 -0
  81. package/dist/handlers/public-types.js +4 -0
  82. package/dist/handlers/public-types.js.map +1 -0
  83. package/dist/handlers/rehydrate.d.ts +8 -0
  84. package/dist/handlers/rehydrate.d.ts.map +1 -0
  85. package/dist/handlers/rehydrate.js +94 -0
  86. package/dist/handlers/rehydrate.js.map +1 -0
  87. package/dist/handlers/render-prompt.d.ts +12 -0
  88. package/dist/handlers/render-prompt.d.ts.map +1 -0
  89. package/dist/handlers/render-prompt.js +41 -0
  90. package/dist/handlers/render-prompt.js.map +1 -0
  91. package/dist/handlers/resolve-tools.d.ts +10 -0
  92. package/dist/handlers/resolve-tools.d.ts.map +1 -0
  93. package/dist/handlers/resolve-tools.js +55 -0
  94. package/dist/handlers/resolve-tools.js.map +1 -0
  95. package/dist/handlers/result-shape.d.ts +94 -0
  96. package/dist/handlers/result-shape.d.ts.map +1 -0
  97. package/dist/handlers/result-shape.js +19 -0
  98. package/dist/handlers/result-shape.js.map +1 -0
  99. package/dist/handlers/run-retrievals.d.ts +10 -0
  100. package/dist/handlers/run-retrievals.d.ts.map +1 -0
  101. package/dist/handlers/run-retrievals.js +64 -0
  102. package/dist/handlers/run-retrievals.js.map +1 -0
  103. package/dist/handlers/run-snapshot.d.ts +4 -0
  104. package/dist/handlers/run-snapshot.d.ts.map +1 -0
  105. package/dist/handlers/run-snapshot.js +26 -0
  106. package/dist/handlers/run-snapshot.js.map +1 -0
  107. package/dist/handlers/setup.d.ts +25 -0
  108. package/dist/handlers/setup.d.ts.map +1 -0
  109. package/dist/handlers/setup.js +231 -0
  110. package/dist/handlers/setup.js.map +1 -0
  111. package/dist/handlers/structured-output.d.ts +38 -0
  112. package/dist/handlers/structured-output.d.ts.map +1 -0
  113. package/dist/handlers/structured-output.js +89 -0
  114. package/dist/handlers/structured-output.js.map +1 -0
  115. package/dist/handlers/tool-errors.d.ts +56 -0
  116. package/dist/handlers/tool-errors.d.ts.map +1 -0
  117. package/dist/handlers/tool-errors.js +73 -0
  118. package/dist/handlers/tool-errors.js.map +1 -0
  119. package/dist/handlers/tool-hitl.d.ts +45 -0
  120. package/dist/handlers/tool-hitl.d.ts.map +1 -0
  121. package/dist/handlers/tool-hitl.js +81 -0
  122. package/dist/handlers/tool-hitl.js.map +1 -0
  123. package/dist/handlers/turn-environment.d.ts +26 -0
  124. package/dist/handlers/turn-environment.d.ts.map +1 -0
  125. package/dist/handlers/turn-environment.js +154 -0
  126. package/dist/handlers/turn-environment.js.map +1 -0
  127. package/dist/hitl-policy.d.ts +45 -0
  128. package/dist/hitl-policy.d.ts.map +1 -0
  129. package/dist/hitl-policy.js +74 -0
  130. package/dist/hitl-policy.js.map +1 -0
  131. package/dist/index.d.ts +31 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +19 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/invoke.d.ts +36 -0
  136. package/dist/invoke.d.ts.map +1 -0
  137. package/dist/invoke.js +228 -0
  138. package/dist/invoke.js.map +1 -0
  139. package/dist/migrations-dir.d.ts +11 -0
  140. package/dist/migrations-dir.d.ts.map +1 -0
  141. package/dist/migrations-dir.js +14 -0
  142. package/dist/migrations-dir.js.map +1 -0
  143. package/dist/project-run-result.d.ts +23 -0
  144. package/dist/project-run-result.d.ts.map +1 -0
  145. package/dist/project-run-result.js +116 -0
  146. package/dist/project-run-result.js.map +1 -0
  147. package/dist/prompt.d.ts +83 -0
  148. package/dist/prompt.d.ts.map +1 -0
  149. package/dist/prompt.js +119 -0
  150. package/dist/prompt.js.map +1 -0
  151. package/dist/provenance-emit.d.ts +44 -0
  152. package/dist/provenance-emit.d.ts.map +1 -0
  153. package/dist/provenance-emit.js +51 -0
  154. package/dist/provenance-emit.js.map +1 -0
  155. package/dist/registry.d.ts +38 -0
  156. package/dist/registry.d.ts.map +1 -0
  157. package/dist/registry.js +125 -0
  158. package/dist/registry.js.map +1 -0
  159. package/dist/retrieval.d.ts +47 -0
  160. package/dist/retrieval.d.ts.map +1 -0
  161. package/dist/retrieval.js +155 -0
  162. package/dist/retrieval.js.map +1 -0
  163. package/dist/run-snapshot-binding.d.ts +77 -0
  164. package/dist/run-snapshot-binding.d.ts.map +1 -0
  165. package/dist/run-snapshot-binding.js +4 -0
  166. package/dist/run-snapshot-binding.js.map +1 -0
  167. package/dist/schema.d.ts +497 -0
  168. package/dist/schema.d.ts.map +1 -0
  169. package/dist/schema.js +133 -0
  170. package/dist/schema.js.map +1 -0
  171. package/dist/streaming.d.ts +118 -0
  172. package/dist/streaming.d.ts.map +1 -0
  173. package/dist/streaming.js +17 -0
  174. package/dist/streaming.js.map +1 -0
  175. package/dist/tenant-policy.d.ts +16 -0
  176. package/dist/tenant-policy.d.ts.map +1 -0
  177. package/dist/tenant-policy.js +77 -0
  178. package/dist/tenant-policy.js.map +1 -0
  179. package/dist/types.d.ts +435 -0
  180. package/dist/types.d.ts.map +1 -0
  181. package/dist/types.js +4 -0
  182. package/dist/types.js.map +1 -0
  183. package/dist/versioning.d.ts +29 -0
  184. package/dist/versioning.d.ts.map +1 -0
  185. package/dist/versioning.js +58 -0
  186. package/dist/versioning.js.map +1 -0
  187. package/migrations/0000_sparkling_talkback.sql +18 -0
  188. package/migrations/0001_tired_warhawk.sql +16 -0
  189. package/migrations/0002_violet_ezekiel.sql +2 -0
  190. package/migrations/meta/0000_snapshot.json +172 -0
  191. package/migrations/meta/0001_snapshot.json +275 -0
  192. package/migrations/meta/0002_snapshot.json +287 -0
  193. package/migrations/meta/_journal.json +27 -0
  194. package/package.json +76 -4
  195. package/src/agent-turn-flow.ts +183 -0
  196. package/src/conversation-binding.ts +147 -0
  197. package/src/define.ts +572 -0
  198. package/src/errors.ts +80 -0
  199. package/src/guardrails-gate.ts +342 -0
  200. package/src/handlers/budget-check.ts +143 -0
  201. package/src/handlers/build-initial-messages.ts +103 -0
  202. package/src/handlers/compose-result.ts +90 -0
  203. package/src/handlers/constants.ts +10 -0
  204. package/src/handlers/context.ts +226 -0
  205. package/src/handlers/dispatch-tools.ts +633 -0
  206. package/src/handlers/errors.ts +282 -0
  207. package/src/handlers/evaluate-guardrails.ts +153 -0
  208. package/src/handlers/final-iteration.ts +30 -0
  209. package/src/handlers/index.ts +63 -0
  210. package/src/handlers/model-call.ts +151 -0
  211. package/src/handlers/persist-final-message.ts +67 -0
  212. package/src/handlers/persist-provenance.ts +39 -0
  213. package/src/handlers/persist-user-message.ts +70 -0
  214. package/src/handlers/public-types.ts +209 -0
  215. package/src/handlers/rehydrate.ts +161 -0
  216. package/src/handlers/render-prompt.ts +46 -0
  217. package/src/handlers/resolve-tools.ts +68 -0
  218. package/src/handlers/result-shape.ts +113 -0
  219. package/src/handlers/run-retrievals.ts +77 -0
  220. package/src/handlers/run-snapshot.ts +44 -0
  221. package/src/handlers/setup.ts +269 -0
  222. package/src/handlers/structured-output.ts +117 -0
  223. package/src/handlers/tool-errors.ts +122 -0
  224. package/src/handlers/tool-hitl.ts +126 -0
  225. package/src/handlers/turn-environment.ts +191 -0
  226. package/src/hitl-policy.ts +128 -0
  227. package/src/index.ts +154 -0
  228. package/src/invoke.ts +299 -0
  229. package/src/migrations-dir.ts +17 -0
  230. package/src/project-run-result.ts +131 -0
  231. package/src/prompt.ts +185 -0
  232. package/src/provenance-emit.ts +100 -0
  233. package/src/registry.ts +164 -0
  234. package/src/retrieval.ts +219 -0
  235. package/src/run-snapshot-binding.ts +87 -0
  236. package/src/schema.ts +154 -0
  237. package/src/streaming.ts +153 -0
  238. package/src/tenant-policy.ts +78 -0
  239. package/src/types.ts +453 -0
  240. package/src/versioning.ts +77 -0
@@ -0,0 +1,282 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { EvaluationResult } from '@kindgi/guardrails';
5
+ import type { PolicyKind } from '@kindgi/policy-contract';
6
+
7
+ import type { AgentError } from '../errors.js';
8
+ import type {
9
+ GuardrailViolationError,
10
+ HitlRequiredError,
11
+ UnresolvedGuardrailError,
12
+ } from '../guardrails-gate.js';
13
+
14
+ /** Structured error returned to the caller when an agent turn fails. */
15
+ export type InvokeAgentError =
16
+ | AgentError
17
+ | UnresolvedToolError
18
+ | ToolVersionUnresolvableError
19
+ | CapabilityRoutingError
20
+ | ModelInvocationError
21
+ | ToolInvocationError
22
+ | BudgetExceededError
23
+ | AgentTurnAbortedError
24
+ | HitlRequiredError
25
+ | GuardrailViolationError
26
+ | UnresolvedGuardrailError
27
+ | OutputSchemaViolationError
28
+ | TenantPolicyUnavailableError;
29
+
30
+ /**
31
+ * A tenant policy the turn must apply couldn't be: its spec doesn't
32
+ * validate, or the policy registry failed. The turn doesn't run without
33
+ * it — skipping a tenant's `hitl` policy would skip its approvals.
34
+ */
35
+ export interface TenantPolicyUnavailableError {
36
+ readonly code: 'tenant-policy-unavailable';
37
+ readonly message: string;
38
+ readonly policyKind: PolicyKind;
39
+ }
40
+
41
+ export interface UnresolvedToolError {
42
+ readonly code: 'unresolved-tool';
43
+ readonly message: string;
44
+ readonly toolId: string;
45
+ /**
46
+ * Failed tool calls sent back to the model this turn before this one
47
+ * ended it (see `Agent.toolErrors`). Present when the failure is a
48
+ * kind the turn retries and its retries ran out.
49
+ */
50
+ readonly toolRetries?: number;
51
+ }
52
+
53
+ /**
54
+ * The agent references a tool that is registered but whose semver range
55
+ * does not match any registered version, or whose range is grammatically
56
+ * broken. Distinct from `unresolved-tool` (no id registered).
57
+ */
58
+ export interface ToolVersionUnresolvableError {
59
+ readonly code: 'tool-version-unresolvable';
60
+ readonly message: string;
61
+ readonly toolId: string;
62
+ readonly requestedRange: string;
63
+ readonly availableVersions?: readonly string[];
64
+ }
65
+
66
+ export interface CapabilityRoutingError {
67
+ readonly code: 'capability-routing-failed';
68
+ readonly message: string;
69
+ readonly cause: unknown;
70
+ }
71
+
72
+ export interface ModelInvocationError {
73
+ readonly code: 'model-invocation-failed';
74
+ readonly message: string;
75
+ readonly cause: unknown;
76
+ }
77
+
78
+ export interface ToolInvocationError {
79
+ readonly code: 'tool-invocation-failed';
80
+ readonly message: string;
81
+ readonly toolId: string;
82
+ readonly cause: unknown;
83
+ /**
84
+ * When the inner failure was an AJV input-schema validation, the
85
+ * validator's `errors[]` array. Hoisted from `cause` to the top
86
+ * level so it survives `toWireError` (which filters `cause` to
87
+ * avoid Error-instance / cycle serialization hazards). Consumers
88
+ * inspecting *which* field failed *how* read this; `fromWire` in
89
+ * `@kindgi/client` projects it back onto the client-side error.
90
+ */
91
+ readonly validationIssues?: readonly Readonly<Record<string, unknown>>[];
92
+ /**
93
+ * The exact payload the model produced that failed validation.
94
+ * Included so authors debugging LLM-tool-arg drift can see whether
95
+ * the model sent `{}`, `{ location: "" }`, `{ city: "…" }`
96
+ * (wrong field name), etc. without instrumenting their handler.
97
+ * Size-capped at ~4KiB stringified — larger payloads truncate
98
+ * with a trailing `…`; sensitive tools should sanitize inputs
99
+ * upstream.
100
+ */
101
+ readonly receivedInput?: unknown;
102
+ /**
103
+ * Failed tool calls sent back to the model this turn before this one
104
+ * ended it (see `Agent.toolErrors`). Present when the failure is a
105
+ * kind the turn retries and its retries ran out.
106
+ */
107
+ readonly toolRetries?: number;
108
+ }
109
+
110
+ export interface BudgetExceededError {
111
+ readonly code: 'budget-exceeded';
112
+ readonly message: string;
113
+ readonly kind: 'steps' | 'cost' | 'wall-time';
114
+ readonly limit: number;
115
+ readonly observed: number;
116
+ }
117
+
118
+ /**
119
+ * The agent declares an `output` schema and its final answer still
120
+ * didn't fit after the allowed repairs. `errors` lists the last
121
+ * attempt's problems; `attempts` counts the answers checked.
122
+ */
123
+ export interface OutputSchemaViolationError {
124
+ readonly code: 'output-schema-violation';
125
+ readonly message: string;
126
+ readonly errors: readonly string[];
127
+ readonly attempts: number;
128
+ }
129
+
130
+ export interface AgentTurnAbortedError {
131
+ readonly code: 'agent-turn-aborted';
132
+ readonly message: string;
133
+ readonly reason: 'external' | 'timeout';
134
+ }
135
+
136
+ /**
137
+ * Thrown by a node handler when a precondition fails or a runtime step
138
+ * short-circuits with a structured error. The kernel captures
139
+ * `Error.message` into `step.failed.payload.message`; we serialize the
140
+ * full structured error as JSON so `projectRunResult` can recover the
141
+ * discriminated union shape on the way out.
142
+ *
143
+ * Deliberately narrowed to `Error` subclass so `cause` becomes a plain
144
+ * property (JSON-round-trippable) instead of the native ErrorOptions
145
+ * behavior.
146
+ */
147
+ export class AgentTurnFailure extends Error {
148
+ readonly payload: InvokeAgentError;
149
+ constructor(payload: InvokeAgentError) {
150
+ super(serializeError(payload));
151
+ this.name = 'AgentTurnFailure';
152
+ this.payload = payload;
153
+ }
154
+ }
155
+
156
+ const FAILURE_SENTINEL = '__agent_turn_failure__' as const;
157
+
158
+ interface SerializedFailure {
159
+ readonly [FAILURE_SENTINEL]: true;
160
+ readonly error: InvokeAgentError;
161
+ }
162
+
163
+ /**
164
+ * Encode an `InvokeAgentError` as a JSON string carried in an Error's
165
+ * message. The sentinel field lets `parseFailureMessage` distinguish
166
+ * our structured failures from other error messages that happen to be
167
+ * JSON-shaped.
168
+ *
169
+ * `cause` fields on inner errors are downgraded to their message form
170
+ * so the payload is JSON-safe.
171
+ */
172
+ function serializeError(error: InvokeAgentError): string {
173
+ return JSON.stringify(
174
+ {
175
+ [FAILURE_SENTINEL]: true as const,
176
+ error: stripCauseFunctions(error),
177
+ } satisfies SerializedFailure,
178
+ (_key, value) => {
179
+ if (value instanceof Error) {
180
+ return { code: (value as { code?: string }).code, message: value.message };
181
+ }
182
+ return value;
183
+ },
184
+ );
185
+ }
186
+
187
+ function stripCauseFunctions(error: InvokeAgentError): InvokeAgentError {
188
+ // JSON.stringify replacer above handles Error instances; nothing more
189
+ // to do here. A pass-through hook for error variants that embed
190
+ // callbacks.
191
+ return error;
192
+ }
193
+
194
+ /**
195
+ * Attempt to reconstruct a structured `InvokeAgentError` from a run's
196
+ * `failureMessage`. Returns `undefined` when the message is not one of
197
+ * ours (e.g. a raw kernel-level exception message).
198
+ */
199
+ export function parseFailureMessage(raw: string | undefined): InvokeAgentError | undefined {
200
+ if (raw === undefined || raw.length === 0) return undefined;
201
+ // Fast path: the raw message IS the single serialized failure (one
202
+ // step.failed for the whole turn, which is the common case).
203
+ const whole = tryParseChunk(raw);
204
+ if (whole !== undefined) return whole;
205
+ // Fallback: the kernel joins multiple step.failed messages with '; '.
206
+ // Recover by extracting balanced-brace substrings and trying each.
207
+ for (const chunk of extractJsonCandidates(raw)) {
208
+ const parsed = tryParseChunk(chunk);
209
+ if (parsed !== undefined) return parsed;
210
+ }
211
+ return undefined;
212
+ }
213
+
214
+ /**
215
+ * Extract substrings that look like well-formed JSON objects by
216
+ * scanning for balanced braces. Only single-level scan — inner strings
217
+ * are respected. Cheap parser used for the rare multi-failure case.
218
+ */
219
+ function extractJsonCandidates(raw: string): string[] {
220
+ const out: string[] = [];
221
+ let depth = 0;
222
+ let start = -1;
223
+ let inString = false;
224
+ let escaped = false;
225
+ for (let i = 0; i < raw.length; i++) {
226
+ const ch = raw[i];
227
+ if (escaped) {
228
+ escaped = false;
229
+ continue;
230
+ }
231
+ if (ch === '\\') {
232
+ escaped = true;
233
+ continue;
234
+ }
235
+ if (ch === '"') {
236
+ inString = !inString;
237
+ continue;
238
+ }
239
+ if (inString) continue;
240
+ if (ch === '{') {
241
+ if (depth === 0) start = i;
242
+ depth += 1;
243
+ } else if (ch === '}') {
244
+ depth -= 1;
245
+ if (depth === 0 && start >= 0) {
246
+ out.push(raw.slice(start, i + 1));
247
+ start = -1;
248
+ }
249
+ }
250
+ }
251
+ return out;
252
+ }
253
+
254
+ function tryParseChunk(chunk: string): InvokeAgentError | undefined {
255
+ const trimmed = chunk.trim();
256
+ if (!trimmed.startsWith('{')) return undefined;
257
+ try {
258
+ const doc = JSON.parse(trimmed) as SerializedFailure | Record<string, unknown>;
259
+ if (
260
+ typeof doc === 'object' &&
261
+ doc !== null &&
262
+ (doc as SerializedFailure)[FAILURE_SENTINEL] === true
263
+ ) {
264
+ return (doc as SerializedFailure).error;
265
+ }
266
+ } catch {
267
+ // Not our JSON — probably a raw JS Error.message. Fall through.
268
+ }
269
+ return undefined;
270
+ }
271
+
272
+ /** Helper for handlers to short-circuit with a structured error. */
273
+ export function throwAgentTurnFailure(error: InvokeAgentError): never {
274
+ throw new AgentTurnFailure(error);
275
+ }
276
+
277
+ /**
278
+ * Placeholder to satisfy TS-strict "declared but unused" on
279
+ * `EvaluationResult` in ambient imports elsewhere; the type is used
280
+ * transitively via `GuardrailViolationError`.
281
+ */
282
+ export type _EvaluationResultAlias = EvaluationResult;
@@ -0,0 +1,153 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { NodeHandler } from '@kindgi/handler';
5
+ import type { Timestamp } from '@kindgi/types';
6
+
7
+ import {
8
+ buildRunTrace,
9
+ categorizeOutcomes,
10
+ describeBlockingViolations,
11
+ evaluateGate,
12
+ } from '../guardrails-gate.js';
13
+ import { emitTurnEvent } from '../streaming.js';
14
+ import type { ConversationMessage } from '../types.js';
15
+
16
+ import type { TurnContext } from './context.js';
17
+ import { throwAgentTurnFailure } from './errors.js';
18
+ import { finalIteration } from './final-iteration.js';
19
+ import { parseJsonAnswer } from './structured-output.js';
20
+
21
+ /**
22
+ * Guardrail gate on the turn's final response, before it is stored.
23
+ * Receives the agent loop's output. A failed guardrail whose action is
24
+ * `halt` throws, so the run fails and the response never reaches the
25
+ * conversation; failures with any other action are stashed on `ctx` for
26
+ * `compose-result` to surface in `AgentTurnResult.violations`. Every
27
+ * failure emits a `guardrail.violated` event. Also records the
28
+ * response's `model-output` provenance node, which the guardrail checks
29
+ * link to.
30
+ */
31
+ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
32
+ return async (input: unknown, kctx) => {
33
+ if (ctx.guardrails === undefined) {
34
+ throwAgentTurnFailure({
35
+ code: 'model-invocation-failed',
36
+ message: 'evaluate-guardrails invoked before setup completed',
37
+ cause: null,
38
+ });
39
+ }
40
+ const final = finalIteration(input, 'evaluate-guardrails');
41
+ const evaluatedAt = new Date().toISOString() as Timestamp;
42
+ // The response as it would be stored; not persisted yet.
43
+ const response: ConversationMessage = {
44
+ sequence: -1,
45
+ role: 'agent',
46
+ content: final.message.content ?? '',
47
+ createdAt: evaluatedAt,
48
+ actor: ctx.input.agent.id,
49
+ };
50
+
51
+ if (ctx.provenance !== undefined) {
52
+ ctx.provenance.addNode({
53
+ id: `model-output:${ctx.usage.steps}`,
54
+ kind: 'model-output',
55
+ timestamp: evaluatedAt,
56
+ modelVersion: `${final.provider.id}/${final.provider.model}`,
57
+ });
58
+ ctx.provenance.addEdge({
59
+ from: `model-output:${ctx.usage.steps}`,
60
+ to: `model-call:${ctx.usage.steps}`,
61
+ kind: 'produced',
62
+ });
63
+ }
64
+
65
+ const finalUsage = {
66
+ steps: ctx.usage.steps,
67
+ promptTokens: ctx.usage.promptTokens,
68
+ completionTokens: ctx.usage.completionTokens,
69
+ totalCostUsd: ctx.usage.totalCostUsd,
70
+ durationMs: Date.now() - ctx.startedAt,
71
+ };
72
+ const structured =
73
+ ctx.input.agent.output !== undefined && !kctx.dryRun
74
+ ? parseJsonAnswer(final.message.content)
75
+ : undefined;
76
+ const trace = buildRunTrace({
77
+ runId: kctx.runId,
78
+ tenantId: ctx.input.tenantId,
79
+ projectId: ctx.input.projectId,
80
+ conversationId: ctx.input.conversationId,
81
+ turnNumber: (ctx.conversation?.turnCount ?? 0) + 1,
82
+ agent: ctx.input.agent,
83
+ userMessage: ctx.input.userMessage,
84
+ ...(ctx.input.input !== undefined && { stepInput: ctx.input.input }),
85
+ ...(structured?.kind === 'ok' && { structuredOutput: structured.value }),
86
+ appended: ctx.appended,
87
+ finalResponse: response,
88
+ usage: finalUsage,
89
+ });
90
+ const outcomes = await evaluateGate(
91
+ ctx.guardrails,
92
+ trace,
93
+ ctx.bindings,
94
+ ctx.tenantPolicy,
95
+ ctx.turnAbort.signal,
96
+ );
97
+ const categorized = categorizeOutcomes(outcomes);
98
+
99
+ const allViolations = [...categorized.blocking, ...categorized.warnings, ...categorized.other];
100
+ for (const v of allViolations) {
101
+ await emitTurnEvent(ctx.bindings.onEvent, {
102
+ kind: 'guardrail.violated',
103
+ action: v.action,
104
+ severity: v.severity,
105
+ guardrailId: v.guardrailId,
106
+ ...(v.result.reason !== undefined && { reason: v.result.reason }),
107
+ });
108
+ if (ctx.provenance !== undefined) {
109
+ ctx.provenance.addNode({
110
+ id: `guardrail-check:${v.guardrailId}`,
111
+ kind: 'guardrail-check',
112
+ timestamp: v.at,
113
+ attributes: {
114
+ guardrailId: v.guardrailId,
115
+ action: v.action,
116
+ severity: v.severity,
117
+ passed: false,
118
+ ...(v.result.reason !== undefined && { reason: v.result.reason }),
119
+ },
120
+ });
121
+ ctx.provenance.addEdge({
122
+ from: `guardrail-check:${v.guardrailId}`,
123
+ to: `model-output:${ctx.usage.steps}`,
124
+ kind: 'influenced-by',
125
+ });
126
+ }
127
+ }
128
+
129
+ if (categorized.blocking.length > 0) {
130
+ const message = describeBlockingViolations(categorized.blocking);
131
+ await emitTurnEvent(ctx.bindings.onEvent, {
132
+ kind: 'turn.failed',
133
+ conversationId: ctx.input.conversationId,
134
+ errorCode: 'guardrail-violation',
135
+ message,
136
+ });
137
+ throwAgentTurnFailure({
138
+ code: 'guardrail-violation',
139
+ message,
140
+ violations: categorized.blocking,
141
+ evaluationErrors: categorized.errors,
142
+ });
143
+ }
144
+
145
+ ctx.nonBlockingViolations = [...categorized.warnings, ...categorized.other];
146
+
147
+ return {
148
+ blocking: categorized.blocking.length,
149
+ warnings: categorized.warnings.length,
150
+ other: categorized.other.length,
151
+ };
152
+ };
153
+ }
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { LoopNodeOutput } from '@kindgi/handler';
5
+
6
+ import type { AgentTurnIterationOutput } from './context.js';
7
+ import { throwAgentTurnFailure } from './errors.js';
8
+
9
+ /**
10
+ * The agent loop's final iteration — the response the turn ends with.
11
+ * `nodeName` names the reading node in the failure message.
12
+ */
13
+ export function finalIteration(loopOutput: unknown, nodeName: string): AgentTurnIterationOutput {
14
+ const output = loopOutput as LoopNodeOutput<AgentTurnIterationOutput> | undefined;
15
+ if (output === undefined) {
16
+ throwAgentTurnFailure({
17
+ code: 'model-invocation-failed',
18
+ message: `${nodeName} received undefined loop output`,
19
+ cause: null,
20
+ });
21
+ }
22
+ if (output.finalOutput === undefined) {
23
+ throwAgentTurnFailure({
24
+ code: 'model-invocation-failed',
25
+ message: `${nodeName} received loop output without finalOutput`,
26
+ cause: null,
27
+ });
28
+ }
29
+ return output.finalOutput;
30
+ }
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { HandlerRegistry, NodeHandler } from '@kindgi/handler';
5
+ import type { NodeId } from '@kindgi/types';
6
+
7
+ import {
8
+ AGENT_LOOP_NODE,
9
+ BUDGET_CHECK_NODE,
10
+ BUILD_INITIAL_MESSAGES_NODE,
11
+ COMPOSE_RESULT_NODE,
12
+ DISPATCH_TOOLS_NODE,
13
+ EVALUATE_GUARDRAILS_NODE,
14
+ MODEL_CALL_NODE,
15
+ PERSIST_FINAL_MESSAGE_NODE,
16
+ PERSIST_PROVENANCE_NODE,
17
+ PERSIST_USER_MESSAGE_NODE,
18
+ RENDER_PROMPT_NODE,
19
+ RUN_RETRIEVALS_NODE,
20
+ SETUP_NODE,
21
+ } from '../agent-turn-flow.js';
22
+
23
+ import { buildBudgetCheckHandler } from './budget-check.js';
24
+ import { buildBuildInitialMessagesHandler } from './build-initial-messages.js';
25
+ import { buildComposeResultHandler } from './compose-result.js';
26
+ import type { TurnContext } from './context.js';
27
+ import { buildDispatchToolsHandler } from './dispatch-tools.js';
28
+ import { buildEvaluateGuardrailsHandler } from './evaluate-guardrails.js';
29
+ import { buildModelCallHandler } from './model-call.js';
30
+ import { buildPersistFinalMessageHandler } from './persist-final-message.js';
31
+ import { buildPersistProvenanceHandler } from './persist-provenance.js';
32
+ import { buildPersistUserMessageHandler } from './persist-user-message.js';
33
+ import { buildRenderPromptHandler } from './render-prompt.js';
34
+ import { buildRunRetrievalsHandler } from './run-retrievals.js';
35
+ import { buildSetupHandler } from './setup.js';
36
+
37
+ // `agent-loop` is a kernel loop node — the executor dispatches it
38
+ // internally; only body-node handlers are registered.
39
+ export function buildHandlers(ctx: TurnContext): HandlerRegistry {
40
+ const entries: [NodeId, NodeHandler][] = [
41
+ [SETUP_NODE as NodeId, buildSetupHandler(ctx)],
42
+ [RENDER_PROMPT_NODE as NodeId, buildRenderPromptHandler(ctx)],
43
+ [PERSIST_USER_MESSAGE_NODE as NodeId, buildPersistUserMessageHandler(ctx)],
44
+ [RUN_RETRIEVALS_NODE as NodeId, buildRunRetrievalsHandler(ctx)],
45
+ [BUILD_INITIAL_MESSAGES_NODE as NodeId, buildBuildInitialMessagesHandler(ctx)],
46
+ [MODEL_CALL_NODE as NodeId, buildModelCallHandler(ctx)],
47
+ [DISPATCH_TOOLS_NODE as NodeId, buildDispatchToolsHandler(ctx)],
48
+ [BUDGET_CHECK_NODE as NodeId, buildBudgetCheckHandler(ctx)],
49
+ [PERSIST_FINAL_MESSAGE_NODE as NodeId, buildPersistFinalMessageHandler(ctx)],
50
+ [EVALUATE_GUARDRAILS_NODE as NodeId, buildEvaluateGuardrailsHandler(ctx)],
51
+ [PERSIST_PROVENANCE_NODE as NodeId, buildPersistProvenanceHandler(ctx)],
52
+ [COMPOSE_RESULT_NODE as NodeId, buildComposeResultHandler(ctx)],
53
+ ];
54
+ // Assert `agent-loop` isn't accidentally registered — the runtime's
55
+ // flow executor rejects handlers for internal-dispatch nodes at load
56
+ // time. Sanity check for regressions.
57
+ if (entries.some(([id]) => id === (AGENT_LOOP_NODE as NodeId))) {
58
+ throw new Error('agent-loop must not have a handler — it is dispatched by the kernel');
59
+ }
60
+ return new Map(entries);
61
+ }
62
+
63
+ export type { TurnContext };
@@ -0,0 +1,151 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { ModelCallInput, ModelMessage } from '@kindgi/capabilities';
5
+ import type { NodeHandler } from '@kindgi/handler';
6
+
7
+ import { emitTurnEvent } from '../streaming.js';
8
+
9
+ import type { AgentTurnIterationOutput, TurnContext } from './context.js';
10
+ import { throwAgentTurnFailure } from './errors.js';
11
+
12
+ /**
13
+ * Loop-body node #1. Invoke the model with the current
14
+ * `nextMessages` accumulator. Emits `model.call.started` +
15
+ * `model.call.completed`, records provenance for the model-call node,
16
+ * and accumulates usage on `ctx.usage`.
17
+ *
18
+ * Output is a partial `AgentTurnIterationOutput` — `dispatch-tools`
19
+ * receives it and finishes composing the iteration output.
20
+ */
21
+ export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
22
+ return async (input: unknown, kctx) => {
23
+ if (ctx.provider === undefined || ctx.model === undefined || ctx.tools === undefined) {
24
+ throwAgentTurnFailure({
25
+ code: 'model-invocation-failed',
26
+ message: 'model-call invoked before setup completed',
27
+ cause: null,
28
+ });
29
+ }
30
+ const shaped = input as { readonly nextMessages?: readonly ModelMessage[] } | undefined;
31
+ const nextMessages: readonly ModelMessage[] = shaped?.nextMessages ?? [];
32
+
33
+ ctx.usage.steps += 1;
34
+
35
+ await emitTurnEvent(ctx.bindings.onEvent, {
36
+ kind: 'model.call.started',
37
+ step: ctx.usage.steps,
38
+ providerId: ctx.provider.metadata.id,
39
+ model: ctx.model.name,
40
+ });
41
+
42
+ let callResult: Awaited<ReturnType<typeof ctx.provider.invoke>>;
43
+ if (kctx.dryRun) {
44
+ // Dry-run: skip the network call. Return a schema-conformant
45
+ // mock indicating the model would have terminated cleanly. The
46
+ // mock requests no tools, so a dry run does not preview tool
47
+ // planning.
48
+ callResult = {
49
+ finishReason: 'stop',
50
+ message: { role: 'assistant', content: '[dry-run: model call skipped]' },
51
+ usage: { promptTokens: 0, completionTokens: 0 },
52
+ costUsd: 0,
53
+ durationMs: 0,
54
+ provider: { id: ctx.provider.metadata.id, model: ctx.model.name },
55
+ };
56
+ } else {
57
+ const callInput: ModelCallInput = {
58
+ model: ctx.model.name,
59
+ messages: nextMessages,
60
+ ...(ctx.tools.definitions.length > 0 && {
61
+ tools: ctx.tools.definitions,
62
+ }),
63
+ abortSignal: ctx.turnAbort.signal,
64
+ };
65
+
66
+ try {
67
+ callResult = await ctx.provider.invoke(callInput);
68
+ } catch (cause) {
69
+ if (ctx.turnAbort.signal.aborted) {
70
+ throwAgentTurnFailure({
71
+ code: 'agent-turn-aborted',
72
+ message: `Agent turn aborted: ${cause instanceof Error ? cause.message : String(cause)}`,
73
+ reason: ctx.abortReason ?? 'timeout',
74
+ });
75
+ }
76
+ // The provider's own words (a 401's "invalid x-api-key", a 429)
77
+ // are what the caller needs; they're in `cause` too, but callers
78
+ // show `message`.
79
+ throwAgentTurnFailure({
80
+ code: 'model-invocation-failed',
81
+ message: `Model call to ${ctx.provider.metadata.id} (${ctx.model.name}) failed: ${describeCause(cause)}`,
82
+ cause,
83
+ });
84
+ }
85
+ }
86
+
87
+ ctx.usage.promptTokens += callResult.usage.promptTokens;
88
+ ctx.usage.completionTokens += callResult.usage.completionTokens;
89
+ ctx.usage.totalCostUsd += callResult.costUsd;
90
+ ctx.lastProvider = callResult.provider;
91
+
92
+ await emitTurnEvent(ctx.bindings.onEvent, {
93
+ kind: 'model.call.completed',
94
+ step: ctx.usage.steps,
95
+ finishReason: callResult.finishReason,
96
+ promptTokens: callResult.usage.promptTokens,
97
+ completionTokens: callResult.usage.completionTokens,
98
+ costUsd: callResult.costUsd,
99
+ durationMs: callResult.durationMs,
100
+ });
101
+
102
+ if (ctx.provenance !== undefined && ctx.userMessage !== undefined) {
103
+ const modelCallNodeId = `model-call:${ctx.usage.steps}`;
104
+ ctx.provenance.addNode({
105
+ id: modelCallNodeId,
106
+ kind: 'model-call',
107
+ timestamp: new Date().toISOString() as never,
108
+ modelVersion: `${callResult.provider.id}/${callResult.provider.model}`,
109
+ attributes: {
110
+ step: ctx.usage.steps,
111
+ promptTokens: callResult.usage.promptTokens,
112
+ completionTokens: callResult.usage.completionTokens,
113
+ costUsd: callResult.costUsd,
114
+ finishReason: callResult.finishReason,
115
+ },
116
+ });
117
+ ctx.provenance.addEdge({
118
+ from: modelCallNodeId,
119
+ to: `input:${ctx.userMessage.sequence}`,
120
+ kind: 'caused-by',
121
+ });
122
+ }
123
+
124
+ // Partial iteration output — `dispatch-tools` finishes it.
125
+ const partial: Omit<AgentTurnIterationOutput, 'iterationAppended' | 'finishedTurn'> & {
126
+ readonly step: number;
127
+ } = {
128
+ step: ctx.usage.steps,
129
+ finishReason: callResult.finishReason,
130
+ message: callResult.message,
131
+ iterationUsage: {
132
+ promptTokens: callResult.usage.promptTokens,
133
+ completionTokens: callResult.usage.completionTokens,
134
+ costUsd: callResult.costUsd,
135
+ },
136
+ provider: callResult.provider,
137
+ nextMessages,
138
+ };
139
+ return partial;
140
+ };
141
+ }
142
+
143
+ /** Longest cause text a failure message carries. */
144
+ const MAX_CAUSE_CHARS = 500;
145
+
146
+ /** A thrown value's message on one line, at most `MAX_CAUSE_CHARS` long. */
147
+ function describeCause(cause: unknown): string {
148
+ const text = (cause instanceof Error ? cause.message : String(cause)).replace(/\s+/g, ' ').trim();
149
+ if (text.length === 0) return cause instanceof Error ? cause.name : 'no detail';
150
+ return text.length > MAX_CAUSE_CHARS ? `${text.slice(0, MAX_CAUSE_CHARS - 1)}…` : text;
151
+ }