@kindgi/agents 0.1.2 → 0.1.4-rc.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 (159) hide show
  1. package/dist/blocks.d.ts +101 -0
  2. package/dist/blocks.d.ts.map +1 -0
  3. package/dist/blocks.js +149 -0
  4. package/dist/blocks.js.map +1 -0
  5. package/dist/conversation-binding.d.ts +9 -1
  6. package/dist/conversation-binding.d.ts.map +1 -1
  7. package/dist/define.d.ts +17 -2
  8. package/dist/define.d.ts.map +1 -1
  9. package/dist/define.js +93 -6
  10. package/dist/define.js.map +1 -1
  11. package/dist/guardrails-gate.d.ts +7 -4
  12. package/dist/guardrails-gate.d.ts.map +1 -1
  13. package/dist/guardrails-gate.js +5 -2
  14. package/dist/guardrails-gate.js.map +1 -1
  15. package/dist/handlers/compose-result.d.ts.map +1 -1
  16. package/dist/handlers/compose-result.js +3 -0
  17. package/dist/handlers/compose-result.js.map +1 -1
  18. package/dist/handlers/context.d.ts +24 -0
  19. package/dist/handlers/context.d.ts.map +1 -1
  20. package/dist/handlers/dispatch-tools.d.ts.map +1 -1
  21. package/dist/handlers/dispatch-tools.js +69 -43
  22. package/dist/handlers/dispatch-tools.js.map +1 -1
  23. package/dist/handlers/errors.d.ts +12 -1
  24. package/dist/handlers/errors.d.ts.map +1 -1
  25. package/dist/handlers/errors.js.map +1 -1
  26. package/dist/handlers/evaluate-guardrails.d.ts.map +1 -1
  27. package/dist/handlers/evaluate-guardrails.js +36 -1
  28. package/dist/handlers/evaluate-guardrails.js.map +1 -1
  29. package/dist/handlers/gate-decision.d.ts +62 -0
  30. package/dist/handlers/gate-decision.d.ts.map +1 -0
  31. package/dist/handlers/gate-decision.js +55 -0
  32. package/dist/handlers/gate-decision.js.map +1 -0
  33. package/dist/handlers/model-call.d.ts +10 -0
  34. package/dist/handlers/model-call.d.ts.map +1 -1
  35. package/dist/handlers/model-call.js +120 -34
  36. package/dist/handlers/model-call.js.map +1 -1
  37. package/dist/handlers/persist-provenance.d.ts.map +1 -1
  38. package/dist/handlers/persist-provenance.js +3 -1
  39. package/dist/handlers/persist-provenance.js.map +1 -1
  40. package/dist/handlers/persist-user-message.d.ts.map +1 -1
  41. package/dist/handlers/persist-user-message.js +5 -16
  42. package/dist/handlers/persist-user-message.js.map +1 -1
  43. package/dist/handlers/public-types.d.ts +37 -3
  44. package/dist/handlers/public-types.d.ts.map +1 -1
  45. package/dist/handlers/rehydrate.d.ts.map +1 -1
  46. package/dist/handlers/rehydrate.js +91 -1
  47. package/dist/handlers/rehydrate.js.map +1 -1
  48. package/dist/handlers/render-prompt.d.ts.map +1 -1
  49. package/dist/handlers/render-prompt.js +4 -1
  50. package/dist/handlers/render-prompt.js.map +1 -1
  51. package/dist/handlers/replay.d.ts +143 -0
  52. package/dist/handlers/replay.d.ts.map +1 -0
  53. package/dist/handlers/replay.js +177 -0
  54. package/dist/handlers/replay.js.map +1 -0
  55. package/dist/handlers/resolve-blocks.d.ts +32 -0
  56. package/dist/handlers/resolve-blocks.d.ts.map +1 -0
  57. package/dist/handlers/resolve-blocks.js +129 -0
  58. package/dist/handlers/resolve-blocks.js.map +1 -0
  59. package/dist/handlers/resolve-tools.d.ts +6 -2
  60. package/dist/handlers/resolve-tools.d.ts.map +1 -1
  61. package/dist/handlers/resolve-tools.js +59 -27
  62. package/dist/handlers/resolve-tools.js.map +1 -1
  63. package/dist/handlers/result-shape.d.ts +7 -0
  64. package/dist/handlers/result-shape.d.ts.map +1 -1
  65. package/dist/handlers/result-shape.js.map +1 -1
  66. package/dist/handlers/run-retrievals.d.ts.map +1 -1
  67. package/dist/handlers/run-retrievals.js +35 -35
  68. package/dist/handlers/run-retrievals.js.map +1 -1
  69. package/dist/handlers/run-snapshot.d.ts.map +1 -1
  70. package/dist/handlers/run-snapshot.js +1 -0
  71. package/dist/handlers/run-snapshot.js.map +1 -1
  72. package/dist/handlers/setup.d.ts +2 -0
  73. package/dist/handlers/setup.d.ts.map +1 -1
  74. package/dist/handlers/setup.js +29 -12
  75. package/dist/handlers/setup.js.map +1 -1
  76. package/dist/handlers/tool-hitl.d.ts +5 -9
  77. package/dist/handlers/tool-hitl.d.ts.map +1 -1
  78. package/dist/handlers/tool-hitl.js +5 -0
  79. package/dist/handlers/tool-hitl.js.map +1 -1
  80. package/dist/handlers/turn-environment.d.ts +13 -2
  81. package/dist/handlers/turn-environment.d.ts.map +1 -1
  82. package/dist/handlers/turn-environment.js +12 -3
  83. package/dist/handlers/turn-environment.js.map +1 -1
  84. package/dist/handlers/turn-provenance.d.ts +67 -0
  85. package/dist/handlers/turn-provenance.d.ts.map +1 -0
  86. package/dist/handlers/turn-provenance.js +160 -0
  87. package/dist/handlers/turn-provenance.js.map +1 -0
  88. package/dist/index.d.ts +10 -1
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +5 -0
  91. package/dist/index.js.map +1 -1
  92. package/dist/invoke.d.ts +14 -1
  93. package/dist/invoke.d.ts.map +1 -1
  94. package/dist/invoke.js +37 -19
  95. package/dist/invoke.js.map +1 -1
  96. package/dist/pins.d.ts +54 -0
  97. package/dist/pins.d.ts.map +1 -0
  98. package/dist/pins.js +51 -0
  99. package/dist/pins.js.map +1 -0
  100. package/dist/prompt.d.ts +12 -2
  101. package/dist/prompt.d.ts.map +1 -1
  102. package/dist/prompt.js +41 -4
  103. package/dist/prompt.js.map +1 -1
  104. package/dist/provenance-emit.d.ts +4 -2
  105. package/dist/provenance-emit.d.ts.map +1 -1
  106. package/dist/provenance-emit.js +4 -2
  107. package/dist/provenance-emit.js.map +1 -1
  108. package/dist/run-snapshot-binding.d.ts +5 -0
  109. package/dist/run-snapshot-binding.d.ts.map +1 -1
  110. package/dist/schema.d.ts +34 -0
  111. package/dist/schema.d.ts.map +1 -1
  112. package/dist/schema.js +16 -0
  113. package/dist/schema.js.map +1 -1
  114. package/dist/streaming.d.ts +5 -0
  115. package/dist/streaming.d.ts.map +1 -1
  116. package/dist/streaming.js.map +1 -1
  117. package/dist/types.d.ts +65 -2
  118. package/dist/types.d.ts.map +1 -1
  119. package/migrations/0003_hesitant_captain_cross.sql +2 -0
  120. package/migrations/0004_stormy_moondragon.sql +1 -0
  121. package/migrations/meta/0003_snapshot.json +321 -0
  122. package/migrations/meta/0004_snapshot.json +327 -0
  123. package/migrations/meta/_journal.json +14 -0
  124. package/package.json +15 -15
  125. package/src/blocks.ts +231 -0
  126. package/src/conversation-binding.ts +9 -1
  127. package/src/define.ts +104 -7
  128. package/src/guardrails-gate.ts +17 -3
  129. package/src/handlers/compose-result.ts +3 -0
  130. package/src/handlers/context.ts +24 -0
  131. package/src/handlers/dispatch-tools.ts +75 -44
  132. package/src/handlers/errors.ts +13 -0
  133. package/src/handlers/evaluate-guardrails.ts +41 -1
  134. package/src/handlers/gate-decision.ts +101 -0
  135. package/src/handlers/model-call.ts +163 -33
  136. package/src/handlers/persist-provenance.ts +3 -1
  137. package/src/handlers/persist-user-message.ts +3 -16
  138. package/src/handlers/public-types.ts +38 -3
  139. package/src/handlers/rehydrate.ts +141 -9
  140. package/src/handlers/render-prompt.ts +13 -7
  141. package/src/handlers/replay.ts +314 -0
  142. package/src/handlers/resolve-blocks.ts +188 -0
  143. package/src/handlers/resolve-tools.ts +76 -28
  144. package/src/handlers/result-shape.ts +8 -0
  145. package/src/handlers/run-retrievals.ts +45 -41
  146. package/src/handlers/run-snapshot.ts +1 -0
  147. package/src/handlers/setup.ts +30 -22
  148. package/src/handlers/tool-hitl.ts +6 -10
  149. package/src/handlers/turn-environment.ts +28 -3
  150. package/src/handlers/turn-provenance.ts +230 -0
  151. package/src/index.ts +44 -0
  152. package/src/invoke.ts +49 -20
  153. package/src/pins.ts +98 -0
  154. package/src/prompt.ts +52 -4
  155. package/src/provenance-emit.ts +4 -1
  156. package/src/run-snapshot-binding.ts +6 -0
  157. package/src/schema.ts +16 -0
  158. package/src/streaming.ts +5 -0
  159. package/src/types.ts +68 -2
