@kindgi/agents 0.1.3 → 0.1.4-rc.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 (119) hide show
  1. package/dist/blocks.d.ts +103 -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/define.d.ts +17 -2
  6. package/dist/define.d.ts.map +1 -1
  7. package/dist/define.js +73 -6
  8. package/dist/define.js.map +1 -1
  9. package/dist/handlers/compose-result.d.ts.map +1 -1
  10. package/dist/handlers/compose-result.js +3 -0
  11. package/dist/handlers/compose-result.js.map +1 -1
  12. package/dist/handlers/context.d.ts +10 -0
  13. package/dist/handlers/context.d.ts.map +1 -1
  14. package/dist/handlers/dispatch-tools.d.ts.map +1 -1
  15. package/dist/handlers/dispatch-tools.js +51 -1
  16. package/dist/handlers/dispatch-tools.js.map +1 -1
  17. package/dist/handlers/errors.d.ts +12 -1
  18. package/dist/handlers/errors.d.ts.map +1 -1
  19. package/dist/handlers/errors.js.map +1 -1
  20. package/dist/handlers/evaluate-guardrails.d.ts.map +1 -1
  21. package/dist/handlers/evaluate-guardrails.js +2 -0
  22. package/dist/handlers/evaluate-guardrails.js.map +1 -1
  23. package/dist/handlers/model-call.d.ts.map +1 -1
  24. package/dist/handlers/model-call.js +11 -0
  25. package/dist/handlers/model-call.js.map +1 -1
  26. package/dist/handlers/public-types.d.ts +21 -1
  27. package/dist/handlers/public-types.d.ts.map +1 -1
  28. package/dist/handlers/rehydrate.d.ts.map +1 -1
  29. package/dist/handlers/rehydrate.js +6 -1
  30. package/dist/handlers/rehydrate.js.map +1 -1
  31. package/dist/handlers/render-prompt.d.ts.map +1 -1
  32. package/dist/handlers/render-prompt.js +4 -1
  33. package/dist/handlers/render-prompt.js.map +1 -1
  34. package/dist/handlers/replay.d.ts +143 -0
  35. package/dist/handlers/replay.d.ts.map +1 -0
  36. package/dist/handlers/replay.js +177 -0
  37. package/dist/handlers/replay.js.map +1 -0
  38. package/dist/handlers/resolve-blocks.d.ts +32 -0
  39. package/dist/handlers/resolve-blocks.d.ts.map +1 -0
  40. package/dist/handlers/resolve-blocks.js +129 -0
  41. package/dist/handlers/resolve-blocks.js.map +1 -0
  42. package/dist/handlers/resolve-tools.d.ts +6 -2
  43. package/dist/handlers/resolve-tools.d.ts.map +1 -1
  44. package/dist/handlers/resolve-tools.js +59 -27
  45. package/dist/handlers/resolve-tools.js.map +1 -1
  46. package/dist/handlers/result-shape.d.ts +7 -0
  47. package/dist/handlers/result-shape.d.ts.map +1 -1
  48. package/dist/handlers/result-shape.js.map +1 -1
  49. package/dist/handlers/run-retrievals.d.ts.map +1 -1
  50. package/dist/handlers/run-retrievals.js +34 -17
  51. package/dist/handlers/run-retrievals.js.map +1 -1
  52. package/dist/handlers/run-snapshot.d.ts.map +1 -1
  53. package/dist/handlers/run-snapshot.js +1 -0
  54. package/dist/handlers/run-snapshot.js.map +1 -1
  55. package/dist/handlers/setup.d.ts +2 -0
  56. package/dist/handlers/setup.d.ts.map +1 -1
  57. package/dist/handlers/setup.js +14 -2
  58. package/dist/handlers/setup.js.map +1 -1
  59. package/dist/handlers/turn-environment.d.ts +13 -2
  60. package/dist/handlers/turn-environment.d.ts.map +1 -1
  61. package/dist/handlers/turn-environment.js +12 -3
  62. package/dist/handlers/turn-environment.js.map +1 -1
  63. package/dist/index.d.ts +8 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +4 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/invoke.d.ts.map +1 -1
  68. package/dist/invoke.js +3 -0
  69. package/dist/invoke.js.map +1 -1
  70. package/dist/pins.d.ts +54 -0
  71. package/dist/pins.d.ts.map +1 -0
  72. package/dist/pins.js +51 -0
  73. package/dist/pins.js.map +1 -0
  74. package/dist/prompt.d.ts +12 -2
  75. package/dist/prompt.d.ts.map +1 -1
  76. package/dist/prompt.js +42 -5
  77. package/dist/prompt.js.map +1 -1
  78. package/dist/run-snapshot-binding.d.ts +5 -0
  79. package/dist/run-snapshot-binding.d.ts.map +1 -1
  80. package/dist/schema.d.ts +17 -0
  81. package/dist/schema.d.ts.map +1 -1
  82. package/dist/schema.js +5 -0
  83. package/dist/schema.js.map +1 -1
  84. package/dist/streaming.d.ts +5 -0
  85. package/dist/streaming.d.ts.map +1 -1
  86. package/dist/streaming.js.map +1 -1
  87. package/dist/types.d.ts +50 -1
  88. package/dist/types.d.ts.map +1 -1
  89. package/migrations/0004_stormy_moondragon.sql +1 -0
  90. package/migrations/meta/0004_snapshot.json +327 -0
  91. package/migrations/meta/_journal.json +7 -0
  92. package/package.json +15 -15
  93. package/src/blocks.ts +233 -0
  94. package/src/define.ts +84 -7
  95. package/src/handlers/compose-result.ts +3 -0
  96. package/src/handlers/context.ts +10 -0
  97. package/src/handlers/dispatch-tools.ts +54 -1
  98. package/src/handlers/errors.ts +13 -0
  99. package/src/handlers/evaluate-guardrails.ts +2 -0
  100. package/src/handlers/model-call.ts +14 -0
  101. package/src/handlers/public-types.ts +22 -1
  102. package/src/handlers/rehydrate.ts +21 -6
  103. package/src/handlers/render-prompt.ts +13 -7
  104. package/src/handlers/replay.ts +314 -0
  105. package/src/handlers/resolve-blocks.ts +188 -0
  106. package/src/handlers/resolve-tools.ts +76 -28
  107. package/src/handlers/result-shape.ts +8 -0
  108. package/src/handlers/run-retrievals.ts +44 -23
  109. package/src/handlers/run-snapshot.ts +1 -0
  110. package/src/handlers/setup.ts +13 -2
  111. package/src/handlers/turn-environment.ts +28 -3
  112. package/src/index.ts +37 -0
  113. package/src/invoke.ts +3 -0
  114. package/src/pins.ts +98 -0
  115. package/src/prompt.ts +53 -5
  116. package/src/run-snapshot-binding.ts +6 -0
  117. package/src/schema.ts +5 -0
  118. package/src/streaming.ts +5 -0
  119. package/src/types.ts +53 -1
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>();
@@ -566,7 +640,10 @@ function buildAgent(spec: DefineAgentSpec, output: AgentOutputSpec | undefined):
566
640
  version: spec.version as Semver,
