@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
@@ -0,0 +1,101 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * The reviewer's answer at an agent turn's approval gates: the tool-call
6
+ * gate (`dispatch-tools`) and the session gate (`setup`). A gate parks the
7
+ * turn on a waitpoint; the approvals route resumes it with the decision
8
+ * (`GateDecisionValue`): approve or reject, the reviewer's rationale, and
9
+ * who decided which approval.
10
+ *
11
+ * A gate fails closed. Only an explicit approve lets the tool run (or the
12
+ * session go on). Anything else blocks: a reject, and also a resume value
13
+ * that isn't a decision at all (a `value` that replaced it, a malformed
14
+ * payload), so no answer can be read as consent by omission.
15
+ */
16
+
17
+ /** The approval subject an agent's tool-call gate parks on. */
18
+ export const TOOL_CALL_GATE_SUBJECT = 'tool-call:pending';
19
+
20
+ /** The approval subject an agent's session gate (`afterTurns`) parks on. */
21
+ export const SESSION_GATE_SUBJECT = 'agent-turn:session-hitl-gate';
22
+
23
+ /**
24
+ * The approval subjects an agent turn parks on. Their resume value is the
25
+ * reviewer's decision and nothing else, so the approvals route takes no
26
+ * `value` for them.
27
+ */
28
+ export const AGENT_GATE_SUBJECTS: ReadonlySet<string> = new Set([
29
+ TOOL_CALL_GATE_SUBJECT,
30
+ SESSION_GATE_SUBJECT,
31
+ ]);
32
+
33
+ /**
34
+ * A gate's resume value: the reviewer's decision, as the approvals route
35
+ * (and the runtime, delivering a decision whose delivery was lost) completes
36
+ * the gate's waitpoint with it. The run's journal keeps it as it came.
37
+ */
38
+ export interface GateDecisionValue {
39
+ readonly decided: 'approve' | 'reject';
40
+ readonly rationale?: string;
41
+ /**
42
+ * Who decided, as an actor: `user:<userId>`, the reviewer's user. Absent
43
+ * from values written before it was recorded.
44
+ */
45
+ readonly decidedBy?: string;
46
+ /** The approval decided. Absent from values written before it was recorded. */
47
+ readonly approvalId?: string;
48
+ }
49
+
50
+ /** Who decided a gate, and which approval, when its value says. */
51
+ interface GateDecider {
52
+ readonly decidedBy?: string;
53
+ readonly approvalId?: string;
54
+ }
55
+
56
+ /** What a gate's resume value decides. */
57
+ export type GateDecision = GateDecider &
58
+ (
59
+ | { readonly approved: true }
60
+ | {
61
+ readonly approved: false;
62
+ /** `rejected`: the reviewer said no. `unreadable`: the answer wasn't a decision. */
63
+ readonly reason: 'rejected' | 'unreadable';
64
+ /** The reviewer's rationale, or why an unreadable answer blocks. */
65
+ readonly rationale: string | undefined;
66
+ }
67
+ );
68
+
69
+ /** Why a gate blocks on an answer that isn't a decision. */
70
+ export const UNREADABLE_DECISION =
71
+ "the approval's answer wasn't a decision (approve or reject), so it counts as a rejection";
72
+
73
+ /**
74
+ * The decision in a gate's resume value. Fails closed: only
75
+ * `{ decided: 'approve' }` approves.
76
+ */
77
+ export function readGateDecision(value: unknown): GateDecision {
78
+ if (typeof value !== 'object' || value === null) {
79
+ return { approved: false, reason: 'unreadable', rationale: UNREADABLE_DECISION };
80
+ }
81
+ const { decided, rationale, decidedBy, approvalId } = value as {
82
+ decided?: unknown;
83
+ rationale?: unknown;
84
+ decidedBy?: unknown;
85
+ approvalId?: unknown;
86
+ };
87
+ const decider: GateDecider = {
88
+ ...(typeof decidedBy === 'string' && { decidedBy }),
89
+ ...(typeof approvalId === 'string' && { approvalId }),
90
+ };
91
+ if (decided === 'approve') return { approved: true, ...decider };
92
+ if (decided === 'reject') {
93
+ return {
94
+ approved: false,
95
+ reason: 'rejected',
96
+ rationale: typeof rationale === 'string' ? rationale : undefined,
97
+ ...decider,
98
+ };
99
+ }
100
+ return { approved: false, reason: 'unreadable', rationale: UNREADABLE_DECISION };
101
+ }
@@ -1,13 +1,25 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
- import type { ModelCallInput, ModelMessage } from '@kindgi/capabilities';
5
- import type { NodeHandler } from '@kindgi/handler';
4
+ import { randomUUID } from 'node:crypto';
5
+
6
+ import {
7
+ type ModelCallInput,
8
+ type ModelMessage,
9
+ type ModelProvider,
10
+ type ModelUsageRecord,
11
+ recordModelUsage,
12
+ } from '@kindgi/capabilities';
13
+ import { attemptsOf } from '@kindgi/capabilities/attempts';
14
+ import type { NodeContext, NodeHandler } from '@kindgi/handler';
15
+ import type { Timestamp } from '@kindgi/types';
6
16
 
