@kindgi/agents 0.1.1 → 0.1.3

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 (96) hide show
  1. package/dist/conversation-binding.d.ts +9 -1
  2. package/dist/conversation-binding.d.ts.map +1 -1
  3. package/dist/define.js +20 -0
  4. package/dist/define.js.map +1 -1
  5. package/dist/guardrails-gate.d.ts +7 -4
  6. package/dist/guardrails-gate.d.ts.map +1 -1
  7. package/dist/guardrails-gate.js +5 -2
  8. package/dist/guardrails-gate.js.map +1 -1
  9. package/dist/handlers/context.d.ts +14 -0
  10. package/dist/handlers/context.d.ts.map +1 -1
  11. package/dist/handlers/dispatch-tools.d.ts.map +1 -1
  12. package/dist/handlers/dispatch-tools.js +57 -60
  13. package/dist/handlers/dispatch-tools.js.map +1 -1
  14. package/dist/handlers/errors.d.ts +11 -1
  15. package/dist/handlers/errors.d.ts.map +1 -1
  16. package/dist/handlers/errors.js.map +1 -1
  17. package/dist/handlers/evaluate-guardrails.d.ts.map +1 -1
  18. package/dist/handlers/evaluate-guardrails.js +34 -1
  19. package/dist/handlers/evaluate-guardrails.js.map +1 -1
  20. package/dist/handlers/gate-decision.d.ts +62 -0
  21. package/dist/handlers/gate-decision.d.ts.map +1 -0
  22. package/dist/handlers/gate-decision.js +55 -0
  23. package/dist/handlers/gate-decision.js.map +1 -0
  24. package/dist/handlers/model-call.d.ts +10 -0
  25. package/dist/handlers/model-call.d.ts.map +1 -1
  26. package/dist/handlers/model-call.js +109 -34
  27. package/dist/handlers/model-call.js.map +1 -1
  28. package/dist/handlers/persist-provenance.d.ts.map +1 -1
  29. package/dist/handlers/persist-provenance.js +3 -1
  30. package/dist/handlers/persist-provenance.js.map +1 -1
  31. package/dist/handlers/persist-user-message.d.ts.map +1 -1
  32. package/dist/handlers/persist-user-message.js +5 -16
  33. package/dist/handlers/persist-user-message.js.map +1 -1
  34. package/dist/handlers/public-types.d.ts +16 -2
  35. package/dist/handlers/public-types.d.ts.map +1 -1
  36. package/dist/handlers/rehydrate.d.ts.map +1 -1
  37. package/dist/handlers/rehydrate.js +85 -0
  38. package/dist/handlers/rehydrate.js.map +1 -1
  39. package/dist/handlers/run-retrievals.d.ts.map +1 -1
  40. package/dist/handlers/run-retrievals.js +2 -19
  41. package/dist/handlers/run-retrievals.js.map +1 -1
  42. package/dist/handlers/setup.d.ts.map +1 -1
  43. package/dist/handlers/setup.js +44 -24
  44. package/dist/handlers/setup.js.map +1 -1
  45. package/dist/handlers/tool-hitl.d.ts +5 -9
  46. package/dist/handlers/tool-hitl.d.ts.map +1 -1
  47. package/dist/handlers/tool-hitl.js +5 -0
  48. package/dist/handlers/tool-hitl.js.map +1 -1
  49. package/dist/handlers/turn-provenance.d.ts +67 -0
  50. package/dist/handlers/turn-provenance.d.ts.map +1 -0
  51. package/dist/handlers/turn-provenance.js +160 -0
  52. package/dist/handlers/turn-provenance.js.map +1 -0
  53. package/dist/index.d.ts +3 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/invoke.d.ts +15 -2
  58. package/dist/invoke.d.ts.map +1 -1
  59. package/dist/invoke.js +38 -22
  60. package/dist/invoke.js.map +1 -1
  61. package/dist/provenance-emit.d.ts +4 -2
  62. package/dist/provenance-emit.d.ts.map +1 -1
  63. package/dist/provenance-emit.js +4 -2
  64. package/dist/provenance-emit.js.map +1 -1
  65. package/dist/schema.d.ts +17 -0
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +11 -0
  68. package/dist/schema.js.map +1 -1
  69. package/dist/types.d.ts +15 -1
  70. package/dist/types.d.ts.map +1 -1
  71. package/migrations/0003_hesitant_captain_cross.sql +2 -0
  72. package/migrations/meta/0003_snapshot.json +321 -0
  73. package/migrations/meta/_journal.json +7 -0
  74. package/package.json +19 -18
  75. package/src/conversation-binding.ts +9 -1
  76. package/src/define.ts +20 -0
  77. package/src/guardrails-gate.ts +17 -3
  78. package/src/handlers/context.ts +14 -0
  79. package/src/handlers/dispatch-tools.ts +76 -61
  80. package/src/handlers/errors.ts +13 -1
  81. package/src/handlers/evaluate-guardrails.ts +39 -1
  82. package/src/handlers/gate-decision.ts +101 -0
  83. package/src/handlers/model-call.ts +149 -33
  84. package/src/handlers/persist-provenance.ts +3 -1
  85. package/src/handlers/persist-user-message.ts +3 -16
  86. package/src/handlers/public-types.ts +16 -2
  87. package/src/handlers/rehydrate.ts +120 -3
  88. package/src/handlers/run-retrievals.ts +2 -19
  89. package/src/handlers/setup.ts +59 -33
  90. package/src/handlers/tool-hitl.ts +6 -10
  91. package/src/handlers/turn-provenance.ts +230 -0
  92. package/src/index.ts +8 -0
  93. package/src/invoke.ts +53 -25
  94. package/src/provenance-emit.ts +4 -1
  95. package/src/schema.ts +11 -0
  96. package/src/types.ts +15 -1