567
641
  name: spec.name,
568
642
  ...(spec.description !== undefined && { description: spec.description }),
569
- 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 } }),
570
647
  capabilities: spec.capabilities.map((c) => ({ ...c })),
571
648
  tools: [...spec.tools],
572
649
  retrieval: spec.retrieval.map((r) => ({ ...r })),
@@ -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
  /**
@@ -189,6 +192,13 @@ export interface TurnContext {
189
192
  * `AgentTurnResult.violations`.
190
193
  */
191
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';
192
202
  /**
193
203
  * Reason recorded when `turnAbort` fires. Used to distinguish
194
204
  * external cancellation from wall-clock timeout in the projected
@@ -20,6 +20,7 @@ import {
20
20
  throwAgentTurnFailure,
21
21
  } from './errors.js';
22
22
  import { TOOL_CALL_GATE_SUBJECT, readGateDecision } from './gate-decision.js';
23
+ import { decideReplayTool } from './replay.js';
23
24
  import {
24
25
  effectiveToolErrorPolicy,
25
26
  toolErrorKindOf,
@@ -244,6 +245,50 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
244
245
  // skip the reviewer's answer. Cross-turn `ask_on_first_use` caching
245
246
  // (via conversation metadata) is not implemented.
246
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
+
247
292
  let toolRejectionPayload: { readonly rationale?: string } | null = null;
248
293
  if (gate !== undefined) {
249
294
  const { argsHash, waitTokenId, timeoutMs } = gate;
@@ -376,6 +421,7 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
376
421
  invocationId: call.id,
377
422
  output: dispatched.value.persisted.content,
378
423
  durationMs: Date.now() - toolStarted,
424
+ ...(replayed !== undefined && { replay: replayed.kind }),
379
425
  });
380
426
  }
381
427
 
@@ -495,7 +541,12 @@ async function appendToolResult(
495
541
  target.iterationAppended.push(persisted.value);
496
542
  return [
497
543
  ...target.nextMessages,
498
- { 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
+ },
499
550
  ];
500
551
  }
501
552
 
@@ -557,6 +608,8 @@ async function dispatchOne(
557
608
  // secret_refs at invoke time. Present iff the caller wired
558
609
  // `bindings.resolveSecret` from a tenant-scoped `SecretBinding`.
559
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 }),
560
613
  };
561
614
  const result = await invokeTool(tool, call.arguments, toolCtx);
562
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;
@@ -18,6 +18,7 @@ import type { ConversationMessage } from '../types.js';
18
18
  import type { TurnContext } from './context.js';
19
19
  import { throwAgentTurnFailure } from './errors.js';
20
20
  import { finalIteration } from './final-iteration.js';
21
+ import { replayTag } from './replay.js';
21
22
  import { parseJsonAnswer } from './structured-output.js';
22
23
 
23
24
  /**
@@ -169,6 +170,7 @@ function judgeUsageSink(ctx: TurnContext, kctx: NodeContext): UsageSink | undefi
169
170
  sink.record({
170
171
  nodeId: kctx.nodeId as unknown as string,
171
172
  agentVersion: ctx.input.agent.version,
173
+ ...(ctx.input.replay !== undefined && { replay: replayTag(ctx.input.replay) }),
172
174
  ...call,
173
175
  }),
174
176
  };
@@ -18,6 +18,7 @@ import { emitTurnEvent } from '../streaming.js';
18
18
 
19
19
  import type { AgentTurnIterationOutput, TurnContext } from './context.js';
20
20
  import { throwAgentTurnFailure } from './errors.js';
21
+ import { replayTag } from './replay.js';
21
22
  import { addModelCallNode } from './turn-provenance.js';
22
23
 
23
24
  /**
@@ -83,6 +84,7 @@ export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
83
84
  ...(ctx.tools.definitions.length > 0 && {
84
85
  tools: ctx.tools.definitions,
85
86
  }),
87
+ ...modelSettingsOf(ctx),
86
88
  abortSignal: ctx.turnAbort.signal,
87
89
  };
88
90
 
@@ -251,6 +253,7 @@ async function recordCall(
251
253
  providerId: ctx.provider.metadata.id,
252
254
  model: ctx.model.name,
253
255
  ...(ctx.provider.metadata.fallback === true && { fallback: true }),
256
+ ...(input.replay !== undefined && { replay: replayTag(input.replay) }),
254
257
  occurredAt: new Date().toISOString(),
255
258
  });
256
259
  return recorded.kind === 'err' ? (recorded.error ?? new Error('no detail')) : undefined;
@@ -265,3 +268,14 @@ function describeCause(cause: unknown): string {
265
268
  if (text.length === 0) return cause instanceof Error ? cause.name : 'no detail';
266
269
  return text.length > MAX_CAUSE_CHARS ? `${text.slice(0, MAX_CAUSE_CHARS - 1)}…` : text;
267
270
  }
271
+
272
+ /** The pinned model-settings block's values, as model-call fields; none when it has none. */
273
+ function modelSettingsOf(
274
+ ctx: TurnContext,
275
+ ): Pick<ModelCallInput, 'temperature' | 'maxOutputTokens'> {
276
+ const settings = ctx.blocks?.modelSettings;
277
+ return {
278
+ ...(settings?.temperature !== undefined && { temperature: settings.temperature }),
279
+ ...(settings?.maxOutputTokens !== undefined && { maxOutputTokens: settings.maxOutputTokens }),
280
+ };
281
+ }
@@ -6,10 +6,11 @@ import type { ProviderRegistry, TenantPolicy, UsageSink } from '@kindgi/capabili
6
6
  import type { EmbeddingProviderRegistry } from '@kindgi/embedding';