7
17
  import { emitTurnEvent } from '../streaming.js';
8
18
 
9
19
  import type { AgentTurnIterationOutput, TurnContext } from './context.js';
10
20
  import { throwAgentTurnFailure } from './errors.js';
21
+ import { replayTag } from './replay.js';
22
+ import { addModelCallNode } from './turn-provenance.js';
11
23
 
12
24
  /**
13
25
  * Loop-body node #1. Invoke the model with the current
@@ -15,6 +27,16 @@ import { throwAgentTurnFailure } from './errors.js';
15
27
  * `model.call.completed`, records provenance for the model-call node,
16
28
  * and accumulates usage on `ctx.usage`.
17
29
  *
30
+ * Every call is recorded in the usage sink (`InvokeAgentBindings.usage`,
31
+ * the runtime's cost ledger) before the step goes on, a call that threw
32
+ * included. A sink that fails is tried again (a record is idempotent by
33
+ * call id); one that still fails fails the step when the call succeeded,
34
+ * so no answered call is left unrecorded. A call that failed keeps its
35
+ * own failure, which says it couldn't be recorded too. The call's id goes into
36
+ * the step's output and its provenance node, which keeps the call's
37
+ * identity (provider, model) while its usage lives in the ledger. A dry
38
+ * run spends nothing and records nothing.
39
+ *
18
40
  * Output is a partial `AgentTurnIterationOutput` — `dispatch-tools`
19
41
  * receives it and finishes composing the iteration output.
20
42
  */
@@ -31,6 +53,8 @@ export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
31
53
  const nextMessages: readonly ModelMessage[] = shaped?.nextMessages ?? [];
32
54
 
33
55
  ctx.usage.steps += 1;
56
+ const step = ctx.usage.steps;
57
+ const callId = randomUUID();
34
58
 
35
59
  await emitTurnEvent(ctx.bindings.onEvent, {
36
60
  kind: 'model.call.started',
@@ -60,28 +84,17 @@ export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
60
84
  ...(ctx.tools.definitions.length > 0 && {
61
85
  tools: ctx.tools.definitions,
62
86
  }),
87
+ ...modelSettingsOf(ctx),
63
88
  abortSignal: ctx.turnAbort.signal,
64
89
  };
65
90
 
91
+ const startedAt = Date.now();
66
92
  try {
67
93
  callResult = await ctx.provider.invoke(callInput);
68
94
  } catch (cause) {
69
- if (ctx.turnAbort.signal.aborted) {
70
- throwAgentTurnFailure({
71
- code: 'agent-turn-aborted',
72
- message: `Agent turn aborted: ${cause instanceof Error ? cause.message : String(cause)}`,
73
- reason: ctx.abortReason ?? 'timeout',
74
- });
75
- }
76
- // The provider's own words (a 401's "invalid x-api-key", a 429)
77
- // are what the caller needs; they're in `cause` too, but callers
78
- // show `message`.
79
- throwAgentTurnFailure({
80
- code: 'model-invocation-failed',
81
- message: `Model call to ${ctx.provider.metadata.id} (${ctx.model.name}) failed: ${describeCause(cause)}`,
82
- cause,
83
- });
95
+ return await failCall(ctx, kctx, { callId, step, cause, startedAt });
84
96
  }
97
+ await recordAnswer(ctx, kctx, callId, step, callResult);
85
98
  }
86
99
 
