@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,342 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { ProviderRegistry, TenantPolicy } from '@kindgi/capabilities';
5
+ import type { ComplianceProvider } from '@kindgi/compliance';
6
+ import {
7
+ type CheckRegistry,
8
+ type EvaluationBindings,
9
+ type EvaluationOutcome,
10
+ type EvaluationResult,
11
+ type Guardrail,
12
+ type ModelCallRecord,
13
+ type RunTrace,
14
+ type ToolCallRecord,
15
+ type ToolResultRecord,
16
+ evaluateAll,
17
+ } from '@kindgi/guardrails';
18
+ import type { AgentId, GuardrailId, ProjectId, Result, RunId, TenantId } from '@kindgi/types';
19
+
20
+ import type { Agent, ConversationId, ConversationMessage } from './types.js';
21
+
22
+ /**
23
+ * Turn usage snapshot passed into `buildRunTrace`. Local mirror of
24
+ * `AgentTurnUsage` (handlers/result-shape.ts) — kept here to avoid a
25
+ * circular import.
26
+ */
27
+ interface TurnUsageSnapshot {
28
+ readonly steps: number;
29
+ readonly promptTokens: number;
30
+ readonly completionTokens: number;
31
+ readonly totalCostUsd: number;
32
+ readonly durationMs: number;
33
+ }
34
+
35
+ /**
36
+ * Bindings the guardrail gate consumes. All optional — if `guardrails`
37
+ * is empty or `checks` is undefined, `evaluateGate` is a no-op. An
38
+ * agent that references a guardrail id missing from `guardrails` fails
39
+ * its turn with `unresolved-guardrail`.
40
+ */
41
+ export interface GuardrailsBindings {
42
+ /** Full guardrail definitions available for this run. Indexed by id. */
43
+ readonly guardrails?: readonly Guardrail[];
44
+ /** Check registry with built-ins + any adapter-registered custom checks. */
45
+ readonly checks?: CheckRegistry;
46
+ /** Optional compliance sink — every failed check emits an evidence record. */
47
+ readonly compliance?: ComplianceProvider;
48
+ /**
49
+ * Provider registry for llm-judge guardrails; the judge model is
50
+ * routed through it under the turn's tenant policy. If absent,
51
+ * llm-judge guardrails error with `judge-missing`; if no provider
52
+ * satisfies the judge's capability, with `judge-routing-failed`.
53
+ */
54
+ readonly providerRegistry?: ProviderRegistry;
55
+ }
56
+
57
+ /**
58
+ * Materialize a `RunTrace` from an agent turn's captured state. The
59
+ * guardrail engine reads only the trace — it doesn't touch memory,
60
+ * kernel journal, or provenance directly (keeps checks pure and
61
+ * dependency-free).
62
+ */
63
+ export function buildRunTrace(input: {
64
+ /** The kernel run of the turn. */
65
+ readonly runId: RunId;
66
+ readonly tenantId: TenantId;
67
+ /** Lets the engine record compliance evidence for a failed check. */
68
+ readonly projectId: ProjectId;
69
+ readonly conversationId: ConversationId;
70
+ /** 1-based number of this turn in the conversation. */
71
+ readonly turnNumber: number;
72
+ readonly agent: Agent;
73
+ /** The user message that started the turn. */
74
+ readonly userMessage: string;
75
+ /** The turn's structured input (`InvokeAgentInput.input`), when it has one. */
76
+ readonly stepInput?: unknown;
77
+ /** The parsed answer, when the agent declares `output`. */
78
+ readonly structuredOutput?: unknown;
79
+ readonly appended: readonly ConversationMessage[];
80
+ readonly finalResponse: ConversationMessage;
81
+ readonly usage: TurnUsageSnapshot;
82
+ }): RunTrace {
83
+ const toolCalls: ToolCallRecord[] = [];
84
+ const toolResults: ToolResultRecord[] = [];
85
+ const modelCalls: ModelCallRecord[] = [];
86
+
87
+ for (const msg of input.appended) {
88
+ if (msg.role === 'agent' && typeof msg.content === 'object' && msg.content !== null) {
89
+ const structured = msg.content as {
90
+ readonly toolCalls?: readonly {
91
+ readonly id: string;
92
+ readonly name: string;
93
+ readonly arguments: Readonly<Record<string, unknown>>;
94
+ }[];
95
+ };
96
+ if (structured.toolCalls !== undefined) {
97
+ for (const call of structured.toolCalls) {
98
+ toolCalls.push({
99
+ toolId: call.name as never,
100
+ toolName: call.name,
101
+ arguments: call.arguments,
102
+ at: msg.createdAt,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ if (msg.role === 'tool' && msg.toolCall !== undefined) {
108
+ toolResults.push({
109
+ toolCallId: msg.toolCall.invocationId,
110
+ output: msg.content,
111
+ at: msg.createdAt,
112
+ });
113
+ }
114
+ }
115
+
116
+ // Approximate a model-call record for cost/latency guardrails — a
117
+ // more granular per-call breakdown would need an observability
118
+ // hook.
119
+ modelCalls.push({
120
+ providerId: 'runtime.picked',
121
+ model: 'runtime.picked',
122
+ promptTokens: input.usage.promptTokens,
123
+ completionTokens: input.usage.completionTokens,
124
+ at: input.finalResponse.createdAt,
125
+ });
126
+
127
+ const output =
128
+ typeof input.finalResponse.content === 'string'
129
+ ? input.finalResponse.content
130
+ : JSON.stringify(input.finalResponse.content);
131
+
132
+ return {
133
+ runId: input.runId,
134
+ tenantId: input.tenantId,
135
+ projectId: input.projectId,
136
+ agentId: input.agent.id as unknown as AgentId,
137
+ output,
138
+ toolCalls,
139
+ toolResults,
140
+ modelCalls,
141
+ mode: 'runtime',
142
+ userInput: input.userMessage,
143
+ conversationId: input.conversationId as unknown as string,
144
+ turnNumber: input.turnNumber,
145
+ totalCostUsd: input.usage.totalCostUsd,
146
+ durationMs: input.usage.durationMs,
147
+ attributes: {
148
+ steps: input.usage.steps,
149
+ ...(input.stepInput !== undefined && { stepInput: input.stepInput }),
150
+ ...(input.structuredOutput !== undefined && { structuredOutput: input.structuredOutput }),
151
+ },
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Filter the bound guardrail list to those referenced by the agent's
157
+ * `guardrails: string[]` declaration. Missing references are collected
158
+ * into `missing` — the caller decides whether that's fatal.
159
+ */
160
+ export function resolveGuardrails(
161
+ agent: Agent,
162
+ bindings: GuardrailsBindings,
163
+ ): { readonly resolved: readonly Guardrail[]; readonly missing: readonly string[] } {
164
+ const available = new Map<string, Guardrail>();
165
+ for (const inv of bindings.guardrails ?? []) available.set(inv.id, inv);
166
+ const resolved: Guardrail[] = [];
167
+ const missing: string[] = [];
168
+ for (const id of agent.guardrails) {
169
+ const inv = available.get(id);
170
+ if (inv !== undefined) resolved.push(inv);
171
+ else missing.push(id);
172
+ }
173
+ return { resolved, missing };
174
+ }
175
+
176
+ /**
177
+ * Evaluate every resolved guardrail against the turn's trace. Returns
178
+ * the raw outcomes so the caller can decide how to react — the gate
179
+ * doesn't halt on its own. `tenantPolicy` (the policy the turn was
180
+ * routed under) also governs which models llm-judge guardrails may use;
181
+ * `abortSignal` (the turn's) reaches every check, so a slow judge or
182
+ * pack check stops when the turn does.
183
+ */
184
+ export async function evaluateGate(
185
+ guardrails: readonly Guardrail[],
186
+ trace: RunTrace,
187
+ bindings: GuardrailsBindings,
188
+ tenantPolicy?: TenantPolicy,
189
+ abortSignal?: AbortSignal,
190
+ ): Promise<readonly EvaluationOutcome[]> {
191
+ if (guardrails.length === 0 || bindings.checks === undefined) return [];
192
+ const evalBindings: EvaluationBindings = {
193
+ ...(bindings.providerRegistry !== undefined && { providerRegistry: bindings.providerRegistry }),
194
+ ...(bindings.compliance !== undefined && { compliance: bindings.compliance }),
195
+ ...(tenantPolicy !== undefined && { tenantPolicy }),
196
+ ...(abortSignal !== undefined && { abortSignal }),
197
+ };
198
+ return await evaluateAll(guardrails, bindings.checks, trace, evalBindings);
199
+ }
200
+
201
+ /**
202
+ * Sort evaluation outcomes by the failed guardrail's action. `halt` is
203
+ * blocking — the turn fails with `guardrail-violation`. `log-only` and
204
+ * `noop` are warnings; every other action (`retry`, `escalate`,
205
+ * `compensate`, or a custom action — actions are open strings) lands in
206
+ * `other`. Warnings and `other` are attached to the successful turn
207
+ * result under `result.violations`; the turn does not carry out those
208
+ * actions. Evaluation errors (a check that could not run) are collected
209
+ * in `errors`.
210
+ */
211
+ export function categorizeOutcomes(outcomes: readonly EvaluationOutcome[]): {
212
+ readonly blocking: readonly EvaluationResult[];
213
+ readonly warnings: readonly EvaluationResult[];
214
+ readonly other: readonly EvaluationResult[];
215
+ readonly errors: readonly { readonly guardrailId: string; readonly message: string }[];
216
+ } {
217
+ const blocking: EvaluationResult[] = [];
218
+ const warnings: EvaluationResult[] = [];
219
+ const other: EvaluationResult[] = [];
220
+ const errors: { guardrailId: string; message: string }[] = [];
221
+ for (const outcome of outcomes) {
222
+ if (outcome.kind === 'err') {
223
+ errors.push({
224
+ guardrailId:
225
+ 'guardrailId' in outcome.error && typeof outcome.error.guardrailId === 'string'
226
+ ? outcome.error.guardrailId
227
+ : '<unknown>',
228
+ message: outcome.error.message,
229
+ });
230
+ continue;
231
+ }
232
+ if (outcome.kind === 'skip') continue;
233
+ const evalResult = outcome.value;
234
+ if (evalResult.result.passed) continue;
235
+ if (evalResult.action === 'halt') blocking.push(evalResult);
236
+ else if (evalResult.action === 'log-only' || evalResult.action === 'noop') {
237
+ warnings.push(evalResult);
238
+ } else other.push(evalResult);
239
+ }
240
+ return { blocking, warnings, other, errors };
241
+ }
242
+
243
+ /**
244
+ * Message for a turn stopped by blocking violations — names each
245
+ * guardrail that fired, with its check's reason when it gave one:
246
+ *
247
+ * Turn blocked by guardrail 'no-pii': Response contains an email address
248
+ * Turn blocked by 2 guardrails: 'no-pii' (Response contains …); 'max-length'
249
+ */
250
+ export function describeBlockingViolations(blocking: readonly EvaluationResult[]): string {
251
+ const reasonOf = (v: EvaluationResult): string | undefined => {
252
+ const reason = v.result.reason?.trim();
253
+ return reason === undefined || reason === '' ? undefined : reason;
254
+ };
255
+ const [only] = blocking;
256
+ if (blocking.length === 1 && only !== undefined) {
257
+ const reason = reasonOf(only);
258
+ return `Turn blocked by guardrail '${only.guardrailId}'${reason !== undefined ? `: ${reason}` : ''}`;
259
+ }
260
+ const named = blocking.map((v) => {
261
+ const reason = reasonOf(v);
262
+ return `'${v.guardrailId}'${reason !== undefined ? ` (${reason})` : ''}`;
263
+ });
264
+ return `Turn blocked by ${blocking.length} guardrails: ${named.join('; ')}`;
265
+ }
266
+
267
+ /**
268
+ * Result of a session-gate check performed BEFORE running a turn:
269
+ * the HITL turn threshold (`conversationPolicy.hitl.afterTurns` or
270
+ * `conversationPolicy.hitlAfterTurns`).
271
+ */
272
+ export type SessionGateResult =
273
+ | { readonly kind: 'ok' }
274
+ | { readonly kind: 'hitl-required'; readonly reason: string; readonly threshold: number };
275
+
276
+ /**
277
+ * Evaluate session-level gates against the conversation state before
278
+ * running the next turn: the HITL threshold from
279
+ * `conversationPolicy.hitl.afterTurns` or `conversationPolicy.hitlAfterTurns`.
280
+ *
281
+ * Overloaded call shapes:
282
+ * - `evaluateSessionGate(agent, turnCount)` — reads the agent's policy
283
+ * directly.
284
+ * - `evaluateSessionGate({ afterTurns? }, turnCount)` — reads a
285
+ * pre-resolved effective policy (the setup handler passes
286
+ * `resolveEffectiveHitlPolicy` output).
287
+ */
288
+ export function evaluateSessionGate(
289
+ input: Agent | { readonly afterTurns?: number },
290
+ currentTurnCount: number,
291
+ ): SessionGateResult {
292
+ const asAgent = input as Agent;
293
+ const asPolicy = input as { readonly afterTurns?: number };
294
+ const threshold: number | undefined =
295
+ 'conversationPolicy' in asAgent
296
+ ? (asAgent.conversationPolicy?.hitl?.afterTurns ?? asAgent.conversationPolicy?.hitlAfterTurns)
297
+ : asPolicy.afterTurns;
298
+ if (threshold !== undefined && currentTurnCount >= threshold) {
299
+ return {
300
+ kind: 'hitl-required',
301
+ reason: `Conversation reached HITL threshold: ${currentTurnCount} turns >= ${threshold}`,
302
+ threshold,
303
+ };
304
+ }
305
+ return { kind: 'ok' };
306
+ }
307
+
308
+ /** Discriminated error emitted when blocking guardrails fail. */
309
+ export interface GuardrailViolationError {
310
+ readonly code: 'guardrail-violation';
311
+ readonly message: string;
312
+ readonly violations: readonly EvaluationResult[];
313
+ /** Errors from checks that couldn't even evaluate (missing check, bad config). */
314
+ readonly evaluationErrors: readonly { readonly guardrailId: string; readonly message: string }[];
315
+ }
316
+
317
+ /**
318
+ * Error shape for a turn blocked by the session HITL threshold.
319
+ * `approvalId` is set when the caller supplied a `bindings.hitl` sink and
320
+ * the enqueue succeeded — downstream code can poll or subscribe on that
321
+ * id. The agent turn itself does not return this code: it parks on a
322
+ * HITL waitpoint instead (`AgentTurnResult.status: 'suspended'`).
323
+ */
324
+ export interface HitlRequiredError {
325
+ readonly code: 'hitl-required';
326
+ readonly message: string;
327
+ readonly threshold: number;
328
+ readonly approvalId?: string;
329
+ }
330
+
331
+ /** Emitted when the agent references guardrail IDs not in the registry. */
332
+ export interface UnresolvedGuardrailError {
333
+ readonly code: 'unresolved-guardrail';
334
+ readonly message: string;
335
+ readonly guardrailIds: readonly string[];
336
+ }
337
+
338
+ // Silence unused-type lint (GuardrailId is used by callers via type inference).
339
+ void 0 as unknown as GuardrailId;
340
+
341
+ // Discriminated Result helper to keep call sites tidy.
342
+ export type GateResult<T, E> = Result<T, E>;
@@ -0,0 +1,143 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { ModelMessage } from '@kindgi/capabilities';
5
+ import type { NodeHandler } from '@kindgi/handler';
6
+
7
+ import type { AgentOutputSpec } from '../types.js';
8
+ import { DEFAULT_MAX_STEPS } from './constants.js';
9
+ import type { AgentTurnIterationOutput, TurnContext } from './context.js';
10
+ import { throwAgentTurnFailure } from './errors.js';
11
+ import {
12
+ DEFAULT_MAX_REPAIRS,
13
+ type OutputChecker,
14
+ outputChecker,
15
+ repairMessage,
16
+ repairsSoFar,
17
+ } from './structured-output.js';
18
+
19
+ /**
20
+ * Loop-body node #3. The final gate before `$loop-end`. Decides
21
+ * whether the iteration finalizes the turn based on:
22
+ *
23
+ * - The model's `finishReason` (anything other than `tool-use` with
24
+ * calls terminates).
25
+ * - `agent.budget.maxSteps` — enforced against `ctx.usage.steps`.
26
+ * - `agent.budget.maxCostUsd` — enforced against cumulative cost.
27
+ * - Cooperative abort from `ctx.turnAbort`.
28
+ * - `agent.output` — a final answer that isn't JSON matching the
29
+ * schema keeps the turn going with a repair message, while repairs
30
+ * (and steps) are left; after that the turn fails with
31
+ * `output-schema-violation`. Skipped on a dry run.
32
+ *
33
+ * Returns the fully-shaped `AgentTurnIterationOutput` — this is the
34
+ * iteration's `$loop-end` output that the loop node's `outputSchema`
35
+ * validates against.
36
+ */
37
+ export function buildBudgetCheckHandler(ctx: TurnContext): NodeHandler {
38
+ let checker: OutputChecker | undefined;
39
+ return async (input: unknown, kctx) => {
40
+ if (ctx.turnAbort.signal.aborted) {
41
+ throwAgentTurnFailure({
42
+ code: 'agent-turn-aborted',
43
+ message: 'Agent turn aborted before budget-check',
44
+ reason: ctx.abortReason ?? 'timeout',
45
+ });
46
+ }
47
+
48
+ const partial = input as {
49
+ readonly step: number;
50
+ readonly finishReason: AgentTurnIterationOutput['finishReason'];
51
+ readonly message: ModelMessage;
52
+ readonly iterationAppended: readonly AgentTurnIterationOutput['iterationAppended'][number][];
53
+ readonly iterationUsage: AgentTurnIterationOutput['iterationUsage'];
54
+ readonly provider: AgentTurnIterationOutput['provider'];
55
+ readonly nextMessages: readonly ModelMessage[];
56
+ readonly hasToolCalls: boolean;
57
+ };
58
+
59
+ const maxSteps = ctx.input.agent.budget?.maxSteps ?? DEFAULT_MAX_STEPS;
60
+ const maxCostUsd = ctx.input.agent.budget?.maxCostUsd;
61
+
62
+ // Cost cap is a hard failure — the caller sees `budget-exceeded`
63
+ // before the turn tries another iteration.
64
+ if (maxCostUsd !== undefined && ctx.usage.totalCostUsd > maxCostUsd) {
65
+ throwAgentTurnFailure({
66
+ code: 'budget-exceeded',
67
+ message: `Agent turn cost budget exceeded (limit ${maxCostUsd}, observed ${ctx.usage.totalCostUsd})`,
68
+ kind: 'cost',
69
+ limit: maxCostUsd,
70
+ observed: ctx.usage.totalCostUsd,
71
+ });
72
+ }
73
+
74
+ // Step cap — if the model still wants another tool call but we've
75
+ // used the allotted steps, fail the run.
76
+ if (partial.hasToolCalls && ctx.usage.steps >= maxSteps) {
77
+ throwAgentTurnFailure({
78
+ code: 'budget-exceeded',
79
+ message: `Agent turn steps budget exceeded (limit ${maxSteps}, observed ${ctx.usage.steps})`,
80
+ kind: 'steps',
81
+ limit: maxSteps,
82
+ observed: ctx.usage.steps,
83
+ });
84
+ }
85
+
86
+ // `finishedTurn` semantics: true when the model's finishReason is
87
+ // terminal (i.e. not a live tool-use) — the current iteration's
88
+ // assistant message is the final answer.
89
+ let finishedTurn = !partial.hasToolCalls;
90
+ let nextMessages = partial.nextMessages;
91
+
92
+ const spec = ctx.input.agent.output;
93
+ if (finishedTurn && spec !== undefined && !kctx.dryRun) {
94
+ checker ??= outputChecker(spec);
95
+ const repair = outputRepair(spec, checker, partial.message, partial.nextMessages, {
96
+ stepsLeft: ctx.usage.steps < maxSteps,
97
+ });
98
+ if (repair !== undefined) {
99
+ finishedTurn = false;
100
+ nextMessages = repair;
101
+ }
102
+ }
103
+
104
+ const output: AgentTurnIterationOutput = {
105
+ finishReason: partial.finishReason,
106
+ message: partial.message,
107
+ iterationAppended: partial.iterationAppended,
108
+ iterationUsage: partial.iterationUsage,
109
+ provider: partial.provider,
110
+ finishedTurn,
111
+ nextMessages,
112
+ };
113
+ return output;
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Check a final answer against the agent's output schema. A valid
119
+ * answer returns `undefined` (the turn finishes). An invalid one with
120
+ * repairs and steps left returns the next model call's messages: the
121
+ * answer, then a repair message listing what's wrong. Otherwise the
122
+ * turn fails with `output-schema-violation`.
123
+ */
124
+ function outputRepair(
125
+ spec: AgentOutputSpec,
126
+ checker: OutputChecker,
127
+ answer: ModelMessage,
128
+ sent: readonly ModelMessage[],
129
+ budget: { readonly stepsLeft: boolean },
130
+ ): readonly ModelMessage[] | undefined {
131
+ const checked = checker.check(answer.content);
132
+ if (checked.kind === 'ok') return undefined;
133
+ const repairs = repairsSoFar(sent);
134
+ if (repairs < (spec.maxRepairs ?? DEFAULT_MAX_REPAIRS) && budget.stepsLeft) {
135
+ return [...sent, answer, repairMessage(spec, checked.errors)];
136
+ }
137
+ throwAgentTurnFailure({
138
+ code: 'output-schema-violation',
139
+ message: `The agent's answer does not match its ${spec.name ?? 'output'} schema after ${repairs} repair${repairs === 1 ? '' : 's'}: ${checked.errors.join('; ')}`,
140
+ errors: checked.errors,
141
+ attempts: repairs + 1,
142
+ });
143
+ }
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { ModelMessage, ModelToolCall } from '@kindgi/capabilities';
5
+ import type { NodeHandler } from '@kindgi/handler';
6
+
7
+ import { formatRetrievedForPrompt } from '../retrieval.js';
8
+ import type { ConversationMessage } from '../types.js';
9
+
10
+ import type { TurnContext } from './context.js';
11
+ import { throwAgentTurnFailure } from './errors.js';
12
+
13
+ /**
14
+ * Compose the initial `modelMessages` array the loop's first iteration
15
+ * feeds to the model:
16
+ *
17
+ * [ system: rendered prompt,
18
+ * system: retrieved context (if any),
19
+ * ...history,
20
+ * user: current message ]
21
+ *
22
+ * Output shape: `{ nextMessages: ModelMessage[] }` — matches the loop
23
+ * iteration output's `nextMessages` field so the loop body can treat
24
+ * iteration 0's input identically to subsequent iterations'.
25
+ */
26
+ export function buildBuildInitialMessagesHandler(ctx: TurnContext): NodeHandler {
27
+ return async (input: unknown) => {
28
+ if (ctx.conversation === undefined || ctx.retrieved === undefined) {
29
+ throwAgentTurnFailure({
30
+ code: 'model-invocation-failed',
31
+ message: 'build-initial-messages invoked before setup / retrievals completed',
32
+ cause: null,
33
+ });
34
+ }
35
+ // `input` is `run-retrievals`'s output; discarded — the rendered
36
+ // prompt is read from setup's sibling output via ctx (retained on
37
+ // the closure by render-prompt through the local field `renderedPrompt`).
38
+ void input;
39
+
40
+ const historyLimit = ctx.input.agent.conversationPolicy?.historyLimit;
41
+ const messages = await ctx.bindings.conversationBinding.readMessages({
42
+ tenantId: ctx.input.tenantId,
43
+ conversationId: ctx.input.conversationId,
44
+ });
45
+ if (messages.kind === 'err') throwAgentTurnFailure(messages.error);
46
+ // The user message just appended is included in the history read;
47
+ // drop it because the composer adds it explicitly as the last
48
+ // element.
49
+ const historyRaw = messages.value.filter((m) => m.sequence !== ctx.userMessage?.sequence);
50
+ const history =
51
+ historyLimit === undefined
52
+ ? historyRaw
53
+ : historyRaw.slice(Math.max(0, historyRaw.length - historyLimit));
54
+
55
+ const contextBlock = formatRetrievedForPrompt(ctx.retrieved);
56
+ const contextMessage: ModelMessage | undefined =
57
+ contextBlock.length > 0 ? { role: 'system', content: contextBlock } : undefined;
58
+
59
+ const rendered = ctx.renderedPrompt;
60
+ if (rendered === undefined) {
61
+ throwAgentTurnFailure({
62
+ code: 'model-invocation-failed',
63
+ message: 'build-initial-messages invoked before render-prompt completed',
64
+ cause: null,
65
+ });
66
+ }
67
+
68
+ const modelMessages: ModelMessage[] = [
69
+ { role: 'system', content: rendered },
70
+ ...(contextMessage !== undefined ? [contextMessage] : []),
71
+ ...history.map(conversationToModelMessage),
72
+ { role: 'user', content: ctx.input.userMessage },
73
+ ];
74
+
75
+ return { nextMessages: modelMessages };
76
+ };
77
+ }
78
+
79
+ function conversationToModelMessage(msg: ConversationMessage): ModelMessage {
80
+ if (msg.role === 'tool') {
81
+ return {
82
+ role: 'tool',
83
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
84
+ ...(msg.toolCall !== undefined && { toolCallId: msg.toolCall.invocationId }),
85
+ };
86
+ }
87
+ if (msg.role === 'agent' && typeof msg.content === 'object' && msg.content !== null) {
88
+ const structured = msg.content as {
89
+ readonly text?: string;
90
+ readonly toolCalls?: readonly ModelToolCall[];
91
+ };
92
+ return {
93
+ role: 'assistant',
94
+ content: structured.text ?? '',
95
+ ...(structured.toolCalls !== undefined && { toolCalls: structured.toolCalls }),
96
+ };
97
+ }
98
+ const role: ModelMessage['role'] = msg.role === 'agent' ? 'assistant' : msg.role;
99
+ return {
100
+ role,
101
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
102
+ };
103
+ }
@@ -0,0 +1,90 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { NodeHandler } from '@kindgi/handler';
5
+
6
+ import { emitTurnEvent } from '../streaming.js';
7
+ import type { AgentTurnResult, AgentTurnUsage } from './result-shape.js';
8
+
9
+ import type { TurnContext } from './context.js';
10
+ import { throwAgentTurnFailure } from './errors.js';
11
+ import { parseJsonAnswer } from './structured-output.js';
12
+
13
+ /** The final answer as JSON — `budget-check` already validated it against the schema. */
14
+ function structuredAnswer(content: unknown): unknown {
15
+ const parsed = parseJsonAnswer(content);
16
+ return parsed.kind === 'ok' ? parsed.value : null;
17
+ }
18
+
19
+ /**
20
+ * The terminal node — builds the caller-facing `AgentTurnResult` from
21
+ * accumulated `TurnContext` state, emits `turn.completed`, and returns
22
+ * the fully-shaped result as the flow's output. `projectRunResult`
23
+ * unwraps this on the way out.
24
+ */
25
+ export function buildComposeResultHandler(ctx: TurnContext): NodeHandler {
26
+ return async (_input, kctx) => {
27
+ if (ctx.finalMessage === undefined || ctx.conversation === undefined) {
28
+ throwAgentTurnFailure({
29
+ code: 'model-invocation-failed',
30
+ message: 'compose-result invoked before final message + conversation ready',
31
+ cause: null,
32
+ });
33
+ }
34
+ const durationMs = Date.now() - ctx.startedAt;
35
+ const usage: AgentTurnUsage = {
36
+ steps: ctx.usage.steps,
37
+ promptTokens: ctx.usage.promptTokens,
38
+ completionTokens: ctx.usage.completionTokens,
39
+ totalCostUsd: ctx.usage.totalCostUsd,
40
+ durationMs,
41
+ };
42
+
43
+ const provider = ctx.lastProvider ?? {
44
+ id: ctx.provider?.metadata.id ?? 'unknown',
45
+ model: ctx.model?.name ?? 'unknown',
46
+ };
47
+
48
+ const result: AgentTurnResult = {
49
+ runId: kctx.runId,
50
+ conversationId: ctx.input.conversationId,
51
+ // Dry-run must not advance the conversation's turn counter (no
52
+ // persistence happened). Real runs report the next turn number.
53
+ turnNumber: kctx.dryRun ? ctx.conversation.turnCount : ctx.conversation.turnCount + 1,
54
+ appended: ctx.appended,
55
+ response: ctx.finalMessage,
56
+ retrieved: ctx.retrieved ?? [],
57
+ violations: ctx.nonBlockingViolations ?? [],
58
+ usage,
59
+ provider,
60
+ ...(ctx.provider?.metadata.fallback === true && {
61
+ warnings: [
62
+ {
63
+ code: 'fallback-provider',
64
+ message: `Answered by "${provider.id}", a fallback provider: no other registered provider satisfies agent "${ctx.input.agent.id}".`,
65
+ },
66
+ ],
67
+ }),
68
+ ...(ctx.input.agent.output !== undefined && {
69
+ output: kctx.dryRun ? null : structuredAnswer(ctx.finalMessage.content),
70
+ }),
71
+ ...(ctx.persistedProvenance !== undefined && { provenance: ctx.persistedProvenance }),
72
+ ...(kctx.dryRun && { dryRun: true }),
73
+ };
74
+
75
+ await emitTurnEvent(ctx.bindings.onEvent, {
76
+ kind: 'turn.completed',
77
+ conversationId: ctx.input.conversationId,
78
+ turnNumber: ctx.conversation.turnCount + 1,
79
+ response: ctx.finalMessage,
80
+ retrieved: ctx.retrieved ?? [],
81
+ durationMs,
82
+ totalCostUsd: ctx.usage.totalCostUsd,
83
+ });
84
+
85
+ // Wrapped explicitly: the runtime reads a returned object with an
86
+ // `output` key as `{ output, stateDelta }`, and a typed turn's
87
+ // result has one.
88
+ return { output: result };
89
+ };
90
+ }
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * Defaults shared across handler modules. Kept in a single tiny module
6
+ * so the loop-body handlers can import them without pulling in the
7
+ * whole invoke.ts orchestration surface.
8
+ */
9
+ export const DEFAULT_MAX_STEPS = 8;
10
+ export const DEFAULT_MAX_WALL_MS = 120_000;