7
7
  import type { MemoryQueryBinding } from '@kindgi/memory';
8
8
  import type { PolicyRegistry } from '@kindgi/policy-contract';
9
- import type { ParentRunRef, RunBinding } from '@kindgi/runtime';
9
+ import type { ParentRunRef, RunBinding, RunReplayRef } from '@kindgi/runtime';
10
10
  import type { ToolRegistry, ToolSecretRef } from '@kindgi/tools';
11
11
  import type { OrgId, ProjectId, ProvenanceId, RunId, TenantId, Timestamp } from '@kindgi/types';
12
12
 
13
+ import type { BlockReader } from '../blocks.js';
13
14
  import type { ConversationBinding } from '../conversation-binding.js';
14
15
  import type { GuardrailsBindings } from '../guardrails-gate.js';
15
16
  import type { ProvenanceBindings } from '../provenance-emit.js';
@@ -17,6 +18,8 @@ import type { RunSnapshotBinding } from '../run-snapshot-binding.js';
17
18
  import type { OnTurnEvent } from '../streaming.js';
18
19
  import type { Agent, ConversationId } from '../types.js';
19
20
 
21
+ import type { ReplayBinding } from './replay.js';
22
+
20
23
  /**
21
24
  * The user-facing input to `invokeAgent`. Split out from `invoke.ts` so
22
25
  * the handler modules can depend on the shape without pulling in the
@@ -54,6 +57,13 @@ export interface InvokeAgentInput {
54
57
  * step. Recorded on the turn's run, so the flow can find its child.
55
58
  */