package/src/define.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  loadZodConverterSync,
10
10
  toJSONSchemaSync,
11
11
  } from '@kindgi/schema';
12
+ import { pickVersion } from '@kindgi/tools';
12
13
  import type { Result, Semver } from '@kindgi/types';
13
14
 
14
15
  import type { InvalidAgentError } from './errors.js';
@@ -16,8 +17,10 @@ import type {
16
17
  Agent,
17
18
  AgentId,
18
19
  AgentOutputSpec,
20
+ BlockRef,
19
21
  ConversationPolicy,
20
22
  PromptParameter,
23
+ PromptRef,
21
24
  RetrievalIntent,
22
25
  ToolRef,
23
26
  TurnBudget,
@@ -48,9 +51,10 @@ export function defineAgent(spec: DefineAgentSpec): Result<Agent, InvalidAgentEr
48
51
  const issues: Issue[] = [
49
52
  ...validateIdentity(spec),
50
53
  ...validateContent(spec),
54
+ ...validateBlockRefs(spec),
51
55
  ...validateArrays(spec),
52
56
  ...validateRetrieval(spec),
53
- ...validateParameters(spec.parameters),
57
+ ...validatePromptParameters(spec.parameters),
54
58
  ...validateBudget(spec.budget),
55
59
  ...validateConversationPolicy(spec.conversationPolicy),
56
60
  ...validateToolErrors(spec.toolErrors),
@@ -126,8 +130,16 @@ export interface DefineAgentSpec {
126
130
  * The system prompt. Sent to the model with every turn as the
127
131
  * baseline instructions. Load-bearing — this is where you shape
128
132
  * the agent's behavior (persona, output format, tool-use policy).
133
+ *
134
+ * Or a prompt block by range (`{ prompt: 'acme.intake-prompt',
135
+ * version: '^1.0.0' }`), whose template and parameters are used
136
+ * instead; then `parameters` stays unset (the block declares them).
129
137
  */
130
- readonly instructions: string;
138
+ readonly instructions: string | PromptRef;
139
+ /** Settings blocks the agent reads, by range (see `Agent.settings`). */
140
+ readonly settings?: readonly BlockRef[];
141
+ /** A model-settings block, by range (see `Agent.modelSettings`). */
142
+ readonly modelSettings?: BlockRef;
131
143
  /**
132
144
  * Required capabilities the agent needs from a `ModelProvider`.
133
145
  * Typically one entry: `[{ needs: [{ feature: 'tool-use' }] }]`
@@ -248,8 +260,19 @@ function validateIdentity(spec: DefineAgentSpec): Issue[] {
248
260
 
249
261
  function validateContent(spec: DefineAgentSpec): Issue[] {
250
262
  const out: Issue[] = [];
251
- if (typeof spec.instructions !== 'string' || spec.instructions.trim().length === 0) {
252
- out.push({ path: '/instructions', message: 'instructions must be a non-empty string' });
263
+ if (typeof spec.instructions === 'object' && spec.instructions !== null) {
264
+ out.push(...blockRefIssues(spec.instructions, '/instructions', 'prompt'));
265
+ if (spec.parameters !== undefined) {
266
+ out.push({
267
+ path: '/parameters',
268
+ message: 'the prompt block declares the parameters: leave parameters unset',
269
+ });
270
+ }
271
+ } else if (typeof spec.instructions !== 'string' || spec.instructions.trim().length === 0) {
272
+ out.push({
273
+ path: '/instructions',
274
+ message: 'instructions must be a non-empty string, or a prompt block { prompt, version }',
275
+ });
253
276
  }
254
277
  if (!Array.isArray(spec.capabilities) || spec.capabilities.length === 0) {
255
278
  out.push({
@@ -260,6 +283,56 @@ function validateContent(spec: DefineAgentSpec): Issue[] {
260
283
  return out;
261
284
  }
262
285
 
286
+ const BLOCK_ID = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)+$/;
287
+
288
+ /** A block reference: a dotted block id and a valid semver range. */
289
+ function blockRefIssues(ref: unknown, path: string, idKey: 'prompt' | 'id'): Issue[] {
290
+ if (ref === null || typeof ref !== 'object') {
291
+ return [{ path, message: `must be { ${idKey}, version }` }];
292
+ }
293
+ const r = ref as Record<string, unknown>;
294
+ const out: Issue[] = [];
295
+ if (typeof r[idKey] !== 'string' || !BLOCK_ID.test(r[idKey] as string)) {
296
+ out.push({
297
+ path: `${path}/${idKey}`,
298
+ message: 'must be a dotted lowercase block id, e.g. "acme.weights"',
299
+ });
300
+ }
301
+ if (typeof r.version !== 'string' || pickVersion([], r.version).kind === 'invalid-range') {
302
+ out.push({
303
+ path: `${path}/version`,
304
+ message: 'must be a semver range, e.g. "^1.0.0" or "1.2.0"',
305
+ });
306
+ }
307
+ return out;
308
+ }
309
+
310
+ /** Settings and model-settings references: valid, and each block named once. */
311
+ function validateBlockRefs(spec: DefineAgentSpec): Issue[] {
312
+ const out: Issue[] = [];
313
+ const seen = new Set<string>();
314
+ const once = (id: unknown, path: string) => {
315
+ if (typeof id !== 'string') return;
316
+ if (seen.has(id)) out.push({ path, message: `settings block "${id}" is referenced twice` });
317
+ seen.add(id);
318
+ };
319
+ if (spec.settings !== undefined) {
320
+ if (!Array.isArray(spec.settings)) {
321
+ out.push({ path: '/settings', message: 'settings must be an array of { id, version }' });
322
+ } else {
323
+ spec.settings.forEach((ref, i) => {
324
+ out.push(...blockRefIssues(ref, `/settings/${i}`, 'id'));
325
+ once((ref as { id?: unknown }).id, `/settings/${i}/id`);
326
+ });
327
+ }
328
+ }
329
+ if (spec.modelSettings !== undefined) {
330
+ out.push(...blockRefIssues(spec.modelSettings, '/modelSettings', 'id'));
331
+ once((spec.modelSettings as { id?: unknown }).id, '/modelSettings/id');
332
+ }
333
+ return out;
334
+ }
335
+
263
336
  function validateArrays(spec: DefineAgentSpec): Issue[] {
264
337
  const out: Issue[] = [];
265
338
  if (!Array.isArray(spec.tools)) {
@@ -334,9 +407,10 @@ function validateIntent(intent: RetrievalIntent, i: number): Issue[] {
334
407
  }
335
408
 
336
409
  const VALID_PARAM_TYPES = new Set(['string', 'number', 'boolean', 'date']);
337
- const AUTO_INJECTED_NAMES = new Set(['today', 'now', 'agent', 'conversation']);
410
+ const AUTO_INJECTED_NAMES = new Set(['today', 'now', 'agent', 'conversation', 'settings']);
338
411
 
339
- function validateParameters(parameters?: readonly PromptParameter[]): Issue[] {
412
+ /** An agent's or a prompt block's declared parameters: the problems, none when they're valid. */
413
+ export function validatePromptParameters(parameters?: readonly PromptParameter[]): Issue[] {
340
414
  if (parameters === undefined) return [];
341
415
  const out: Issue[] = [];
342
416
  const seen = new Set<string>();
@@ -448,6 +522,7 @@ function validateConversationPolicy(policy?: ConversationPolicy): Issue[] {
448
522
  message: 'hitl.timeoutMs must be a positive integer (ms)',
449
523
  });
450
524
  }
525
+ out.push(...validateOnTimeout(h.onTimeout));
451
526
  const ROLES: ReadonlySet<string> = new Set(['standard', 'senior', 'admin']);
452
527
  if (
453
528
  h.defaultReviewerRole !== undefined &&
@@ -490,6 +565,25 @@ function validateConversationPolicy(policy?: ConversationPolicy): Issue[] {
490
565
  return out;
491
566
  }
492
567
 
568
+ /**
569
+ * `hitl.onTimeout`: only `'escalate'`, what the runtime does when an
570
+ * approval's time runs out. `'auto-approve'` / `'auto-reject'` aren't
571
+ * implemented, so they're refused instead of accepted and ignored.
572
+ */
573
+ function validateOnTimeout(onTimeout: unknown): Issue[] {
574
+ if (onTimeout === undefined || onTimeout === 'escalate') return [];
575
+ const path = '/conversationPolicy/hitl/onTimeout';
576
+ if (onTimeout === 'auto-approve' || onTimeout === 'auto-reject') {
577
+ return [
578
+ {
579
+ path,
580
+ message: `hitl.onTimeout '${onTimeout}' isn't supported: an approval that times out escalates one reviewer tier, and at admin it expires (the turn fails with hitl-cancelled). Use 'escalate', or leave it out.`,
581
+ },
582
+ ];
583
+ }
584
+ return [{ path, message: "hitl.onTimeout must be 'escalate'" }];
585
+ }
586
+
493
587
  type OutputOutcome =
494
588
  | { readonly kind: 'none' }
495
589
  | { readonly kind: 'ok'; readonly value: AgentOutputSpec }
@@ -546,7 +640,10 @@ function buildAgent(spec: DefineAgentSpec, output: AgentOutputSpec | undefined):
546
640
  version: spec.version as Semver,
547
641
  name: spec.name,
548
642
  ...(spec.description !== undefined && { description: spec.description }),
549
- instructions: spec.instructions,
643
+ instructions:
644
+ typeof spec.instructions === 'string' ? spec.instructions : { ...spec.instructions },
645
+ ...(spec.settings !== undefined && { settings: spec.settings.map((r) => ({ ...r })) }),
646
+ ...(spec.modelSettings !== undefined && { modelSettings: { ...spec.modelSettings } }),
550
647
  capabilities: spec.capabilities.map((c) => ({ ...c })),
551
648
  tools: [...spec.tools],
552
649
  retrieval: spec.retrieval.map((r) => ({ ...r })),
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
- import type { ProviderRegistry, TenantPolicy } from '@kindgi/capabilities';
4
+ import type { ProviderRegistry, TenantPolicy, UsageSink } from '@kindgi/capabilities';
5
5
  import type { ComplianceProvider } from '@kindgi/compliance';
6
6
  import {
7
7
  type CheckRegistry,
@@ -15,7 +15,15 @@ import {
15
15
  type ToolResultRecord,
16
16
  evaluateAll,
17
17
  } from '@kindgi/guardrails';
18
- import type { AgentId, GuardrailId, ProjectId, Result, RunId, TenantId } from '@kindgi/types';
18
+ import type {
19
+ AgentId,
20
+ GuardrailId,
21
+ OrgId,
22
+ ProjectId,
23
+ Result,
24
+ RunId,
25
+ TenantId,
26
+ } from '@kindgi/types';
19
27
 
20
28
  import type { Agent, ConversationId, ConversationMessage } from './types.js';
21
29
 
@@ -66,6 +74,8 @@ export function buildRunTrace(input: {
66
74
  readonly tenantId: TenantId;
67
75
  /** Lets the engine record compliance evidence for a failed check. */
68
76
  readonly projectId: ProjectId;
77
+ /** The project's org, when it has one. */
78
+ readonly orgId?: OrgId;
69
79
  readonly conversationId: ConversationId;
70
80
  /** 1-based number of this turn in the conversation. */
71
81
  readonly turnNumber: number;
@@ -133,6 +143,7 @@ export function buildRunTrace(input: {
133
143
  runId: input.runId,
134
144
  tenantId: input.tenantId,
135
145
  projectId: input.projectId,
146
+ ...(input.orgId !== undefined && { orgId: input.orgId }),
136
147
  agentId: input.agent.id as unknown as AgentId,
137
148
  output,
138
149
  toolCalls,
@@ -179,7 +190,8 @@ export function resolveGuardrails(
179
190
  * doesn't halt on its own. `tenantPolicy` (the policy the turn was
180
191
  * routed under) also governs which models llm-judge guardrails may use;
181
192
  * `abortSignal` (the turn's) reaches every check, so a slow judge or
182
- * pack check stops when the turn does.
193
+ * pack check stops when the turn does. `usage` (the turn's sink) records
194
+ * every llm-judge call, as the turn's own model calls are.
183
195
  */
184
196
  export async function evaluateGate(
185
197
  guardrails: readonly Guardrail[],
@@ -187,6 +199,7 @@ export async function evaluateGate(
187
199
  bindings: GuardrailsBindings,
188
200
  tenantPolicy?: TenantPolicy,
189
201
  abortSignal?: AbortSignal,
202
+ usage?: UsageSink,
190
203
  ): Promise<readonly EvaluationOutcome[]> {
191
204
  if (guardrails.length === 0 || bindings.checks === undefined) return [];
192
205
  const evalBindings: EvaluationBindings = {
@@ -194,6 +207,7 @@ export async function evaluateGate(
194
207
  ...(bindings.compliance !== undefined && { compliance: bindings.compliance }),
195
208
  ...(tenantPolicy !== undefined && { tenantPolicy }),
196
209
  ...(abortSignal !== undefined && { abortSignal }),
210
+ ...(usage !== undefined && { usage }),
197
211
  };
198
212
  return await evaluateAll(guardrails, bindings.checks, trace, evalBindings);
199
213
  }
@@ -8,6 +8,7 @@ import type { AgentTurnResult, AgentTurnUsage } from './result-shape.js';
8
8
 
9
9
  import type { TurnContext } from './context.js';
10
10
  import { throwAgentTurnFailure } from './errors.js';
11
+ import { replayReport } from './replay.js';
11
12
  import { parseJsonAnswer } from './structured-output.js';
12
13
 
13
14
  /** The final answer as JSON — `budget-check` already validated it against the schema. */
@@ -45,6 +46,7 @@ export function buildComposeResultHandler(ctx: TurnContext): NodeHandler {
45
46
  model: ctx.model?.name ?? 'unknown',
46
47
  };
47
48
 
49
+ const replay = replayReport(ctx);
48
50
  const result: AgentTurnResult = {
49
51
  runId: kctx.runId,
50
52
  conversationId: ctx.input.conversationId,
@@ -70,6 +72,7 @@ export function buildComposeResultHandler(ctx: TurnContext): NodeHandler {
70
72
  }),
71
73
  ...(ctx.persistedProvenance !== undefined && { provenance: ctx.persistedProvenance }),
72
74
  ...(kctx.dryRun && { dryRun: true }),
75
+ ...(replay !== undefined && { replay }),
73
76
  };
74
77
 
75
78
  await emitTurnEvent(ctx.bindings.onEvent, {
@@ -19,6 +19,7 @@ import type { ProvenanceBindings } from '../provenance-emit.js';
19
19
  import type { Agent, Conversation, ConversationMessage, RetrievedFact } from '../types.js';
20
20
 
21
21
  import type { HitlBindings, InvokeAgentBindings, InvokeAgentInput } from './public-types.js';
22
+ import type { TurnBlocks } from './resolve-blocks.js';
22
23
  import type { ToolErrorPolicy } from './tool-errors.js';
23
24
 
24
25
  /**
@@ -53,6 +54,8 @@ export interface TurnContext {
53
54
  * Populated by `setup` — resolved tools (per-name map + model
54
55
  * definitions).
55
56
  */
57
+ /** The data blocks the turn runs with (set by setup; none when the agent references none). */
58
+ blocks?: TurnBlocks;
56
59
  tools?: {
57
60
  readonly definitions: readonly ModelToolDefinition[];
58
61
  /**
@@ -143,6 +146,20 @@ export interface TurnContext {
143
146
  * Present only when the builder was wired AND persistence succeeded.
144
147
  */
145
148
  persistedProvenance?: import('@kindgi/provenance').Provenance;
149
+ /**
150
+ * This turn's tool results so far, by invocation id: a model call read
151
+ * them all (they're in its input), so its provenance node is
152
+ * `influenced-by` each. Filled by `dispatch-tools`, and by the rebuild
153
+ * of a resumed turn.
154
+ */
155
+ toolResultIds?: string[];
156
+ /**
157
+ * The tool-call approvals this turn's journal shows decided, by
158
+ * invocation id: each call's provenance shows the approval it waited on.
159
+ * Populated by `rehydrateTurnContext` (a decision only reaches a turn
160
+ * that parked, and resumes).
161
+ */
162
+ toolApprovals?: ReadonlyMap<string, import('./turn-provenance.js').ToolApproval>;
146
163
  /**
147
164
  * The tenant policy this turn's model was routed under (bound policy
148
165
  * merged with the policy registry's). Populated by `setup`; guardrail
@@ -175,6 +192,13 @@ export interface TurnContext {
175
192
  * `AgentTurnResult.violations`.
176
193
  */
177
194
  nonBlockingViolations?: readonly EvaluationResult[];
195
+ /**
196
+ * A replay turn's tool calls so far, and what happened to each
197
+ * (`decideReplayTool`). Rebuilt from the journal on resume.
198
+ */
199
+ replayTrace?: import('./replay.js').ReplayToolTrace[];
200
+ /** A replay turn's session approval, when it reached the gate (`replaySessionApproval`). */
201
+ replayApproval?: 'followed' | 'skipped';
178
202
  /**
179
203
  * Reason recorded when `turnAbort` fires. Used to distinguish
180
204
  * external cancellation from wall-clock timeout in the projected
@@ -19,13 +19,16 @@ import {
19
19
  type UnresolvedToolError,
20
20
  throwAgentTurnFailure,
21
21
  } from './errors.js';
22
+ import { TOOL_CALL_GATE_SUBJECT, readGateDecision } from './gate-decision.js';
23
+ import { decideReplayTool } from './replay.js';
22
24
  import {
23
25
  effectiveToolErrorPolicy,
24
26
  toolErrorKindOf,
25
27
  toolErrorResult,
26
28
  toolRetriesSoFar,
27
29
  } from './tool-errors.js';
28
- import { type ToolHitlDecision, computeToolCallWaitToken, hashToolArgs } from './tool-hitl.js';
30
+ import { TOOL_GATE_RECORD_PREFIX, computeToolCallWaitToken, hashToolArgs } from './tool-hitl.js';
31
+ import { addStepToolNodes } from './turn-provenance.js';
29
32
 
30
33
  /**
31
34
  * Resolves the effective HITL mode + reviewer role for a specific
@@ -111,7 +114,7 @@ async function decideToolGate(
111
114
  cause: null,
112
115
  });
113
116
  }
114
- return kctx.record(`tool-hitl-gate:${call.id}`, (): ToolGateRecord | undefined => {
117
+ return kctx.record(`${TOOL_GATE_RECORD_PREFIX}${call.id}`, (): ToolGateRecord | undefined => {
115
118
  const resolved = resolveEffectiveToolHitl(effectiveHitl, tool);
116
119
  if (resolved.mode === 'never_ask') return undefined;
117
120
  const argsHash = hashToolArgs(call.arguments);
@@ -242,6 +245,50 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
242
245
  // skip the reviewer's answer. Cross-turn `ask_on_first_use` caching
243
246
  // (via conversation metadata) is not implemented.
244
247
  const gate = await decideToolGate(ctx, kctx, call, tool);
248
+
249
+ // A replay turn decides the call first: a recorded or refused call
250
+ // runs nothing, so it asks for no approval either.
251
+ const replayed =
252
+ ctx.input.replay === undefined
253
+ ? undefined
254
+ : await decideReplayTool(ctx, kctx, {
255
+ step: partial.step,
256
+ callId: call.id,
257
+ tool,
258
+ version: resolvedVersion,
259
+ arguments: call.arguments,
260
+ gated: gate !== undefined,
261
+ });
262
+ if (replayed !== undefined && replayed.kind !== 'live') {
263
+ const replayStarted = Date.now();
264
+ await emitTurnEvent(ctx.bindings.onEvent, {
265
+ kind: 'tool.started',
266
+ step: partial.step,
267
+ toolId: call.name,
268
+ toolVersion: resolvedVersion,
269
+ toolVersionRange: requestedRange,
270
+ invocationId: call.id,
271
+ arguments: call.arguments,
272
+ });
273
+ nextMessages = await appendToolResult(ctx, call, replayed.result, {
274
+ toolId: tool.id as unknown as string,
275
+ nextMessages,
276
+ iterationAppended,
277
+ });
278
+ await emitTurnEvent(ctx.bindings.onEvent, {
279
+ kind: 'tool.completed',
280
+ step: partial.step,
281
+ toolId: call.name,
282
+ toolVersion: resolvedVersion,
283
+ toolVersionRange: requestedRange,
284
+ invocationId: call.id,
285
+ output: replayed.result as never,
286
+ durationMs: Date.now() - replayStarted,
287
+ replay: replayed.kind,
288
+ });
289
+ continue;
290
+ }
291
+
245
292
  let toolRejectionPayload: { readonly rationale?: string } | null = null;
246
293
  if (gate !== undefined) {
247
294
  const { argsHash, waitTokenId, timeoutMs } = gate;
@@ -251,7 +298,8 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
251
298
  try {
252
299
  await ctx.bindings.hitl.enqueue({
253
300
  tenantId: ctx.input.tenantId,
254
- subjectKind: 'tool-call:pending',
301
+ projectId: ctx.input.projectId,
302
+ subjectKind: TOOL_CALL_GATE_SUBJECT,
255
303
  subjectRef: {
256
304
  conversationId: ctx.input.conversationId,
257
305
  agentId: ctx.input.agent.id,
@@ -275,14 +323,16 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
275
323
  }
276
324
 
277
325
  try {
278
- const decision = await kctx.waitForToken<ToolHitlDecision>(waitTokenId, {
279
- timeoutMs,
280
- });
281
- if (decision.decided === 'reject') {
326
+ // Fails closed: only an explicit approve runs the tool. A reject,
327
+ // or an answer that isn't a decision at all, gives the model a
328
+ // rejected result instead.
329
+ const decision = readGateDecision(
330
+ await kctx.waitForToken<unknown>(waitTokenId, { timeoutMs }),
331
+ );
332
+ if (!decision.approved) {
282
333
  toolRejectionPayload =
283
334
  decision.rationale !== undefined ? { rationale: decision.rationale } : {};
284
335
  }
285
- // approve → fall through to dispatch
286
336
  } catch (cause) {
287
337
  if (cause instanceof WaitpointCancelledError) {
288
338
  throwAgentTurnFailure({
@@ -371,44 +421,15 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
371
421
  invocationId: call.id,
372
422
  output: dispatched.value.persisted.content,
373
423
  durationMs: Date.now() - toolStarted,
424
+ ...(replayed !== undefined && { replay: replayed.kind }),
374
425
  });
375
-
376
- if (ctx.provenance !== undefined) {
377
- const modelCallNodeId = `model-call:${partial.step}`;
378
- const toolCallNodeId = `tool-call:${call.id}`;
379
- const toolResultNodeId = `tool-result:${call.id}`;
380
- ctx.provenance.addNode({
381
- id: toolCallNodeId,
382
- kind: 'tool-call',
383
- timestamp: dispatched.value.persisted.createdAt,
384
- attributes: {
385
- toolId: call.name,
386
- invocationId: call.id,
387
- // Capture the exact version the
388
- // registry picked at run start + the range the agent asked
389
- // for, so a replay can pin against the same version.
390
- toolVersion: resolvedVersion,
391
- toolVersionRange: requestedRange,
392
- },
393
- });
394
- ctx.provenance.addNode({
395
- id: toolResultNodeId,
396
- kind: 'tool-result',
397
- timestamp: dispatched.value.persisted.createdAt,
398
- });
399
- ctx.provenance.addEdge({
400
- from: toolCallNodeId,
401
- to: modelCallNodeId,
402
- kind: 'invoked',
403
- });
404
- ctx.provenance.addEdge({
405
- from: toolResultNodeId,
406
- to: toolCallNodeId,
407
- kind: 'produced',
408
- });
409
- }
410
426
  }
411
427
 
428
+ // Every call's nodes, once the step has all its results: the ones it
429
+ // ran, the rejected and failed ones, and those it took from before a
430
+ // park in this step.
431
+ addStepToolNodes(ctx, partial.step, iterationAppended);
432
+
412
433
  const forwarded: Partial<AgentTurnIterationOutput> & {
413
434
  readonly step: number;
414
435
  readonly hasToolCalls: true;
@@ -520,7 +541,12 @@ async function appendToolResult(
520
541
  target.iterationAppended.push(persisted.value);
521
542
  return [
522
543
  ...target.nextMessages,
523
- { role: 'tool', content: JSON.stringify(output), toolCallId: call.id },
544
+ {
545
+ role: 'tool',
546
+ // As a tool's own result reaches the model: a string as it is.
547
+ content: typeof output === 'string' ? output : JSON.stringify(output),
548
+ toolCallId: call.id,
549
+ },
524
550
  ];
525
551
  }
526
552
 
@@ -573,12 +599,17 @@ async function dispatchOne(
573
599
  const toolCtx: ToolContext = {
574
600
  tenantId: ctx.input.tenantId,
575
601
  runId,
602
+ // The run's own project and org, never the model's arguments.
603
+ projectId: ctx.input.projectId,
604
+ ...(ctx.input.orgId !== undefined && { orgId: ctx.input.orgId }),
576
605
  requestId: call.id,
577
606
  abortSignal: ctx.turnAbort.signal,
578
607
  // HTTP tools built via defineTool({spec: {kind: 'http'}}) resolve declared
579
608
  // secret_refs at invoke time. Present iff the caller wired
580
609
  // `bindings.resolveSecret` from a tenant-scoped `SecretBinding`.
581
610
  ...(ctx.bindings.resolveSecret !== undefined && { resolveSecret: ctx.bindings.resolveSecret }),
611
+ // The pinned settings blocks' values, by block id.
612
+ ...(ctx.blocks !== undefined && { settings: ctx.blocks.settings }),
582
613
  };
583
614
  const result = await invokeTool(tool, call.arguments, toolCtx);
584
615
  if (result.kind === 'err') {
@@ -16,6 +16,7 @@ export type InvokeAgentError =
16
16
  | AgentError
17
17
  | UnresolvedToolError
18
18
  | ToolVersionUnresolvableError
19
+ | BlockUnresolvableError
19
20
  | CapabilityRoutingError
20
21
  | ModelInvocationError
21
22
  | ToolInvocationError
@@ -75,6 +76,18 @@ export interface ToolVersionUnresolvableError {
75
76
  readonly availableVersions?: readonly string[];
76
77
  }
77
78
 
79
+ /**
80
+ * A data block the agent references can't be loaded: no version in its
81
+ * range, a pinned version that's gone, the wrong kind, model settings
82
+ * that aren't, or a runtime that serves no blocks.
83
+ */
84
+ export interface BlockUnresolvableError {
85
+ readonly code: 'block-unresolvable';
86
+ readonly message: string;
87
+ readonly blockId: string;
88
+ readonly requestedRange?: string;
89
+ }
90
+
78
91
  export interface CapabilityRoutingError {
79
92
  readonly code: 'capability-routing-failed';
80
93
  readonly message: string;
@@ -1,7 +1,9 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
- import type { NodeHandler } from '@kindgi/handler';
4
+ import type { UsageSink } from '@kindgi/capabilities';
5
+ import type { EvaluationOutcome } from '@kindgi/guardrails';
6
+ import type { NodeContext, NodeHandler } from '@kindgi/handler';
5
7
  import type { Timestamp } from '@kindgi/types';
6
8
 
7
9
  import {
@@ -16,6 +18,7 @@ import type { ConversationMessage } from '../types.js';
16
18
  import type { TurnContext } from './context.js';
17
19
  import { throwAgentTurnFailure } from './errors.js';
18
20
  import { finalIteration } from './final-iteration.js';
21
+ import { replayTag } from './replay.js';
19
22
  import { parseJsonAnswer } from './structured-output.js';
20
23
 
21
24
  /**
@@ -77,6 +80,7 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
77
80
  runId: kctx.runId,
78
81
  tenantId: ctx.input.tenantId,
79
82
  projectId: ctx.input.projectId,
83
+ ...(ctx.input.orgId !== undefined && { orgId: ctx.input.orgId }),
80
84
  conversationId: ctx.input.conversationId,
81
85
  turnNumber: (ctx.conversation?.turnCount ?? 0) + 1,
82
86
  agent: ctx.input.agent,
@@ -93,7 +97,9 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
93
97
  ctx.bindings,
94
98
  ctx.tenantPolicy,
95
99
  ctx.turnAbort.signal,
100
+ judgeUsageSink(ctx, kctx),
96
101
  );
102
+ throwIfJudgeCallsUnrecorded(outcomes);
97
103
  const categorized = categorizeOutcomes(outcomes);
98
104
 
99
105
  const allViolations = [...categorized.blocking, ...categorized.warnings, ...categorized.other];
@@ -151,3 +157,37 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
151
157
  };
152
158
  };
153
159
  }
160
+
161
+ /**
162
+ * The turn's usage sink for its llm-judge calls, adding what only the
163
+ * turn knows: the step that made them and the agent's version.
164
+ */
165
+ function judgeUsageSink(ctx: TurnContext, kctx: NodeContext): UsageSink | undefined {
166
+ const sink = ctx.bindings.usage;
167
+ if (sink === undefined) return undefined;
168
+ return {
169
+ record: (call) =>
170
+ sink.record({
171
+ nodeId: kctx.nodeId as unknown as string,
172
+ agentVersion: ctx.input.agent.version,
173
+ ...(ctx.input.replay !== undefined && { replay: replayTag(ctx.input.replay) }),
174
+ ...call,
175
+ }),
176
+ };
177
+ }
178
+
179
+ /**
180
+ * A judge call that answered but couldn't be recorded fails the step, as
181
+ * the turn's own model calls do: no answered call is left unrecorded.
182
+ */
183
+ function throwIfJudgeCallsUnrecorded(outcomes: readonly EvaluationOutcome[]): void {
184
+ for (const outcome of outcomes) {
185
+ if (outcome.kind === 'err' && outcome.error.code === 'judge-usage-unrecorded') {
186
+ throwAgentTurnFailure({
187
+ code: 'persistence-error',
188
+ message: outcome.error.message,
189
+ cause: outcome.error,
190
+ });
191
+ }
192
+ }
193
+ }