@@ -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
@@ -19,6 +19,13 @@ export type {
19
19
  export { defineAgent } from './define.js';
20
20
  export type { DefineAgentSpec } from './define.js';
21
21
  export { resolveEffectiveHitlPolicy } from './hitl-policy.js';
22
+ export {
23
+ AGENT_GATE_SUBJECTS,
24
+ SESSION_GATE_SUBJECT,
25
+ TOOL_CALL_GATE_SUBJECT,
26
+ readGateDecision,
27
+ } from './handlers/gate-decision.js';
28
+ export type { GateDecision, GateDecisionValue } from './handlers/gate-decision.js';
22
29
  export type { EffectiveHitlPolicy } from './hitl-policy.js';
23
30
  export { agentStepOutput, invokeAgent, resumeAgentTurn } from './invoke.js';
24
31
  export type {
@@ -35,6 +42,7 @@ export type {
35
42
  ModelInvocationError,
36
43
  OutputSchemaViolationError,
37
44
  ResumeAgentTurnInput,
45
+ RunSnapshotError,
38
46
  ToolInvocationError,
39
47
  UnresolvedToolError,
40
48
  } from './invoke.js';
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,6 +83,12 @@ 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 }),
86
93
  ...(input.dryRun === true && { options: { dryRun: true } }),
87
94
  // Authorization — carry principal + authz into the run so every
@@ -106,6 +113,12 @@ export interface ResumeAgentTurnInput {
106
113
  * handler's agent-version-mismatch guard).
107
114
  */
108
115
  readonly agent: Agent;
116
+ /**
117
+ * The org of the run's project (the snapshot's `projectId`), when it has
118
+ * one. The caller resolves it from the project, as for `invokeAgent`'s
119
+ * `orgId`, so the resumed turn's tools and guardrails see it too.
120
+ */
121
+ readonly orgId?: OrgId;
109
122
  }
110
123
 
111
124
  /**
@@ -126,7 +139,8 @@ export interface ResumeAgentTurnInput {
126
139
  * runId — this happens when the setup handler's snapshot write failed
127
140
  * (the write is best-effort). Recovery: cancel the run explicitly;
128
141
  * there is no way to reconstruct the InvokeAgentInput without the
129
- * snapshot.
142
+ * snapshot. Fails with `run-snapshot-unreadable` when reading it failed;
143
+ * resuming again may work.
130
144
  */