56
59
  readonly parent?: ParentRunRef;
60
+ /**
61
+ * Marks the turn as a replay: an eval run re-running a past run (`of`)
62
+ * on this agent version. Recorded on the turn's run and in its snapshot.
63
+ * Its tool calls are decided by `InvokeAgentBindings.replay` (every call
64
+ * is refused when that isn't wired), and only a read-only tool can run.
65
+ */
66
+ readonly replay?: RunReplayRef;
57
67
  readonly participantId?: string;
58
68
  readonly abortSignal?: AbortSignal;
59
69
  /**
@@ -101,6 +111,12 @@ export interface InvokeAgentBindings extends GuardrailsBindings {
101
111
  * sees another tenant's tools.
102
112
  */
103
113
  readonly toolRegistry: ToolRegistry;
114
+ /**
115
+ * Data blocks (prompts and settings). Setup loads the blocks the agent
116
+ * references at their pinned versions. Optional: an agent that
117
+ * references blocks fails its turn (`block-unresolvable`) without it.
118
+ */
119
+ readonly blockReader?: BlockReader;
104
120
  /**
105
121
  * Caller-plugged data-access surface for memory reads.
106
122
  * The Kindgi runtime supplies a Postgres-backed implementation;
@@ -175,6 +191,11 @@ export interface InvokeAgentBindings extends GuardrailsBindings {
175
191
  * unaffected.
176
192
  */
177
193
  readonly resolveSecret?: (ref: ToolSecretRef) => Promise<string>;
194
+ /**
195
+ * How replay turns (`InvokeAgentInput.replay`) decide their tool calls,
196
+ * retrievals and session approval. Consulted only for a replay turn.
197
+ */
198
+ readonly replay?: ReplayBinding;
178
199
  }
179
200
 
180
201
  export interface HitlBindings {
@@ -10,7 +10,9 @@
10
10
  *
11
11
  * - the environment — conversation, approval rules, guardrails, tools,
12
12
  * policies — is resolved again, routed to the provider and model
13
- * `setup` journaled;
13
+ * `setup` journaled, with the tool versions it journaled (a range
14
+ * isn't resolved again: a version published meanwhile doesn't run
15
+ * mid-turn);
14
16
  * - the messages the turn stored come back from the conversation, from
15
17
  * the turn's user message (`persist-user-message` journals its
16
18
  * sequence);