87
100
  ctx.usage.promptTokens += callResult.usage.promptTokens;
@@ -100,32 +113,27 @@ export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
100
113
  });
101
114
 
102
115
  if (ctx.provenance !== undefined && ctx.userMessage !== undefined) {
103
- const modelCallNodeId = `model-call:${ctx.usage.steps}`;
104
- ctx.provenance.addNode({
105
- id: modelCallNodeId,
106
- kind: 'model-call',
107
- timestamp: new Date().toISOString() as never,
108
- modelVersion: `${callResult.provider.id}/${callResult.provider.model}`,
109
- attributes: {
116
+ addModelCallNode(
117
+ ctx.provenance,
118
+ {
110
119
  step: ctx.usage.steps,
111
- promptTokens: callResult.usage.promptTokens,
112
- completionTokens: callResult.usage.completionTokens,
113
- costUsd: callResult.costUsd,
120
+ callId,
121
+ provider: callResult.provider,
114
122
  finishReason: callResult.finishReason,
123
+ at: new Date().toISOString() as Timestamp,
115
124
  },
116
- });
117
- ctx.provenance.addEdge({
118
- from: modelCallNodeId,
119
- to: `input:${ctx.userMessage.sequence}`,
120
- kind: 'caused-by',
121
- });
125
+ ctx.userMessage.sequence,
126
+ ctx.toolResultIds ?? [],
127
+ );
122
128
  }
123
129
 
124
130
  // Partial iteration output — `dispatch-tools` finishes it.
125
131
  const partial: Omit<AgentTurnIterationOutput, 'iterationAppended' | 'finishedTurn'> & {
126
132
  readonly step: number;
133
+ readonly callId: string;
127
134
  } = {
128
135
  step: ctx.usage.steps,
136
+ callId,
129
137
  finishReason: callResult.finishReason,
130
138
  message: callResult.message,
131
139
  iterationUsage: {
@@ -140,6 +148,117 @@ export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
140
148
  };
141
149
  }
142
150
 
151
+ /**
152
+ * A call that answered: record it before the step goes on. A sink that
153
+ * still fails after its retries fails the step: an answered call isn't
154
+ * left unrecorded.
155
+ */
156
+ async function recordAnswer(
157
+ ctx: TurnContext,
158
+ kctx: NodeContext,
159
+ callId: string,
160
+ step: number,
161
+ answer: Awaited<ReturnType<ModelProvider['invoke']>>,
162
+ ): Promise<void> {
163
+ const { message: _answer, ...result } = answer;
164
+ const unrecorded = await recordCall(ctx, kctx, {
165
+ callId,
166
+ step,
167
+ status: 'ok',
168
+ result,
169
+ durationMs: answer.durationMs,
170
+ });
171
+ if (unrecorded !== undefined) {
172
+ throwAgentTurnFailure({
173
+ code: 'persistence-error',
174
+ message: `The model call couldn't be recorded: ${describeCause(unrecorded)}`,
175
+ cause: unrecorded,
176
+ });
177
+ }
178
+ }
179
+
180
+ /**
181
+ * A call that threw: record it, then fail the step with the call's own
182
+ * failure (aborted, or the provider's words). A record that failed too
183
+ * is said after it.
184
+ */
185
+ async function failCall(
186
+ ctx: TurnContext,
187
+ kctx: NodeContext,
188
+ failed: {
189
+ readonly callId: string;
190
+ readonly step: number;
191
+ readonly cause: unknown;
192
+ readonly startedAt: number;
193
+ },
194
+ ): Promise<never> {
195
+ const { cause } = failed;
196
+ const attempts = attemptsOf(cause);
197
+ const unrecorded = await recordCall(ctx, kctx, {
198
+ callId: failed.callId,
199
+ step: failed.step,
200
+ status: 'failed',
201
+ error: { message: describeCause(cause), ...(attempts !== undefined && { attempts }) },
202
+ durationMs: Date.now() - failed.startedAt,
203
+ });
204
+ const andUnrecorded =
205
+ unrecorded === undefined
206
+ ? ''
207
+ : ` (and the failed call couldn't be recorded: ${describeCause(unrecorded)})`;
208
+ if (ctx.turnAbort.signal.aborted) {
209
+ throwAgentTurnFailure({
210
+ code: 'agent-turn-aborted',
211
+ message: `Agent turn aborted: ${cause instanceof Error ? cause.message : String(cause)}${andUnrecorded}`,
212
+ reason: ctx.abortReason ?? 'timeout',
213
+ });
214
+ }
215
+ // The provider's own words (a 401's "invalid x-api-key", a 429) are
216
+ // what the caller needs; they're in `cause` too, but callers show
217
+ // `message`.
218
+ throwAgentTurnFailure({
219
+ code: 'model-invocation-failed',
220
+ message: `Model call to ${ctx.provider?.metadata.id} (${ctx.model?.name}) failed: ${describeCause(cause)}${andUnrecorded}`,
221
+ cause,
222
+ });
223
+ }
224
+
225
+ /** What the call came to, for `recordCall`. */
226
+ type CallOutcome = Pick<
227
+ ModelUsageRecord,
228
+ 'callId' | 'step' | 'status' | 'result' | 'error' | 'durationMs'
229
+ >;
230
+
231
+ /**
232
+ * Record a model call in the usage sink, when the turn has one, trying
233
+ * a failing sink again. Resolves with the sink's last failure when it
234
+ * couldn't record; the caller decides what that means for the step.
235
+ */
236
+ async function recordCall(
237
+ ctx: TurnContext,
238
+ kctx: NodeContext,
239
+ call: CallOutcome,
240
+ ): Promise<unknown | undefined> {
241
+ const sink = ctx.bindings.usage;
242
+ if (sink === undefined || ctx.provider === undefined || ctx.model === undefined) return;
243
+ const { input } = ctx;
244
+ const recorded = await recordModelUsage(sink, {
245
+ ...call,
246
+ tenantId: input.tenantId,
247
+ projectId: input.projectId,
248
+ runId: kctx.runId,
249
+ agentId: input.agent.id as unknown as string,
250
+ agentVersion: input.agent.version,
251
+ conversationId: input.conversationId as unknown as string,
252
+ nodeId: kctx.nodeId as unknown as string,
253
+ providerId: ctx.provider.metadata.id,
254
+ model: ctx.model.name,
255
+ ...(ctx.provider.metadata.fallback === true && { fallback: true }),
256
+ ...(input.replay !== undefined && { replay: replayTag(input.replay) }),
257
+ occurredAt: new Date().toISOString(),
258
+ });
259
+ return recorded.kind === 'err' ? (recorded.error ?? new Error('no detail')) : undefined;
260
+ }
261
+
143
262
  /** Longest cause text a failure message carries. */
144
263
  const MAX_CAUSE_CHARS = 500;
145
264
 
@@ -149,3 +268,14 @@ function describeCause(cause: unknown): string {
149
268
  if (text.length === 0) return cause instanceof Error ? cause.name : 'no detail';
150
269
  return text.length > MAX_CAUSE_CHARS ? `${text.slice(0, MAX_CAUSE_CHARS - 1)}…` : text;
151
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
+ }
@@ -28,7 +28,9 @@ export function buildPersistProvenanceHandler(ctx: TurnContext): NodeHandler {
28
28
  // want).
29
29
  return { persisted: false };
30
30
  }
31
- const persisted = await persistProvenance(ctx.provenance, ctx.provenanceBindings);
31
+ const persisted = await persistProvenance(ctx.provenance, ctx.provenanceBindings, {
32
+ projectId: ctx.input.projectId,
33
+ });
32
34
  if (persisted.kind === 'ok') {
33
35
  ctx.persistedProvenance = persisted.value;
34
36
  return { persisted: true };
@@ -8,6 +8,7 @@ import type { ConversationMessage } from '../types.js';
8
8
 
9
9
  import type { TurnContext } from './context.js';
10
10
  import { throwAgentTurnFailure } from './errors.js';
11
+ import { addInputNode } from './turn-provenance.js';
11
12
 
12
13
  /**
13
14
  * Persist the caller's user message BEFORE the model is called. A
@@ -33,14 +34,7 @@ export function buildPersistUserMessageHandler(ctx: TurnContext): NodeHandler {
33
34
  ctx.userMessage = synthetic;
34
35
  ctx.appended.push(synthetic);
35
36
 
36
- if (ctx.provenance !== undefined) {
37
- ctx.provenance.addNode({
38
- id: `input:${synthetic.sequence}`,
39
- kind: 'input',
40
- timestamp: synthetic.createdAt,
41
- ...(synthetic.actor !== undefined && { actor: synthetic.actor }),
42
- });
43
- }
37
+ if (ctx.provenance !== undefined) addInputNode(ctx.provenance, synthetic);
44
38
  return { sequence: synthetic.sequence };
45
39
  }
46
40
 
@@ -56,14 +50,7 @@ export function buildPersistUserMessageHandler(ctx: TurnContext): NodeHandler {
56
50
  ctx.userMessage = persisted.value;
57
51
  ctx.appended.push(persisted.value);
58
52
 
59
- if (ctx.provenance !== undefined) {
60
- ctx.provenance.addNode({
61
- id: `input:${persisted.value.sequence}`,
62
- kind: 'input',
63
- timestamp: persisted.value.createdAt,
64
- ...(persisted.value.actor !== undefined && { actor: persisted.value.actor }),
65
- });
66
- }
53
+ if (ctx.provenance !== undefined) addInputNode(ctx.provenance, persisted.value);
67
54
 
68
55
  return { sequence: persisted.value.sequence };
69
56
  };
@@ -2,14 +2,15 @@
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
4
  import type { Principal } from '@kindgi/authz';
5
- import type { ProviderRegistry, TenantPolicy } from '@kindgi/capabilities';
5
+ import type { ProviderRegistry, TenantPolicy, UsageSink } from '@kindgi/capabilities';
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
- import type { ProjectId, ProvenanceId, RunId, TenantId, Timestamp } from '@kindgi/types';
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
@@ -32,6 +35,12 @@ export interface InvokeAgentInput {
32
35
  * at the caller layer.
33
36
  */
34
37
  readonly projectId: ProjectId;
38
+ /**
39
+ * The project's org, when it belongs to one: the runtime resolves it
40
+ * from the project, never from input. The turn's tools get it as
41
+ * `ToolContext.orgId`, its guardrails as `trace.orgId`.
42
+ */
43
+ readonly orgId?: OrgId;
35
44
  readonly agent: Agent;
36
45
  readonly conversationId: ConversationId;
37
46
  readonly userMessage: string;
@@ -48,6 +57,13 @@ export interface InvokeAgentInput {
48
57
  * step. Recorded on the turn's run, so the flow can find its child.
49
58
  */
50
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;
51
67
  readonly participantId?: string;
52
68
  readonly abortSignal?: AbortSignal;
53
69
  /**
@@ -95,6 +111,12 @@ export interface InvokeAgentBindings extends GuardrailsBindings {
95
111
  * sees another tenant's tools.
96
112
  */
97
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;
98
120
  /**
99
121
  * Caller-plugged data-access surface for memory reads.
100
122
  * The Kindgi runtime supplies a Postgres-backed implementation;
@@ -153,6 +175,12 @@ export interface InvokeAgentBindings extends GuardrailsBindings {
153
175
  readonly onEvent?: OnTurnEvent;
154
176
  readonly provenance?: ProvenanceBindings;
155
177
  readonly hitl?: HitlBindings;
178
+ /**
179
+ * Where the turn records each model call (the runtime's cost ledger):
180
+ * its usage, provider, model, run, agent and step, before the step
181
+ * that made it goes on. Absent: calls aren't recorded.
182
+ */
183
+ readonly usage?: UsageSink;
156
184
  /**
157
185
  * Declarative HTTP tools: optional secret resolver populated
158
186
  * from the deployment's tenant-scoped `SecretBinding`. When present,
@@ -163,6 +191,11 @@ export interface InvokeAgentBindings extends GuardrailsBindings {
163
191
  * unaffected.
164
192
  */
165
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;
166
199
  }
167
200
 
168
201
  export interface HitlBindings {
@@ -171,6 +204,8 @@ export interface HitlBindings {
171
204
 
172
205
  export interface HitlEnqueueInput {
173
206
  readonly tenantId: TenantId;
207
+ /** The turn's project: approval lists filter by it. */
208
+ readonly projectId?: ProjectId;
174
209
  readonly subjectKind: string;
175
210
  readonly subjectRef: Readonly<Record<string, unknown>>;
176
211
  readonly requiredRole?: 'standard' | 'senior' | 'admin';