@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
@@ -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
 
@@ -0,0 +1,230 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * The provenance nodes and edges an agent turn records, in one place:
6
+ * the steps add them as they run, and a turn resumed after a park
7
+ * (`rehydrate.ts`) adds those of the steps that ran before it from what
8
+ * they left durable (the conversation, the journal). Either way the DAG
9
+ * is the same.
10
+ */
11
+
12
+ import type { ProvenanceBuilder } from '@kindgi/provenance';
13
+ import type { Timestamp } from '@kindgi/types';
14
+
15
+ import type { ConversationMessage, RetrievedFact } from '../types.js';
16
+ import type { TurnContext } from './context.js';
17
+ import type { GateDecision } from './gate-decision.js';
18
+
19
+ /** The turn's user message. */
20
+ export function addInputNode(provenance: ProvenanceBuilder, message: ConversationMessage): void {
21
+ provenance.addNode({
22
+ id: `input:${message.sequence}`,
23
+ kind: 'input',
24
+ timestamp: message.createdAt,
25
+ ...(message.actor !== undefined && { actor: message.actor }),
26
+ });
27
+ }
28
+
29
+ /** The facts the turn retrieved for its user message. */
30
+ export function addRetrievalNodes(
31
+ provenance: ProvenanceBuilder,
32
+ retrieved: readonly RetrievedFact[],
33
+ input: ConversationMessage,
34
+ ): void {
35
+ for (const r of retrieved) {
36
+ provenance.addNode({
37
+ id: `retrieval:${r.fact.id}`,
38
+ kind: 'retrieval',
39
+ timestamp: input.createdAt,
40
+ ...(r.fact.contentHash !== undefined && { contentHash: r.fact.contentHash }),
41
+ attributes: {
42
+ factId: r.fact.id,
43
+ factType: r.fact.type,
44
+ intentScope: r.intent.scope,
45
+ ...(r.score !== undefined && { score: r.score }),
46
+ },
47
+ });
48
+ provenance.addEdge({
49
+ from: `retrieval:${r.fact.id}`,
50
+ to: `input:${input.sequence}`,
51
+ kind: 'influenced-by',
52
+ });
53
+ }
54
+ }
55
+
56
+ /** One model call: its identity; its usage is the cost ledger's, by `callId`. */
57
+ export interface ModelCallFacts {
58
+ readonly step: number;
59
+ readonly callId: string;
60
+ readonly provider: { readonly id: string; readonly model: string };
61
+ readonly finishReason: string;
62
+ readonly at: Timestamp;
63
+ }
64
+
65
+ /**
66
+ * A model call, `caused-by` the turn's user message and `influenced-by`
67
+ * each of this turn's tool results it read: all of them so far, since
68
+ * the call's input holds every message of the turn before it.
69
+ */
70
+ export function addModelCallNode(
71
+ provenance: ProvenanceBuilder,
72
+ call: ModelCallFacts,
73
+ inputSequence: number,
74
+ toolResultIds: readonly string[],
75
+ ): void {
76
+ const id = `model-call:${call.step}`;
77
+ provenance.addNode({
78
+ id,
79
+ kind: 'model-call',
80
+ timestamp: call.at,
81
+ modelVersion: `${call.provider.id}/${call.provider.model}`,
82
+ attributes: {
83
+ step: call.step,
84
+ callId: call.callId,
85
+ providerId: call.provider.id,
86
+ model: call.provider.model,
87
+ finishReason: call.finishReason,
88
+ },
89
+ });
90
+ provenance.addEdge({ from: id, to: `input:${inputSequence}`, kind: 'caused-by' });
91
+ for (const invocationId of toolResultIds) {
92
+ provenance.addEdge({ from: id, to: `tool-result:${invocationId}`, kind: 'influenced-by' });
93
+ }
94
+ }
95
+
96
+ /**
97
+ * A tool call the model at `step` made, and its result: the stored tool
98
+ * message (a tool's output, a rejected approval's, or a failed call's).
99
+ * `version` is the tool the agent's binding resolved, when it has one.
100
+ */
101
+ export function addToolNodes(
102
+ provenance: ProvenanceBuilder,
103
+ step: number,
104
+ result: ConversationMessage,
105
+ version: { readonly resolvedVersion: string; readonly requestedRange: string } | undefined,
106
+ ): void {
107
+ const call = result.toolCall;
108
+ if (call === undefined) return;
109
+ const callNodeId = `tool-call:${call.invocationId}`;
110
+ const resultNodeId = `tool-result:${call.invocationId}`;
111
+ provenance.addNode({
112
+ id: callNodeId,
113
+ kind: 'tool-call',
114
+ timestamp: result.createdAt,
115
+ attributes: {
116
+ toolId: call.toolId,
117
+ invocationId: call.invocationId,
118
+ // The version the registry picked for the turn and the range the
119
+ // agent asked for, so a replay can pin against the same version.
120
+ ...(version !== undefined && {
121
+ toolVersion: version.resolvedVersion,
122
+ toolVersionRange: version.requestedRange,
123
+ }),
124
+ },
125
+ });
126
+ provenance.addNode({ id: resultNodeId, kind: 'tool-result', timestamp: result.createdAt });
127
+ provenance.addEdge({ from: callNodeId, to: `model-call:${step}`, kind: 'invoked' });
128
+ provenance.addEdge({ from: resultNodeId, to: callNodeId, kind: 'produced' });
129
+ }
130
+
131
+ /**
132
+ * The tool calls of one model step: a node pair for each result the step
133
+ * stored, and their invocation ids added to the turn's tool results.
134
+ */
135
+ export function addStepToolNodes(
136
+ ctx: TurnContext,
137
+ step: number,
138
+ stored: readonly ConversationMessage[],
139
+ ): void {
140
+ for (const result of stored) {
141
+ if (result.role !== 'tool' || result.toolCall === undefined) continue;
142
+ const { invocationId, toolId } = result.toolCall;
143
+ if (ctx.provenance !== undefined) {
144
+ addToolNodes(ctx.provenance, step, result, versionOf(ctx, toolId));
145
+ const approval = ctx.toolApprovals?.get(invocationId);
146
+ if (approval !== undefined) {
147
+ addToolApprovalNodes(
148
+ ctx.provenance,
149
+ invocationId,
150
+ approval,
151
+ ctx.input.agent.id as unknown as string,
152
+ );
153
+ }
154
+ }
155
+ ctx.toolResultIds ??= [];
156
+ ctx.toolResultIds.push(invocationId);
157
+ }
158
+ }
159
+
160
+ /** A tool call's approval, decided: the wait it parked on and the answer. */
161
+ export interface ToolApproval {
162
+ readonly waitTokenId: string;
163
+ /** When the call parked on the approval. */
164
+ readonly parkedAt: Timestamp;
165
+ /** When the decision reached the run. */
166
+ readonly decidedAt: Timestamp;
167
+ readonly decision: GateDecision;
168
+ }
169
+
170
+ /**
171
+ * The approval a tool call waited on, as the session gate records its own:
172
+ * a `wait` node (the agent parked) `resumed-from` a `resume` node (the
173
+ * decision, its actor whoever decided). The call `waited-on` the wait, and
174
+ * its result was `caused-by` the decision: the tool's output when approved,
175
+ * the rejection when not.
176
+ */
177
+ export function addToolApprovalNodes(
178
+ provenance: ProvenanceBuilder,
179
+ invocationId: string,
180
+ approval: ToolApproval,
181
+ agentId: string,
182
+ ): void {
183
+ const waitId = `tool-hitl-gate-wait:${invocationId}`;
184
+ const resumeId = `tool-hitl-gate-resume:${invocationId}`;
185
+ const { decision } = approval;
186
+ provenance.addNode({
187
+ id: waitId,
188
+ kind: 'wait',
189
+ timestamp: approval.parkedAt,
190
+ actor: `agent:${agentId}`,
191
+ attributes: { gate: 'tool-call', invocationId, waitTokenId: approval.waitTokenId },
192
+ });
193
+ provenance.addNode({
194
+ id: resumeId,
195
+ kind: 'resume',
196
+ timestamp: approval.decidedAt,
197
+ ...(decision.decidedBy !== undefined && { actor: decision.decidedBy }),
198
+ attributes: {
199
+ gate: 'tool-call',
200
+ decision: decisionOf(decision),
201
+ ...(!decision.approved &&
202
+ decision.reason === 'rejected' &&
203
+ decision.rationale !== undefined && { rationale: decision.rationale }),
204
+ ...(decision.approvalId !== undefined && { approvalId: decision.approvalId }),
205
+ },
206
+ });
207
+ provenance.addEdge({ from: `tool-call:${invocationId}`, to: waitId, kind: 'waited-on' });
208
+ provenance.addEdge({ from: waitId, to: resumeId, kind: 'resumed-from' });
209
+ provenance.addEdge({ from: `tool-result:${invocationId}`, to: resumeId, kind: 'caused-by' });
210
+ }
211
+
212
+ /** A gate decision as a `resume` node records it. */
213
+ export function decisionOf(decision: GateDecision): 'approve' | 'reject' | 'unreadable' {
214
+ if (decision.approved) return 'approve';
215
+ return decision.reason === 'rejected' ? 'reject' : 'unreadable';
216
+ }
217
+
218
+ /** The version the agent's binding of `toolId` resolved, if it has one. */
219
+ function versionOf(
220
+ ctx: TurnContext,
221
+ toolId: string,
222
+ ): { readonly resolvedVersion: string; readonly requestedRange: string } | undefined {
223
+ const bindings = [...(ctx.tools?.byName.values() ?? [])];
224
+ const binding =
225
+ ctx.tools?.byName.get(toolId) ??
226
+ bindings.find((b) => (b.tool.id as unknown as string) === toolId);
227
+ return binding === undefined
228
+ ? undefined
229
+ : { resolvedVersion: binding.resolvedVersion, requestedRange: binding.requestedRange };
230
+ }
package/src/index.ts CHANGED
@@ -17,10 +17,44 @@ 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';
38
+ export {
39
+ AGENT_GATE_SUBJECTS,
40
+ SESSION_GATE_SUBJECT,
41
+ TOOL_CALL_GATE_SUBJECT,
42
+ readGateDecision,
43
+ } from './handlers/gate-decision.js';
44
+ export type { GateDecision, GateDecisionValue } from './handlers/gate-decision.js';
22
45
  export type { EffectiveHitlPolicy } from './hitl-policy.js';