@@ -46,6 +48,8 @@ import type { ConversationMessage, RetrievedFact } from '../types.js';
46
48
  import type { TurnContext } from './context.js';
47
49
  import { throwAgentTurnFailure } from './errors.js';
48
50
  import { readGateDecision } from './gate-decision.js';
51
+ import { rehydrateReplay } from './replay.js';
52
+ import type { PinnedBlockVersions } from './resolve-blocks.js';
49
53
  import { TOOL_GATE_RECORD_PREFIX } from './tool-hitl.js';
50
54
  import {
51
55
  loadTurnConversation,
@@ -107,10 +111,12 @@ export async function rehydrateTurnContext(
107
111
  journal: readonly JournalEntry[],
108
112
  ): Promise<boolean> {
109
113
  const steps = completedSteps(journal);
110
- const setup = outputOf<{ readonly providerId: string; readonly providerModel: string }>(
111
- steps,
112
- 'setup',
113
- );
114
+ const setup = outputOf<{
115
+ readonly providerId: string;
116
+ readonly providerModel: string;
117
+ readonly toolVersions?: Readonly<Record<string, string>>;
118
+ readonly blockVersions?: PinnedBlockVersions;
119
+ }>(steps, 'setup');
114
120
  if (setup === undefined) return false;
115
121
 
116
122
  if (ctx.bindings.provenance?.newBuilder !== undefined) {
@@ -119,7 +125,15 @@ export async function rehydrateTurnContext(
119
125
  }
120
126
  await loadTurnConversation(ctx);
121
127
  ctx.hitlPolicy = await resolveTurnHitlPolicy(ctx);
122
- await resolveTurnEnvironment(ctx, { providerId: setup.providerId, model: setup.providerModel });
128
+ // The route and the tool versions `setup` resolved: a resumed turn
129
+ // runs those, never a range resolved again (a journal from before
130
+ // `toolVersions` was recorded resolves the ranges, as it did).
131
+ await resolveTurnEnvironment(
132
+ ctx,
133
+ { providerId: setup.providerId, model: setup.providerModel },
134
+ setup.toolVersions,
135
+ setup.blockVersions,
136
+ );
123
137
 
124
138
  const userMessage = outputOf<{ readonly sequence: number }>(steps, 'persist-user-message');
125
139
  if (userMessage !== undefined) {
@@ -151,6 +165,7 @@ export async function rehydrateTurnContext(
151
165
  }
152
166
  ctx.toolApprovals = toolApprovalsOf(journal);
153
167
  rebuildProvenance(ctx, steps, retrievals?.retrieved);
168
+ rehydrateReplay(ctx, journal);
154
169
  return true;
155
170
  }
156
171
 
@@ -25,14 +25,20 @@ export function buildRenderPromptHandler(ctx: TurnContext): NodeHandler {
25
25
  cause: null,
26
26
  });
27
27
  }
28
- const rendered = renderInstructions(ctx.input.agent, {
29
- parameters: ctx.input.parameters ?? {},
30
- ...(ctx.input.input !== undefined && { input: ctx.input.input }),
31
- conversation: {
32
- id: ctx.input.conversationId,
33
- turn: ctx.conversation.turnCount + 1,
28
+ const rendered = renderInstructions(
29
+ ctx.input.agent,
30
+ {
31
+ parameters: ctx.input.parameters ?? {},
32
+ ...(ctx.input.input !== undefined && { input: ctx.input.input }),
33
+ conversation: {
34
+ id: ctx.input.conversationId,
35
+ turn: ctx.conversation.turnCount + 1,
36
+ },
37
+ ...(ctx.blocks !== undefined && { settings: ctx.blocks.settings }),
34
38
  },
35
- });
39
+ // The pinned prompt block, when the instructions come from one.
40
+ ctx.blocks?.prompt?.content,
41
+ );
36
42
  if (!rendered.ok) {
37
43
  throwAgentTurnFailure({
38
44
  code: 'model-invocation-failed',