131
145
  /**
132
146
  * Rebuild a resumed turn's context from its journal (see
@@ -167,9 +181,9 @@ export async function resumeAgentTurn(
167
181
  return {
168
182
  kind: 'err',
169
183
  error: {
170
- code: 'run-snapshot-missing',
171
- message: `Failed to load run snapshot for run ${input.runId as unknown as string}: ${snapshotResult.error.message}`,
172
- } as never,
184
+ code: 'run-snapshot-unreadable',
185
+ message: `Could not read the run snapshot of run ${input.runId as unknown as string}: ${snapshotResult.error.message}`,
186
+ },
173
187
  };
174
188
  }
175
189
  const snapshot = snapshotResult.value;
@@ -179,7 +193,7 @@ export async function resumeAgentTurn(
179
193
  error: {
180
194
  code: 'run-snapshot-missing',
181
195
  message: `No run snapshot for run ${input.runId as unknown as string} — resume-context unavailable.`,
182
- } as never,
196
+ },
183
197
  };
184
198
  }
185
199
 
@@ -202,25 +216,7 @@ export async function resumeAgentTurn(
202
216
  }
203
217
 
204
218
  // 3. Reconstruct the InvokeAgentInput envelope from the snapshot.
205
- const reconstructedInput: InvokeAgentInput = {
206
- tenantId: snapshot.tenantId,
207
- projectId: snapshot.projectId,
208
- agent: input.agent,
209
- conversationId: snapshot.conversationId,
210
- userMessage: snapshot.userMessage,
211
- ...(snapshot.parameters !== undefined && { parameters: snapshot.parameters }),
212
- ...(snapshot.input !== undefined && { input: snapshot.input }),
213
- ...(snapshot.participantId !== undefined && { participantId: snapshot.participantId }),
214
- ...(snapshot.dryRun && { dryRun: true }),
215
- ...(snapshot.principal !== undefined &&
216
- snapshot.principal !== null && {
217
- principal: snapshot.principal as Principal,
218
- }),
219
- ...(snapshot.authz !== undefined &&
220
- snapshot.authz !== null && {
221
- authz: snapshot.authz as { readonly fgaApiUrl: string },
222
- }),
223
- };
219
+ const reconstructedInput = turnInputFromSnapshot(snapshot, input);
224
220
 
225
221
  // 4. Build the same TurnContext + handlers as invokeAgent().
226
222
  const started = Date.now();
@@ -287,6 +283,7 @@ export type {
287
283
  InvokeAgentError,
288
284
  ModelInvocationError,
289
285
  OutputSchemaViolationError,
286
+ RunSnapshotError,
290
287
  ToolInvocationError,
291
288
  UnresolvedToolError,
292
289
  } from './handlers/errors.js';
@@ -297,3 +294,34 @@ export type {
297
294
  InvokeAgentBindings,
298
295
  InvokeAgentInput,
299
296
  } from './handlers/public-types.js';
297
+
298
+ /**
299
+ * The `InvokeAgentInput` a resumed turn runs with: the one captured in its
300
+ * snapshot at run start, the agent the caller resolved, and the org of the
301
+ * run's project, which the caller resolves as for `invokeAgent`.
302
+ */
303
+ export function turnInputFromSnapshot(
304
+ snapshot: RunSnapshotRecord,
305
+ input: Pick<ResumeAgentTurnInput, 'agent' | 'orgId'>,
306
+ ): InvokeAgentInput {
307
+ return {
308
+ tenantId: snapshot.tenantId,
309
+ projectId: snapshot.projectId,
310
+ ...(input.orgId !== undefined && { orgId: input.orgId }),
311
+ agent: input.agent,
312
+ conversationId: snapshot.conversationId,
313
+ userMessage: snapshot.userMessage,
314
+ ...(snapshot.parameters !== undefined && { parameters: snapshot.parameters }),
315
+ ...(snapshot.input !== undefined && { input: snapshot.input }),
316
+ ...(snapshot.participantId !== undefined && { participantId: snapshot.participantId }),
317
+ ...(snapshot.dryRun && { dryRun: true }),
318
+ ...(snapshot.principal !== undefined &&
319
+ snapshot.principal !== null && {
320
+ principal: snapshot.principal as Principal,
321
+ }),
322
+ ...(snapshot.authz !== undefined &&
323
+ snapshot.authz !== null && {
324
+ authz: snapshot.authz as { readonly fgaApiUrl: string },
325
+ }),
326
+ };
327
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  Provenance,
7
7
  ProvenanceBuilder,
8
8
  ProvenanceEmitBinding,
9
+ ProvenanceEmitContext,
9
10
  } from '@kindgi/provenance';
10
11
  import { signProvenance } from '@kindgi/provenance';
11
12
  import type { Result } from '@kindgi/types';