23
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';
24
58
  export type {
25
59
  AgentStepOutput,
26
60
  AgentTurnAbortedError,
@@ -80,6 +114,14 @@ export type {
80
114
  RenderFailureError,
81
115
  RenderResult,
82
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';
83
125
  export { createAgentRegistry } from './registry.js';
84
126
  export type { AgentRegistry } from './registry.js';
85
127
  export {
@@ -98,12 +140,14 @@ export type {
98
140
  AgentBindings,
99
141
  AgentId,
100
142
  AgentOutputSpec,
143
+ BlockRef,
101
144
  Conversation,
102
145
  ConversationId,
103
146
  ConversationMessage,
104
147
  ConversationPolicy,
105
148
  MessageRole,
106
149
  PromptParameter,
150
+ PromptRef,
107
151
  RetrievalIntent,
108
152
  RetrievedFact,
109
153
  ToolRef,
package/src/invoke.ts CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  import type { Principal } from '@kindgi/authz';
5
5
  import type { KernelError, RunResult } from '@kindgi/runtime';
6
- import type { Result, RunId, TenantId } from '@kindgi/types';
6
+ import type { OrgId, Result, RunId, TenantId } from '@kindgi/types';
7
7
 
8
8
  import { AGENT_TURN_FLOW } from './agent-turn-flow.js';
9
9
  import { DEFAULT_MAX_WALL_MS } from './handlers/constants.js';
@@ -14,6 +14,7 @@ import type { InvokeAgentBindings, InvokeAgentInput } from './handlers/public-ty
14
14
  import { rehydrateTurnContext } from './handlers/rehydrate.js';
15
15
  import type { AgentTurnResult } from './handlers/result-shape.js';
16
16
  import { projectRunResult } from './project-run-result.js';
17
+ import type { RunSnapshotRecord } from './run-snapshot-binding.js';
17
18
  import type { Agent } from './types.js';
18
19
 
19
20
  /**
@@ -82,7 +83,15 @@ export async function invokeAgent(
82
83
  flow: AGENT_TURN_FLOW,
83
84
  handlers,
84
85
  input: input.userMessage,
86
+ // The run's record names the agent, its version and the conversation.
87
+ agent: {
88
+ id: input.agent.id,
89
+ version: input.agent.version,
90
+ conversationId: input.conversationId,
91
+ },
85
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 }),
86
95
  ...(input.dryRun === true && { options: { dryRun: true } }),
87
96
  // Authorization — carry principal + authz into the run so every
88
97
  // tool invocation inside the agent's turn is checked.
@@ -106,6 +115,12 @@ export interface ResumeAgentTurnInput {
106
115
  * handler's agent-version-mismatch guard).
107
116
  */
108
117
  readonly agent: Agent;
118
+ /**
119
+ * The org of the run's project (the snapshot's `projectId`), when it has
120
+ * one. The caller resolves it from the project, as for `invokeAgent`'s
121
+ * `orgId`, so the resumed turn's tools and guardrails see it too.
122
+ */
123
+ readonly orgId?: OrgId;
109
124
  }
110
125
 
111
126
  /**
@@ -203,25 +218,7 @@ export async function resumeAgentTurn(
203
218
  }
204
219
 
205
220
  // 3. Reconstruct the InvokeAgentInput envelope from the snapshot.
206
- const reconstructedInput: InvokeAgentInput = {
207
- tenantId: snapshot.tenantId,
208
- projectId: snapshot.projectId,
209
- agent: input.agent,
210
- conversationId: snapshot.conversationId,
211
- userMessage: snapshot.userMessage,
212
- ...(snapshot.parameters !== undefined && { parameters: snapshot.parameters }),
213
- ...(snapshot.input !== undefined && { input: snapshot.input }),
214
- ...(snapshot.participantId !== undefined && { participantId: snapshot.participantId }),
215
- ...(snapshot.dryRun && { dryRun: true }),
216
- ...(snapshot.principal !== undefined &&
217
- snapshot.principal !== null && {
218
- principal: snapshot.principal as Principal,
219
- }),
220
- ...(snapshot.authz !== undefined &&
221
- snapshot.authz !== null && {
222
- authz: snapshot.authz as { readonly fgaApiUrl: string },
223
- }),
224
- };
221
+ const reconstructedInput = turnInputFromSnapshot(snapshot, input);
225
222
 
226
223
  // 4. Build the same TurnContext + handlers as invokeAgent().
227
224
  const started = Date.now();
@@ -299,3 +296,35 @@ export type {
299
296
  InvokeAgentBindings,
300
297
  InvokeAgentInput,
301
298
  } from './handlers/public-types.js';
299
+
300
+ /**
301
+ * The `InvokeAgentInput` a resumed turn runs with: the one captured in its
302
+ * snapshot at run start, the agent the caller resolved, and the org of the
303
+ * run's project, which the caller resolves as for `invokeAgent`.
304
+ */
305
+ export function turnInputFromSnapshot(
306
+ snapshot: RunSnapshotRecord,
307
+ input: Pick<ResumeAgentTurnInput, 'agent' | 'orgId'>,
308
+ ): InvokeAgentInput {
309
+ return {
310
+ tenantId: snapshot.tenantId,
311
+ projectId: snapshot.projectId,
312
+ ...(input.orgId !== undefined && { orgId: input.orgId }),
313
+ agent: input.agent,
314
+ conversationId: snapshot.conversationId,
315
+ userMessage: snapshot.userMessage,
316
+ ...(snapshot.parameters !== undefined && { parameters: snapshot.parameters }),
317
+ ...(snapshot.input !== undefined && { input: snapshot.input }),
318
+ ...(snapshot.participantId !== undefined && { participantId: snapshot.participantId }),
319
+ ...(snapshot.dryRun && { dryRun: true }),
320
+ ...(snapshot.principal !== undefined &&
321
+ snapshot.principal !== null && {
322
+ principal: snapshot.principal as Principal,
323
+ }),
324
+ ...(snapshot.authz !== undefined &&
325
+ snapshot.authz !== null && {
326
+ authz: snapshot.authz as { readonly fgaApiUrl: string },
327
+ }),
328
+ ...(snapshot.replay !== undefined && snapshot.replay !== null && { replay: snapshot.replay }),
329
+ };
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
  }