@kindgi/agents 0.1.3 → 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 (119) 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/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 +41 -4
  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 +231 -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 +52 -4
  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
@@ -1,10 +1,11 @@
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 { NodeContext, NodeHandler } from '@kindgi/handler';
5
5
 
6
6
  import { runRetrievals } from '../retrieval.js';
7
7
  import { emitTurnEvent } from '../streaming.js';
8
+ import type { RetrievedFact } from '../types.js';
8
9
 
9
10
  import type { TurnContext } from './context.js';
10
11
  import { throwAgentTurnFailure } from './errors.js';
@@ -17,7 +18,7 @@ import { addRetrievalNodes } from './turn-provenance.js';
17
18
  * is wired.
18
19
  */
19
20
  export function buildRunRetrievalsHandler(ctx: TurnContext): NodeHandler {
20
- return async () => {
21
+ return async (_input, kctx) => {
21
22
  if (ctx.conversation === undefined || ctx.userMessage === undefined) {
22
23
  throwAgentTurnFailure({
23
24
  code: 'model-invocation-failed',
@@ -25,36 +26,56 @@ export function buildRunRetrievalsHandler(ctx: TurnContext): NodeHandler {
25
26
  cause: null,
26
27
  });
27
28
  }
28
- const retrieved = await runRetrievals(
29
- ctx.input.agent,
30
- ctx.conversation,
31
- ctx.input.conversationId,
32
- ctx.input.userMessage,
33
- {
34
- memory: ctx.bindings.memoryBinding,
35
- ...(ctx.bindings.embeddingRegistry !== undefined && {
36
- embeddingRegistry: ctx.bindings.embeddingRegistry,
37
- }),
38
- ...(ctx.bindings.embeddingModel !== undefined && {
39
- embeddingModel: ctx.bindings.embeddingModel,
40
- }),
41
- },
42
- );
43
- if (retrieved.kind === 'err') throwAgentTurnFailure(retrieved.error);
44
- ctx.retrieved = retrieved.value;
29
+ const facts = (await recordedRetrievals(ctx, kctx)) ?? (await retrieveLive(ctx));
30
+ ctx.retrieved = facts;
45
31
 
46
32
  await emitTurnEvent(ctx.bindings.onEvent, {
47
33
  kind: 'retrieval.completed',
48
- count: retrieved.value.length,
49
- factIds: retrieved.value.map((r) => r.fact.id),
34
+ count: facts.length,
35
+ factIds: facts.map((r) => r.fact.id),
50
36
  });
51
37
 
52
38
  if (ctx.provenance !== undefined && ctx.userMessage !== undefined) {
53
- addRetrievalNodes(ctx.provenance, retrieved.value, ctx.userMessage);
39
+ addRetrievalNodes(ctx.provenance, facts, ctx.userMessage);
54
40
  }
55
41
 
56
42
  // The facts go in the journal: a resumed turn restores them from it
57
43
  // (`rehydrateTurnContext`) rather than retrieving again.
58
- return { count: retrieved.value.length, retrieved: retrieved.value };
44
+ return { count: facts.length, retrieved: facts };
59
45
  };
60
46
  }
47
+
48
+ /** A replay's retrievals: what the past run retrieved, when the replay binding has it. */
49
+ async function recordedRetrievals(
50
+ ctx: TurnContext,
51
+ kctx: NodeContext,
52
+ ): Promise<readonly RetrievedFact[] | undefined> {
53
+ const replay = ctx.input.replay;
54
+ if (replay === undefined || ctx.bindings.replay?.retrievals === undefined) return undefined;
55
+ return ctx.bindings.replay.retrievals({
56
+ tenantId: ctx.input.tenantId,
57
+ runId: kctx.runId,
58
+ replay,
59
+ });
60
+ }
61
+
62
+ async function retrieveLive(ctx: TurnContext): Promise<readonly RetrievedFact[]> {
63
+ if (ctx.conversation === undefined) return [];
64
+ const retrieved = await runRetrievals(
65
+ ctx.input.agent,
66
+ ctx.conversation,
67
+ ctx.input.conversationId,
68
+ ctx.input.userMessage,
69
+ {
70
+ memory: ctx.bindings.memoryBinding,
71
+ ...(ctx.bindings.embeddingRegistry !== undefined && {
72
+ embeddingRegistry: ctx.bindings.embeddingRegistry,
73
+ }),
74
+ ...(ctx.bindings.embeddingModel !== undefined && {
75
+ embeddingModel: ctx.bindings.embeddingModel,
76
+ }),
77
+ },
78
+ );
79
+ if (retrieved.kind === 'err') throwAgentTurnFailure(retrieved.error);
80
+ return retrieved.value;
81
+ }
@@ -40,5 +40,6 @@ export async function writeRunSnapshot(ctx: TurnContext, kctx: NodeContext): Pro
40
40
  ...(ctx.input.dryRun === true && { dryRun: true }),
41
41
  ...(ctx.input.principal !== undefined && { principal: ctx.input.principal }),
42
42
  ...(ctx.input.authz !== undefined && { authz: ctx.input.authz }),
43
+ ...(ctx.input.replay !== undefined && { replay: ctx.input.replay }),
43
44
  });