@@ -55,6 +56,8 @@ export interface ProvenanceBindings {
55
56
  export async function persistProvenance(
56
57
  builder: ProvenanceBuilder,
57
58
  bindings: ProvenanceBindings,
59
+ /** Stored beside the record (the turn's project); see `ProvenanceEmitContext`. */
60
+ context?: ProvenanceEmitContext,
58
61
  ): Promise<Result<Provenance, PersistenceError>> {
59
62
  let provenance: Provenance = builder.snapshot();
60
63
  if (bindings.keyProvider !== undefined) {
@@ -84,7 +87,7 @@ export async function persistProvenance(
84
87
  },
85
88
  };
86
89
  }
87
- const emitted = await bindings.emitBinding.emit(provenance);
90
+ const emitted = await bindings.emitBinding.emit(provenance, context);
88
91
  if (emitted.kind === 'err') {
89
92
  return {
90
93
  kind: 'err',
package/src/schema.ts CHANGED
@@ -43,6 +43,13 @@ export const agentConversations = pgTable(
43
43
  * SSO subject). Kept as text so the caller decides the shape.
44
44
  */
45
45
  participantId: text('participant_id'),
46
+ /**
47
+ * The project the conversation is in, set when it is opened (a run
48
+ * opens it in the run's project). Null when opened without one, and
49
+ * on conversations from before the column (nothing is backfilled).
50
+ * No foreign key: `projects` belongs to the runtime's schema.
51
+ */
52
+ projectId: uuid('project_id'),
46
53
  /**
47
54
  * Structural scope — project id, matter id, etc. Persisted through
48
55
  * the versioning envelope so shape evolution is migrate-on-read.
@@ -74,6 +81,10 @@ export const agentConversations = pgTable(
74
81
  t.tenantId,
75
82
  t.participantId,
76
83
  ),
84
+ /** One project's conversations, newest first (`listConversationsPage({ scope })`). */
85
+ tenantProjectOpenedIdx: index('agent_conversations_tenant_project_opened_idx')
86
+ .on(t.tenantId, t.projectId, t.openedAt.desc())
87
+ .where(sql`${t.projectId} IS NOT NULL`),
77
88
  /** Partial index: open conversations sorted by recency. Used by the UI list. */
78
89
  openRecentIdx: index('agent_conversations_open_recent_idx')
79
90
  .on(t.tenantId, t.lastMessageAt.desc())
package/src/types.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import type { Capability } from '@kindgi/capabilities';
5
5
  import type { Fact, MemoryScope } from '@kindgi/memory';
6
6
  import type { ToolErrorsSpec, ToolHitlMode, ToolHitlRule } from '@kindgi/policy-contract';
7
- import type { Brand, ConversationId, Semver, TenantId, Timestamp } from '@kindgi/types';
7
+ import type { Brand, ConversationId, ProjectId, Semver, TenantId, Timestamp } from '@kindgi/types';
8
8
 
9
9
  /**
10
10
  * Branded agent id. Convention: dotted namespace under the tenant's
@@ -204,6 +204,14 @@ export interface ConversationHitlPolicy {
204
204
  * (see `resolveEffectiveHitlPolicy`).
205
205
  */
206
206
  readonly timeoutMs?: number;
207
+ /**
208
+ * What happens when an approval's time runs out: it escalates one
209
+ * reviewer tier, and at `admin` it expires (the turn fails with
210
+ * `hitl-cancelled`). `'escalate'` is the only behavior today, and the
211
+ * default; `'auto-approve'` and `'auto-reject'` are refused rather than
212
+ * accepted and ignored.
213
+ */
214
+ readonly onTimeout?: 'escalate';
207
215
  }
208
216
 
209
217
  /**
@@ -416,6 +424,12 @@ export interface Conversation {
416
424
  readonly title: string;
417
425
  /** UUID or free-form identifier for the human participant. */
418
426
  readonly participantId?: string;
427
+ /**
428
+ * The project the conversation is in, set when it is opened (by a run,
429
+ * from the run's project). Absent on conversations opened without one,
430
+ * and on those from before it was stored.
431
+ */
432
+ readonly projectId?: ProjectId;
419
433
  /** Additional attributes (project id, matter id, etc.). */
420
434
  readonly scope: MemoryScope;
421
435
  readonly openedAt: Timestamp;