44
45
  }
@@ -12,6 +12,7 @@ import { emitTurnEvent } from '../streaming.js';
12
12
  import type { TurnContext } from './context.js';
13
13
  import { throwAgentTurnFailure } from './errors.js';
14
14
  import { SESSION_GATE_SUBJECT, readGateDecision } from './gate-decision.js';
15
+ import { followReplaySessionGate } from './replay.js';
15
16
  import { writeRunSnapshot } from './run-snapshot.js';
16
17
  import {
17
18
  loadTurnConversation,
@@ -37,7 +38,7 @@ function computeSessionGateWaitToken(input: {
37
38
  }
38
39
 
39
40
  /** The `record` key of the session gate's decision. */
40
- const SESSION_GATE_RECORD = 'session-hitl-gate';
41
+ export const SESSION_GATE_RECORD = 'session-hitl-gate';
41
42
 
42
43
  /**
43
44
  * The session gate's decision, as `setup` records it the first time
@@ -140,7 +141,11 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
140
141
  timeoutMs: effectiveHitl.timeoutMs,
141
142
  };
142
143
  });
143
- if (gate !== undefined) {
144
+ if (gate !== undefined && ctx.input.replay !== undefined) {
145
+ // A replay follows the past run's decision at this gate, or skips it
146
+ // when none was recorded; it never waits for a reviewer.
147
+ await followReplaySessionGate(ctx, kctx);
148
+ } else if (gate !== undefined) {
144
149
  const { waitTokenId, timeoutMs } = gate;
145
150
  const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
146
151
 
@@ -290,6 +295,12 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
290
295
  providerId: environment.providerId,
291
296
  providerModel: environment.model,
292
297
  toolCount: environment.toolCount,
298
+ // The version each tool resolved to: a resumed turn runs these.
299
+ toolVersions: environment.toolVersions,
300
+ // And each data block's.
301
+ ...(environment.blockVersions !== undefined && {
302
+ blockVersions: environment.blockVersions,
303
+ }),
293
304
  };
294
305
  };
295
306
  }
@@ -21,6 +21,7 @@ import { mergeTenantPolicies } from '../tenant-policy.js';
21
21
  import type { Conversation } from '../types.js';
22
22
  import type { TurnContext } from './context.js';
23
23
  import { throwAgentTurnFailure } from './errors.js';
24
+ import { type PinnedBlockVersions, resolveTurnBlocks } from './resolve-blocks.js';
24
25
  import { resolveTurnTools } from './resolve-tools.js';
25
26
  import { type ToolErrorPolicy, effectiveToolErrorPolicy } from './tool-errors.js';
26
27
 
@@ -61,16 +62,33 @@ export interface PinnedRoute {
61
62
  readonly model: string;
62
63
  }
63
64
 
65
+ /**
66
+ * The tool version each of the agent's tool references resolved to, by
67
+ * tool id, as `setup` journals it: a resumed turn runs these versions,
68
+ * whatever the registry holds by then.
69
+ */
70
+ export type PinnedToolVersions = Readonly<Record<string, string>>;
71
+
64
72
  /**
65
73
  * Resolve the turn's guardrails, tools, tenant policy, tool-error policy
66
74
  * and model onto `ctx`. With `pinned`, the route is that provider and
67
75
  * model, still under the tenant's current policy; one no longer
68
- * registered or allowed fails the turn.
76
+ * registered or allowed fails the turn. With `pinnedTools`, each tool is
77
+ * that exact version, not its range resolved again; one no longer
78
+ * registered fails the turn.
69
79
  */
70
80
  export async function resolveTurnEnvironment(
71
81
  ctx: TurnContext,
72
82
  pinned?: PinnedRoute,
73
- ): Promise<PinnedRoute & { readonly toolCount: number }> {
83
+ pinnedTools?: PinnedToolVersions,
84
+ pinnedBlocks?: PinnedBlockVersions,
85
+ ): Promise<
86
+ PinnedRoute & {
87
+ readonly toolCount: number;
88
+ readonly toolVersions: PinnedToolVersions;
89
+ readonly blockVersions?: PinnedBlockVersions;
90
+ }
91
+ > {
74
92
  const invResolution = resolveGuardrails(ctx.input.agent, ctx.bindings);
75
93
  if (invResolution.missing.length > 0) {
76
94
  throwAgentTurnFailure({
@@ -84,7 +102,10 @@ export async function resolveTurnEnvironment(
84
102
  // This turn's tools come from the tenant's own registry — never a
85
103
  // registry shared across concurrent turns of other tenants.
86
104
  const tenantTools = await ctx.bindings.toolRegistry.forTenant(ctx.input.tenantId);
87
- ctx.tools = resolveTurnTools(tenantTools, ctx.input.agent);
105
+ ctx.tools = resolveTurnTools(tenantTools, ctx.input.agent, pinnedTools);
106
+ // The data blocks, at the versions pinned (the turn's own on resume).
107
+ const blocks = await resolveTurnBlocks(ctx, pinnedBlocks);
108
+ if (blocks !== undefined) ctx.blocks = blocks;
88
109
 
89
110
  const capability = ctx.input.agent.capabilities[0];
90
111
  if (capability === undefined) {
@@ -151,6 +172,10 @@ export async function resolveTurnEnvironment(
151
172
  providerId: routed.value.provider.metadata.id,
152
173
  model: routed.value.model.name,
153
174
  toolCount: ctx.tools.definitions.length,
175
+ toolVersions: Object.fromEntries(
176
+ [...ctx.tools.byName].map(([id, binding]) => [id, binding.resolvedVersion]),
177
+ ),
178
+ ...(blocks !== undefined && { blockVersions: blocks.versions }),
154
179
  };
155
180
  }
156
181
 
package/src/index.ts CHANGED
@@ -17,6 +17,22 @@ export type {
17
17
  RunSnapshotWriteInput,
18
18
  } from './run-snapshot-binding.js';
19
19
  export { defineAgent } from './define.js';
20
+ export {
21
+ BLOCK_KINDS,
22
+ MODEL_SETTINGS_SCHEMA,
23
+ settingsSchemaIssues,
24
+ validateBlock,
25
+ } from './blocks.js';
26
+ export type {
27
+ BlockDefinition,
28
+ BlockIssue,
29
+ BlockKind,
30
+ BlockReader,
31
+ InvalidBlock,
32
+ ModelSettings,
33
+ PromptBlockContent,
34
+ SettingsBlockContent,
35
+ } from './blocks.js';
20
36
  export type { DefineAgentSpec } from './define.js';
21
37
  export { resolveEffectiveHitlPolicy } from './hitl-policy.js';
22
38
  export {
@@ -28,6 +44,17 @@ export {
28
44
  export type { GateDecision, GateDecisionValue } from './handlers/gate-decision.js';
29
45
  export type { EffectiveHitlPolicy } from './hitl-policy.js';
30
46
  export { agentStepOutput, invokeAgent, resumeAgentTurn } from './invoke.js';
47
+ export { isReadOnlyTool } from './handlers/replay.js';
48
+ export { SESSION_GATE_RECORD } from './handlers/setup.js';
49
+ export type {
50
+ ReplayApproval,
51
+ ReplayBinding,
52
+ ReplayToolDecision,
53
+ ReplayToolInput,
54
+ ReplayToolTrace,
55
+ ReplayTurnRef,
56
+ ReplayTurnReport,
57
+ } from './handlers/replay.js';
31
58
  export type {
32
59
  AgentStepOutput,
33
60
  AgentTurnAbortedError,
@@ -87,6 +114,14 @@ export type {
87
114
  RenderFailureError,
88
115
  RenderResult,
89
116
  } from './prompt.js';
117
+ export { pinChanges, pinsDigest } from './pins.js';
118
+ export type {
119
+ AgentDerivation,
120
+ AgentDerivationReason,
121
+ AgentPins,
122
+ PinChange,
123
+ PinSet,
124
+ } from './pins.js';
90
125
  export { createAgentRegistry } from './registry.js';
91
126
  export type { AgentRegistry } from './registry.js';
92
127
  export {
@@ -105,12 +140,14 @@ export type {
105
140
  AgentBindings,
106
141
  AgentId,
107
142
  AgentOutputSpec,
143
+ BlockRef,
108
144
  Conversation,
109
145
  ConversationId,
110
146
  ConversationMessage,
111
147
  ConversationPolicy,
112
148
  MessageRole,
113
149
  PromptParameter,
150
+ PromptRef,
114
151
  RetrievalIntent,
115
152
  RetrievedFact,
116
153
  ToolRef,
package/src/invoke.ts CHANGED
@@ -90,6 +90,8 @@ export async function invokeAgent(
90
90
  conversationId: input.conversationId,
91
91
  },
92
92
  ...(input.parent !== undefined && { parent: input.parent }),
93
+ // A replay's run says so, and which eval run and past run it is for.
94
+ ...(input.replay !== undefined && { replay: input.replay }),
93
95
  ...(input.dryRun === true && { options: { dryRun: true } }),
94
96
  // Authorization — carry principal + authz into the run so every
95
97
  // tool invocation inside the agent's turn is checked.
@@ -323,5 +325,6 @@ export function turnInputFromSnapshot(
323
325
  snapshot.authz !== null && {
324
326
  authz: snapshot.authz as { readonly fgaApiUrl: string },
325
327
  }),
328
+ ...(snapshot.replay !== undefined && snapshot.replay !== null && { replay: snapshot.replay }),
326
329
  };
327
330
  }
package/src/pins.ts ADDED
@@ -0,0 +1,98 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import { createHash } from 'node:crypto';
5
+
6
+ import { canonicalize } from '@kindgi/schema';
7
+ import type { VersionDerivation, VersionDerivationReason } from '@kindgi/types';
8
+
9
+ /**
10
+ * The exact version of each block an agent version runs: its lockfile.
11
+ *
12
+ * An agent names its tools by range (`{ id: 'acme.lookup', version:
13
+ * '^1.0.0' }`), like `package.json`. When a version of the agent is
14
+ * published, the runtime resolves each range once, and every run of that
15
+ * version uses the versions recorded here. So a new tool version reaches
16
+ * an agent only through a new agent version, and two runs of one agent
17
+ * version always run the same blocks.
18
+ *
19
+ * Set by the runtime at publish, never authored. A version published
20
+ * before pins existed has none and resolves its ranges per run.
21
+ */
22
+ export interface AgentPins {
23
+ /** Tool id → the exact version this agent version runs. */
24
+ readonly tools: Readonly<Record<string, string>>;
25
+ /** Prompt block id → exact version. Empty until the agent references prompt blocks. */
26
+ readonly prompts: Readonly<Record<string, string>>;
27
+ /** Settings block id → exact version. Empty until the agent references settings blocks. */
28
+ readonly settings: Readonly<Record<string, string>>;
29
+ }
30
+
31
+ /**
32
+ * One string naming a set of pins: `sha256:<hex>` of the pins'
33
+ * canonical JSON (keys sorted, no whitespace). Two agent versions with
34
+ * the same digest run the same blocks; the digest is what a comparison
35
+ * of versions, or a gate, records and compares. The Python SDK's
36
+ * `pins_digest` computes the same string.
37
+ */
38
+ export function pinsDigest(pins: AgentPins): string {
39
+ const canonical = canonicalize({
40
+ tools: pins.tools,
41
+ prompts: pins.prompts,
42
+ settings: pins.settings,
43
+ });
44
+ return `sha256:${createHash('sha256').update(canonical, 'utf8').digest('hex')}`;
45
+ }
46
+
47
+ /** Why a deploy registered an agent version under another number (`VersionDerivationReason`). */
48
+ export type AgentDerivationReason = VersionDerivationReason;
49
+
50
+ /** The version an agent version was registered in place of, and why. */
51
+ export type AgentDerivation = VersionDerivation;
52
+
53
+ /** One pin that differs between two versions of an agent or a flow. */
54
+ export interface PinChange {
55
+ readonly kind: 'tool' | 'prompt' | 'setting' | 'agent';
56
+ readonly id: string;
57
+ /** The earlier version's pin; absent when it didn't pin this block. */
58
+ readonly from?: string;
59
+ /** The later version's pin; absent when it doesn't pin this block. */
60
+ readonly to?: string;
61
+ }
62
+
63
+ const PIN_KINDS = [
64
+ ['tools', 'tool'],
65
+ ['prompts', 'prompt'],
66
+ ['settings', 'setting'],
67
+ ['agents', 'agent'],
68
+ ] as const;
69
+
70
+ /** A version's pins by kind: an agent's (`AgentPins`) or a flow's (`FlowPins`). */
71
+ export type PinSet = Partial<
72
+ Record<(typeof PIN_KINDS)[number][0], Readonly<Record<string, string>>>
73
+ >;
74
+
75
+ /**
76
+ * The pins that differ from `before` to `after`, by kind then id. With
77
+ * no `before` (a version published before pins), every pin of `after`
78
+ * is a change.
79
+ */
80
+ export function pinChanges(before: PinSet | undefined, after: PinSet): PinChange[] {
81
+ const changes: PinChange[] = [];
82
+ for (const [key, kind] of PIN_KINDS) {
83
+ const was = before?.[key] ?? {};
84
+ const now = after[key] ?? {};
85
+ for (const id of [...new Set([...Object.keys(was), ...Object.keys(now)])].sort()) {
86
+ const from = was[id];
87
+ const to = now[id];
88
+ if (from === to) continue;
89
+ changes.push({
90
+ kind,
91
+ id,
92
+ ...(from !== undefined && { from }),
93
+ ...(to !== undefined && { to }),
94
+ });
95
+ }
96
+ }
97
+ return changes;
98
+ }
package/src/prompt.ts CHANGED
@@ -3,7 +3,8 @@
3
3
 
4
4
  import { Liquid } from 'liquidjs';
5
5
 
6
- import type { Agent, PromptParameter } from './types.js';
6
+ import type { PromptBlockContent } from './blocks.js';
7
+ import type { Agent, PromptParameter, PromptRef } from './types.js';
7
8
 
8
9
  /**
9
10
  * Reserved variable namespaces the runtime injects automatically.
@@ -20,8 +21,18 @@ import type { Agent, PromptParameter } from './types.js';
20
21
  * - `input` — the turn's structured input (`InvokeAgentInput.input`),
21
22
  * e.g. `{{ input.grievance.summary }}`; unset when
22
23
  * the turn has none.
24
+ * - `settings` — the agent's settings blocks' values, by block id:
25
+ * `{{ settings["acme.weights"].recency }}` reads
26
+ * block `acme.weights`; unset when it has none.
23
27
  */
24
- export const AUTO_INJECTED_VARS = ['today', 'now', 'agent', 'conversation', 'input'] as const;
28
+ export const AUTO_INJECTED_VARS = [
29
+ 'today',
30
+ 'now',
31
+ 'agent',
32
+ 'conversation',
33
+ 'input',
34
+ 'settings',
35
+ ] as const;
25
36
 
26
37
  /**
27
38
  * Shape passed to `renderInstructions`. Framework auto-vars are
@@ -37,6 +48,8 @@ export interface RenderContext {
37
48
  };
38
49
  /** The turn's structured input, rendered as `{{ input.* }}`. */
39
50
  readonly input?: unknown;
51
+ /** The agent's settings blocks' values, by block id: `{{ settings["<id>"].<key> }}`. */
52
+ readonly settings?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
40
53
  /**
41
54
  * Optional clock override — tests inject a fixed date. Defaults to
42
55
  * `new Date()`.
@@ -84,14 +97,21 @@ export interface RenderFailureError {
84
97
  * Uses `strictVariables: true` so an unresolved `{{ var }}` is an error
85
98
  * — never a silent empty string. This is load-bearing for correctness
86
99
  * (a system prompt with a missing firm name is worse than a hard fail).
100
+ *
101
+ * When the instructions come from a prompt block, `prompt` is the
102
+ * version the turn runs (its template and declared parameters); an
103
+ * agent whose instructions are a prompt block can't render without it.
87
104
  */
88
105
  export function renderInstructions(
89
106
  agent: Agent,
90
107
  context: RenderContext,
108
+ prompt?: PromptBlockContent,
91
109
  ):
92
110
  | { readonly ok: true; readonly value: RenderResult }
93
111
  | { readonly ok: false; readonly error: PromptRenderError } {
94
- const declared = agent.parameters ?? [];
112
+ const source = instructionsSource(agent, prompt);
113
+ if (!source.ok) return source;
114
+ const { template, declared } = source.value;
95
115
  const missing = requiredMissing(declared, context.parameters);
96
116
  if (missing.length > 0) {
97
117
  return {
@@ -107,7 +127,7 @@ export function renderInstructions(
107
127
  const merged = mergeContext(agent, declared, context);
108
128
  const liquid = new Liquid({ strictVariables: true, strictFilters: true });
109
129
  try {
110
- const rendered = liquid.parseAndRenderSync(agent.instructions, merged);
130
+ const rendered = liquid.parseAndRenderSync(template, merged);
111
131
  return { ok: true, value: { rendered, context: merged } };
112
132
  } catch (cause) {
113
133
  // LiquidJS throws UndefinedVariableError for unresolved refs — surface
@@ -135,6 +155,33 @@ export function renderInstructions(
135
155
  }
136
156
  }
137
157
 
158
+ /** The template to render and the parameters it declares: the prompt block's, or the agent's own. */
159
+ function instructionsSource(
160
+ agent: Agent,
161
+ prompt: PromptBlockContent | undefined,
162
+ ):
163
+ | {
164
+ readonly ok: true;
165
+ readonly value: { readonly template: string; readonly declared: readonly PromptParameter[] };
166
+ }
167
+ | { readonly ok: false; readonly error: PromptRenderError } {
168
+ if (prompt !== undefined) {
169
+ return { ok: true, value: { template: prompt.template, declared: prompt.parameters ?? [] } };
170
+ }
171
+ if (typeof agent.instructions === 'string') {
172
+ return { ok: true, value: { template: agent.instructions, declared: agent.parameters ?? [] } };
173
+ }
174
+ const ref: PromptRef = agent.instructions;
175
+ return {
176
+ ok: false,
177
+ error: {
178
+ code: 'render-failure',
179
+ message: `The instructions come from prompt block "${ref.prompt}" (${ref.version}), which wasn't loaded`,
180
+ cause: null,
181
+ },
182
+ };
183
+ }
184
+
138
185
  function requiredMissing(
139
186
  declared: readonly PromptParameter[],
140
187
  supplied: Readonly<Record<string, unknown>>,
@@ -181,5 +228,6 @@ function mergeContext(
181
228
  };
182
229
  }
183
230
  if (context.input !== undefined) values.input = context.input;
231
+ if (context.settings !== undefined) values.settings = context.settings;
184
232
  return values;
185
233
  }
@@ -3,6 +3,8 @@
3
3
 
4
4
  import type { ProjectId, Result, RunId, Semver, TenantId, Timestamp } from '@kindgi/types';
5
5
 
6
+ import type { RunReplayRef } from '@kindgi/runtime';
7
+
6
8
  import type { PersistenceError } from './errors.js';
7
9
  import type { AgentId, ConversationId } from './types.js';
8
10
 
@@ -37,6 +39,8 @@ export interface RunSnapshotWriteInput {
37
39
  * as `principal`.
38
40
  */
39
41
  readonly authz?: unknown;
42
+ /** The turn's replay marker (`InvokeAgentInput.replay`): a resumed replay stays one. */
43
+ readonly replay?: RunReplayRef;
40
44
  }
41
45
 
42
46
  /**
@@ -59,6 +63,8 @@ export interface RunSnapshotRecord {
59
63
  readonly dryRun: boolean;
60
64
  readonly principal?: unknown;
61
65
  readonly authz?: unknown;
66
+ /** The turn's replay marker (`InvokeAgentInput.replay`): a resumed replay stays one. */
67
+ readonly replay?: RunReplayRef;
62
68
  readonly createdAt: Timestamp;
63
69
  }
64
70
 
package/src/schema.ts CHANGED
@@ -148,6 +148,11 @@ export const agentRunSnapshots = pgTable(
148
148
  * Threaded back into the resumed TurnContext.
149
149
  */
150
150
  authz: jsonb('authz'),
151
+ /**
152
+ * The replay marker (`{of, evalRunId}`) of a replay turn, so a resumed
153
+ * replay stays one: its tool calls are still decided by the replay rules.
154
+ */
155
+ replay: jsonb('replay'),
151
156
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
152
157
  },
153
158
  (t) => ({
package/src/streaming.ts CHANGED
@@ -86,6 +86,11 @@ export interface ToolCompletedEvent {
86
86
  readonly invocationId: string;
87
87
  readonly output: unknown;
88
88
  readonly durationMs: number;
89
+ /**
90
+ * In a replay turn: whether the tool ran (`live`), the past run's result
91
+ * was used (`recorded`), or the call was refused (`refused`).
92
+ */
93
+ readonly replay?: 'live' | 'recorded' | 'refused';
89
94
  }
90
95
 
91
96
  export interface ToolFailedEvent {
package/src/types.ts CHANGED
@@ -6,6 +6,8 @@ import type { Fact, MemoryScope } from '@kindgi/memory';
6
6
  import type { ToolErrorsSpec, ToolHitlMode, ToolHitlRule } from '@kindgi/policy-contract';
7
7
  import type { Brand, ConversationId, ProjectId, Semver, TenantId, Timestamp } from '@kindgi/types';
8
8
 
9
+ import type { AgentDerivation, AgentPins } from './pins.js';
10
+
9
11
  /**
10
12
  * Branded agent id. Convention: dotted namespace under the tenant's
11
13
  * pack — e.g. `acme.citation-verifier`, `acme.drafting`.
@@ -121,6 +123,22 @@ export interface ToolRef {
121
123
  readonly version: string;
122
124
  }
123
125
 
126
+ /**
127
+ * A prompt block an agent's instructions come from: its id and a semver
128
+ * range, resolved like a tool's (`pickVersion`) and pinned when the
129
+ * agent version is published.
130
+ */
131
+ export interface PromptRef {
132
+ readonly prompt: string;
133
+ readonly version: string;
134
+ }
135
+
136
+ /** A settings block an agent reads: its id and a semver range, pinned at publish. */
137
+ export interface BlockRef {
138
+ readonly id: string;
139
+ readonly version: string;
140
+ }
141
+
124
142
  /**
125
143
  * Per-agent behavior for multi-turn conversations. The agent chooses:
126
144
  * - How many prior messages to load (`historyLimit`; unset = all).
@@ -292,8 +310,13 @@ export interface Agent {
292
310
  * unresolved reference fails the turn at invoke time
293
311
  * (`model-invocation-failed` whose `cause` is the `missing-parameter`
294
312
  * render error), never a silent empty string.
313
+ *
314
+ * Or a prompt block, by range (`{ prompt: 'acme.intake-prompt',
315
+ * version: '^1.0.0' }`): its template renders here instead, with the
316
+ * parameters it declares, and the version that runs is pinned when the
317
+ * agent version is published (`pins.prompts`).
295
318
  */
296
- readonly instructions: string;
319
+ readonly instructions: string | PromptRef;
297
320
  /**
298
321
  * Typed parameters the caller supplies at invoke time. The UI reads
299
322
  * this to build a "configure agent" form; the runtime validates each
@@ -405,6 +428,35 @@ export interface Agent {
405
428
  * A tenant's `tool-errors` policy can lower it. See `ToolErrorsSpec`.
406
429
  */
407
430
  readonly toolErrors?: ToolErrorsSpec;
431
+ /**
432
+ * The exact block versions this agent version runs, resolved by the
433
+ * runtime when the version was published (see `AgentPins`). Never
434
+ * authored: `defineAgent` doesn't take it. Absent on an agent defined
435
+ * in code and on a version published before pins existed; its tool
436
+ * ranges then resolve per run.
437
+ */
438
+ readonly pins?: AgentPins;
439
+ /**
440
+ * Settings blocks the agent reads, by range. Each block's values reach
441
+ * its tools as `ToolContext.settings[<block id>]` and its templates as
442
+ * `settings.<block id>.<key>`; the versions are pinned at publish
443
+ * (`pins.settings`).
444
+ */
445
+ readonly settings?: readonly BlockRef[];
446
+ /**
447
+ * A settings block of model settings (`MODEL_SETTINGS_SCHEMA`:
448
+ * `temperature`, `maxOutputTokens`) the turn's model calls use. Pinned
449
+ * at publish with the other settings.
450
+ */
451
+ readonly modelSettings?: BlockRef;
452
+ /** `pinsDigest(pins)`, recorded when the version was published. */
453
+ readonly pinsDigest?: string;
454
+ /**
455
+ * Set by the runtime on a version it registered under another number
456
+ * than the definition's, when a deploy couldn't register that number
457
+ * as it was (see `AgentDerivation`). Never authored.
458
+ */
459
+ readonly derivedFrom?: AgentDerivation;
408
460
  }
409
461
 
410
462
  /**