@kindgi/agents 0.1.4 → 0.1.5

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 (137) hide show
  1. package/README.md +1 -1
  2. package/dist/blocks.d.ts +19 -0
  3. package/dist/blocks.d.ts.map +1 -1
  4. package/dist/blocks.js +59 -1
  5. package/dist/blocks.js.map +1 -1
  6. package/dist/conversation-binding.d.ts +49 -3
  7. package/dist/conversation-binding.d.ts.map +1 -1
  8. package/dist/define.d.ts +7 -1
  9. package/dist/define.d.ts.map +1 -1
  10. package/dist/define.js +132 -7
  11. package/dist/define.js.map +1 -1
  12. package/dist/drafted-template.d.ts +34 -0
  13. package/dist/drafted-template.d.ts.map +1 -0
  14. package/dist/drafted-template.js +95 -0
  15. package/dist/drafted-template.js.map +1 -0
  16. package/dist/guardrails-gate.d.ts +28 -13
  17. package/dist/guardrails-gate.d.ts.map +1 -1
  18. package/dist/guardrails-gate.js +59 -21
  19. package/dist/guardrails-gate.js.map +1 -1
  20. package/dist/handlers/build-initial-messages.d.ts +8 -2
  21. package/dist/handlers/build-initial-messages.d.ts.map +1 -1
  22. package/dist/handlers/build-initial-messages.js +23 -21
  23. package/dist/handlers/build-initial-messages.js.map +1 -1
  24. package/dist/handlers/compose-result.d.ts.map +1 -1
  25. package/dist/handlers/compose-result.js +21 -0
  26. package/dist/handlers/compose-result.js.map +1 -1
  27. package/dist/handlers/context.d.ts +6 -1
  28. package/dist/handlers/context.d.ts.map +1 -1
  29. package/dist/handlers/dispatch-tools.d.ts.map +1 -1
  30. package/dist/handlers/dispatch-tools.js +17 -5
  31. package/dist/handlers/dispatch-tools.js.map +1 -1
  32. package/dist/handlers/errors.d.ts +14 -1
  33. package/dist/handlers/errors.d.ts.map +1 -1
  34. package/dist/handlers/errors.js.map +1 -1
  35. package/dist/handlers/evaluate-guardrails.d.ts +6 -1
  36. package/dist/handlers/evaluate-guardrails.d.ts.map +1 -1
  37. package/dist/handlers/evaluate-guardrails.js +46 -4
  38. package/dist/handlers/evaluate-guardrails.js.map +1 -1
  39. package/dist/handlers/history.d.ts +24 -0
  40. package/dist/handlers/history.d.ts.map +1 -0
  41. package/dist/handlers/history.js +52 -0
  42. package/dist/handlers/history.js.map +1 -0
  43. package/dist/handlers/persist-final-message.d.ts.map +1 -1
  44. package/dist/handlers/persist-final-message.js +2 -0
  45. package/dist/handlers/persist-final-message.js.map +1 -1
  46. package/dist/handlers/persist-user-message.d.ts.map +1 -1
  47. package/dist/handlers/persist-user-message.js +2 -0
  48. package/dist/handlers/persist-user-message.js.map +1 -1
  49. package/dist/handlers/public-types.d.ts +12 -2
  50. package/dist/handlers/public-types.d.ts.map +1 -1
  51. package/dist/handlers/rehydrate.d.ts.map +1 -1
  52. package/dist/handlers/rehydrate.js +7 -4
  53. package/dist/handlers/rehydrate.js.map +1 -1
  54. package/dist/handlers/remember-tool.d.ts +22 -0
  55. package/dist/handlers/remember-tool.d.ts.map +1 -0
  56. package/dist/handlers/remember-tool.js +156 -0
  57. package/dist/handlers/remember-tool.js.map +1 -0
  58. package/dist/handlers/replay.d.ts +55 -1
  59. package/dist/handlers/replay.d.ts.map +1 -1
  60. package/dist/handlers/replay.js +23 -6
  61. package/dist/handlers/replay.js.map +1 -1
  62. package/dist/handlers/resolve-blocks.d.ts.map +1 -1
  63. package/dist/handlers/resolve-blocks.js +19 -8
  64. package/dist/handlers/resolve-blocks.js.map +1 -1
  65. package/dist/handlers/result-shape.d.ts +10 -5
  66. package/dist/handlers/result-shape.d.ts.map +1 -1
  67. package/dist/handlers/result-shape.js.map +1 -1
  68. package/dist/handlers/run-retrievals.d.ts +2 -1
  69. package/dist/handlers/run-retrievals.d.ts.map +1 -1
  70. package/dist/handlers/run-retrievals.js +42 -16
  71. package/dist/handlers/run-retrievals.js.map +1 -1
  72. package/dist/handlers/turn-environment.d.ts.map +1 -1
  73. package/dist/handlers/turn-environment.js +2 -1
  74. package/dist/handlers/turn-environment.js.map +1 -1
  75. package/dist/handlers/turn-provenance.d.ts +19 -3
  76. package/dist/handlers/turn-provenance.d.ts.map +1 -1
  77. package/dist/handlers/turn-provenance.js +116 -2
  78. package/dist/handlers/turn-provenance.js.map +1 -1
  79. package/dist/index.d.ts +13 -8
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +5 -3
  82. package/dist/index.js.map +1 -1
  83. package/dist/invoke.d.ts +1 -1
  84. package/dist/invoke.d.ts.map +1 -1
  85. package/dist/invoke.js +2 -0
  86. package/dist/invoke.js.map +1 -1
  87. package/dist/remember.d.ts +49 -0
  88. package/dist/remember.d.ts.map +1 -0
  89. package/dist/remember.js +89 -0
  90. package/dist/remember.js.map +1 -0
  91. package/dist/retrieval.d.ts +114 -28
  92. package/dist/retrieval.d.ts.map +1 -1
  93. package/dist/retrieval.js +433 -104
  94. package/dist/retrieval.js.map +1 -1
  95. package/dist/schema.d.ts +17 -0
  96. package/dist/schema.d.ts.map +1 -1
  97. package/dist/schema.js +6 -0
  98. package/dist/schema.js.map +1 -1
  99. package/dist/streaming.d.ts +16 -1
  100. package/dist/streaming.d.ts.map +1 -1
  101. package/dist/streaming.js.map +1 -1
  102. package/dist/types.d.ts +136 -10
  103. package/dist/types.d.ts.map +1 -1
  104. package/migrations/0005_condemned_hellcat.sql +1 -0
  105. package/migrations/meta/0005_snapshot.json +333 -0
  106. package/migrations/meta/_journal.json +7 -0
  107. package/package.json +15 -15
  108. package/src/blocks.ts +75 -1
  109. package/src/conversation-binding.ts +53 -3
  110. package/src/define.ts +144 -9
  111. package/src/drafted-template.ts +118 -0
  112. package/src/guardrails-gate.ts +90 -26
  113. package/src/handlers/build-initial-messages.ts +29 -22
  114. package/src/handlers/compose-result.ts +21 -0
  115. package/src/handlers/context.ts +12 -1
  116. package/src/handlers/dispatch-tools.ts +19 -5
  117. package/src/handlers/errors.ts +16 -1
  118. package/src/handlers/evaluate-guardrails.ts +47 -4
  119. package/src/handlers/history.ts +57 -0
  120. package/src/handlers/persist-final-message.ts +2 -0
  121. package/src/handlers/persist-user-message.ts +2 -0
  122. package/src/handlers/public-types.ts +18 -2
  123. package/src/handlers/rehydrate.ts +12 -8
  124. package/src/handlers/remember-tool.ts +207 -0
  125. package/src/handlers/replay.ts +80 -8
  126. package/src/handlers/resolve-blocks.ts +21 -7
  127. package/src/handlers/result-shape.ts +19 -5
  128. package/src/handlers/run-retrievals.ts +52 -19
  129. package/src/handlers/turn-environment.ts +2 -1
  130. package/src/handlers/turn-provenance.ts +133 -2
  131. package/src/index.ts +33 -2
  132. package/src/invoke.ts +3 -0
  133. package/src/remember.ts +136 -0
  134. package/src/retrieval.ts +591 -125
  135. package/src/schema.ts +6 -0
  136. package/src/streaming.ts +17 -0
  137. package/src/types.ts +134 -10
@@ -9,6 +9,12 @@
9
9
  * - `live`: the tool runs. Only a tool declared read-only (see
10
10
  * `isReadOnlyTool`) with no approval to wait for can; a `live`
11
11
  * decision for any other is refused here, whatever the binding says;
12
+ * - `recomputed`: the tool runs, as for `live`, because the replayed
13
+ * version pins other settings than the past run's, and the tool
14
+ * reads from nowhere (no `reads`, `network` or `sensitive-data-egress`
15
+ * effect): its result is computed again from the same arguments, so
16
+ * the replay hasn't diverged from the past run. A tool that does read
17
+ * from somewhere runs as `live`;
12
18
  * - `recorded`: the past run's result for the same call is used;
13
19
  * - `refused`: the tool doesn't run, and the model gets the given result.
14
20
  *
@@ -24,7 +30,7 @@ import type { JournalEntry, ValueRecordedPayload } from '@kindgi/runtime';
24
30
  import type { Tool } from '@kindgi/tools';
25
31
  import type { RunId, TenantId } from '@kindgi/types';
26
32
 
27
- import type { RetrievedFact } from '../types.js';
33
+ import type { RecalledMemory, RetrievedFact } from '../types.js';
28
34
 
29
35
  import type { TurnContext } from './context.js';
30
36
  import { throwAgentTurnFailure } from './errors.js';
@@ -53,7 +59,19 @@ export interface ReplayToolInput extends ReplayTurnRef {
53
59
  }
54
60
 
55
61
  export type ReplayToolDecision =
56
- | { readonly kind: 'live' }
62
+ | {
63
+ readonly kind: 'live';
64
+ /**
65
+ * The env values the past run's call of this tool was sent, for a
66
+ * read-only tool run live: the replay sends them, so the tool reads
67
+ * the config the past run saw rather than today's. Names it doesn't
68
+ * hold (a tool version that declares more) resolve as usual.
69
+ * Absent: the past run recorded none (from before env was recorded,
70
+ * or a tool it didn't call), and the call resolves today's values.
71
+ */
72
+ readonly env?: Readonly<Record<string, string>>;
73
+ }
74
+ | { readonly kind: 'recomputed' }
57
75
  | { readonly kind: 'recorded'; readonly result: unknown }
58
76
  | { readonly kind: 'refused'; readonly result: unknown; readonly reason: string };
59
77
 
@@ -75,12 +93,35 @@ export interface ReplayBinding {
75
93
  * retrieve live. Absent: retrieve live.
76
94
  */
77
95
  retrievals?(input: ReplayTurnRef): Promise<readonly RetrievedFact[] | undefined>;
96
+ /**
97
+ * What the turn's recall of earlier conversations returns when its
98
+ * retrievals are the past run's: the messages the past run recalled.
99
+ * Absent (or `undefined`): none, as a run from before recall had.
100
+ */
101
+ recalled?(input: ReplayTurnRef): Promise<readonly RecalledMemory[] | undefined>;
78
102
  /**
79
103
  * The past run's decision at the session approval gate, when the replay
80
104
  * reaches that gate: the replay follows it. `undefined` (or absent): the
81
105
  * gate is skipped, and the result says so.
82
106
  */
83
107
  sessionApproval?(input: ReplayTurnRef): Promise<ReplayApproval | undefined>;
108
+ /**
109
+ * Block content the replay runs instead of the agent version's pinned
110
+ * content (a comparison's `overrides`: an improvement pass's search):
111
+ * settings values and prompt templates, by block id. The same for every
112
+ * turn of a replay, on resume too. `undefined` (or absent): the pinned
113
+ * content.
114
+ */
115
+ overrides?(input: {
116
+ readonly tenantId: TenantId;
117
+ readonly replay: RunReplayRef;
118
+ }): Promise<ReplayOverrides | undefined>;
119
+ }
120
+
121
+ /** Block content a replay runs instead of the pinned content, by block id. */
122
+ export interface ReplayOverrides {
123
+ readonly settings?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
124
+ readonly prompts?: Readonly<Record<string, { readonly template: string }>>;
84
125
  }
85
126
 
86
127
  /** One tool call of a replay turn, and what happened to it. */
@@ -91,6 +132,12 @@ export interface ReplayToolTrace {
91
132
  readonly toolVersion: string;
92
133
  readonly arguments: unknown;
93
134
  readonly source: 'live' | 'recorded' | 'refused';
135
+ /**
136
+ * With `live`: it ran again from the same arguments under other
137
+ * settings and reads from nowhere, so its result isn't new data (the
138
+ * replay hasn't diverged). Absent otherwise.
139
+ */
140
+ readonly recomputed?: true;
94
141
  /** Why it was refused. A refused call is what the turn would have done. */
95
142
  readonly reason?: string;
96
143
  }
@@ -119,6 +166,20 @@ const CHANGING_EFFECTS: ReadonlySet<string> = new Set([
119
166
  'external-side-effect',
120
167
  ]);
121
168
 
169
+ /** Effects that read from somewhere: a re-run of such a tool can see other data. */
170
+ const READING_EFFECTS: ReadonlySet<string> = new Set(['reads', 'network', 'sensitive-data-egress']);
171
+
172
+ /** A read-only tool that reads from nowhere either: its result follows from its arguments (and settings). */
173
+ export function isComputeOnlyTool(tool: {
174
+ readonly mutating?: boolean;
175
+ readonly effects?: readonly { readonly kind: string }[] | readonly string[];
176
+ }): boolean {
177
+ return (
178
+ isReadOnlyTool(tool) &&
179
+ !(tool.effects ?? []).some((e) => READING_EFFECTS.has(typeof e === 'string' ? e : e.kind))
180
+ );
181
+ }
182
+
122
183
  /** A tool that declares it changes nothing: `mutating: false`, and no changing effect. */
123
184
  export function isReadOnlyTool(tool: {
124
185
  readonly mutating?: boolean;
@@ -139,9 +200,10 @@ function refusedResult(reason: string): Readonly<Record<string, unknown>> {
139
200
  return { status: 'not-executed', reason };
140
201
  }
141
202
 
142
- /** The journaled decision of one call: its trace entry, and the result the model got. */
203
+ /** The journaled decision of one call: its trace entry, the result the model got, and a live call's env. */
143
204
  interface RecordedDecision extends ReplayToolTrace {
144
205
  readonly result?: unknown;
206
+ readonly env?: Readonly<Record<string, string>>;
145
207
  }
146
208
 
147
209
  /**
@@ -202,21 +264,31 @@ export async function decideReplayTool(
202
264
  if (decision.kind === 'refused') {
203
265
  return { ...base, source: 'refused', reason: decision.reason, result: decision.result };
204
266
  }
205
- // `live` runs only a read-only tool, and never waits for an approval.
267
+ // `live` and `recomputed` run only a read-only tool, and never wait
268
+ // for an approval; a tool that reads from somewhere is `live`.
206
269
  if (!isReadOnlyTool(call.tool)) return refuse(CHANGES_REASON);
207
270
  if (call.gated) return refuse(GATED_REASON);
208
- return { ...base, source: 'live' };
271
+ const recomputed = decision.kind === 'recomputed' && isComputeOnlyTool(call.tool);
272
+ return {
273
+ ...base,
274
+ source: 'live',
275
+ ...(recomputed && { recomputed: true as const }),
276
+ ...(decision.kind === 'live' && decision.env !== undefined && { env: decision.env }),
277
+ };
209
278
  },
210
279
  );
211
280
  addReplayTrace(ctx, traceOf(decided));
212
- if (decided.source === 'live') return { kind: 'live' };
281
+ if (decided.source === 'live') {
282
+ if (decided.recomputed === true) return { kind: 'recomputed' };
283
+ return { kind: 'live', ...(decided.env !== undefined && { env: decided.env }) };
284
+ }
213
285
  if (decided.source === 'recorded') return { kind: 'recorded', result: decided.result };
214
286
  return { kind: 'refused', result: decided.result, reason: decided.reason ?? CHANGES_REASON };
215
287
  }
216
288
 
217
- /** A journaled decision's trace entry (without the result). */
289
+ /** A journaled decision's trace entry (without the result or the env values). */
218
290
  function traceOf(decided: RecordedDecision): ReplayToolTrace {
219
- const { result: _result, ...trace } = decided;
291
+ const { result: _result, env: _env, ...trace } = decided;
220
292
  return trace;
221
293
  }
222
294
 
@@ -13,6 +13,7 @@ import {
13
13
 
14
14
  import type { TurnContext } from './context.js';
15
15
  import { throwAgentTurnFailure } from './errors.js';
16
+ import type { ReplayOverrides } from './replay.js';
16
17
 
17
18
  /** The block versions a turn runs, by kind then id, as `setup` journals them. */
18
19
  export interface PinnedBlockVersions {
@@ -90,7 +91,14 @@ export async function resolveTurnBlocks(
90
91
  versions[ref.kind][ref.id] = version;
91
92
  loaded.set(ref, block);
92
93
  }
93
- return turnBlocks(refs, loaded, versions);
94
+ return turnBlocks(refs, loaded, versions, await replayOverrides(ctx));
95
+ }
96
+
97
+ /** A replay's block content in place of the pinned content (a comparison's overrides), if any. */
98
+ async function replayOverrides(ctx: TurnContext): Promise<ReplayOverrides | undefined> {
99
+ const replay = ctx.input.replay;
100
+ if (replay === undefined || ctx.bindings.replay?.overrides === undefined) return undefined;
101
+ return ctx.bindings.replay.overrides({ tenantId: ctx.input.tenantId, replay });
94
102
  }
95
103
 
96
104
  function blockRefs(ctx: TurnContext): Ref[] {
@@ -139,6 +147,7 @@ function turnBlocks(
139
147
  refs: readonly Ref[],
140
148
  loaded: ReadonlyMap<Ref, BlockDefinition>,
141
149
  versions: PinnedBlockVersions,
150
+ overrides?: ReplayOverrides,
142
151
  ): TurnBlocks {
143
152
  const settings: Record<string, Readonly<Record<string, unknown>>> = {};
144
153
  let prompt: TurnBlocks['prompt'];
@@ -146,18 +155,23 @@ function turnBlocks(
146
155
  for (const ref of refs) {
147
156
  const block = loaded.get(ref) as BlockDefinition;
148
157
  if (block.kind === 'prompt') {
149
- prompt = { id: block.id, version: block.version, content: block.content };
150
- } else if (ref.role === 'model-settings') {
151
- const issues = settingsSchemaIssues(block.content.values, MODEL_SETTINGS_SCHEMA);
158
+ const template = overrides?.prompts?.[block.id]?.template;
159
+ const content = template === undefined ? block.content : { ...block.content, template };
160
+ prompt = { id: block.id, version: block.version, content };
161
+ continue;
162
+ }
163
+ const values = overrides?.settings?.[block.id] ?? block.content.values;
164
+ if (ref.role === 'model-settings') {
165
+ const issues = settingsSchemaIssues(values, MODEL_SETTINGS_SCHEMA);
152
166
  if (issues.length > 0) {
153
167
  fail(
154
168
  ref,
155
- `${describe(ref)} version ${block.version} isn't model settings: ${issues.map((i) => `${i.path} ${i.message}`).join('; ')}`,
169
+ `${describe(ref)} version ${block.version}${overrides?.settings?.[block.id] !== undefined ? " (with the replay's values)" : ''} isn't model settings: ${issues.map((i) => `${i.path} ${i.message}`).join('; ')}`,
156
170
  );
157
171
  }
158
- modelSettings = block.content.values as ModelSettings;
172
+ modelSettings = values as ModelSettings;
159
173
  } else {
160
- settings[block.id] = block.content.values;
174
+ settings[block.id] = values;
161
175
  }
162
176
  }
163
177
  return {
@@ -5,7 +5,12 @@ import type { EvaluationResult } from '@kindgi/guardrails';
5
5
  import type { Provenance } from '@kindgi/provenance';
6
6
  import type { RunId } from '@kindgi/types';
7
7
 
8
- import type { ConversationId, ConversationMessage, RetrievedFact } from '../types.js';
8
+ import type {
9
+ ConversationId,
10
+ ConversationMessage,
11
+ RecalledMemory,
12
+ RetrievedFact,
13
+ } from '../types.js';
9
14
 
10
15
  import type { ReplayTurnReport } from './replay.js';
11
16
 
@@ -31,11 +36,18 @@ export interface AgentTurnUsage {
31
36
  */
32
37
  export interface AgentTurnWarning {
33
38
  /**
34
- * `fallback-provider`: a fallback provider answered. Any other code is a
35
- * provider's own warning about its answers (`ModelCallResult.warnings`),
36
- * such as dev-echo's `dev-echo-not-a-model`.
39
+ * `fallback-provider`: a fallback provider answered.
40
+ * `memory-needs-participant`: the agent keeps memory per end user
41
+ * (`same-user`), but the run named none (`participantId`), so it read and
42
+ * kept none.
43
+ * Any other code is a provider's own warning about its answers
44
+ * (`ModelCallResult.warnings`), such as dev-echo's `dev-echo-not-a-model`.
37
45
  */
38
- readonly code: 'fallback-provider' | 'dev-echo-not-a-model' | (string & {});
46
+ readonly code:
47
+ | 'fallback-provider'
48
+ | 'memory-needs-participant'
49
+ | 'dev-echo-not-a-model'
50
+ | (string & {});
39
51
  readonly message: string;
40
52
  }
41
53
 
@@ -47,6 +59,8 @@ export interface AgentTurnResult {
47
59
  readonly appended: readonly ConversationMessage[];
48
60
  readonly response: ConversationMessage;
49
61
  readonly retrieved: readonly RetrievedFact[];
62
+ /** Messages of earlier conversations the turn recalled (intents over conversations). */
63
+ readonly recalled?: readonly RecalledMemory[];
50
64
  readonly violations: readonly EvaluationResult[];
51
65
  readonly usage: AgentTurnUsage;
52
66
  readonly provider: { readonly id: string; readonly model: string };
@@ -3,19 +3,21 @@
3
3
 
4
4
  import type { NodeContext, NodeHandler } from '@kindgi/handler';
5
5
 
6
- import { runRetrievals } from '../retrieval.js';
6
+ import { runUserId } from '../remember.js';
7
+ import { type RetrievalPass, retrieveForTurn } from '../retrieval.js';
7
8
  import { emitTurnEvent } from '../streaming.js';
8
- import type { RetrievedFact } from '../types.js';
9
9
 
10
10
  import type { TurnContext } from './context.js';
11
11
  import { throwAgentTurnFailure } from './errors.js';
12
+ import { historyStart } from './history.js';
12
13
  import { addRetrievalNodes } from './turn-provenance.js';
13
14
 
14
15
  /**
15
16
  * Execute the agent's declared retrieval intents. Populates
16
17
  * `ctx.retrieved` for downstream prompt building + emits
17
18
  * `retrieval.completed`. Adds provenance nodes + edges when a builder
18
- * is wired.
19
+ * is wired. The journal keeps the facts (with each one's rank in each
20
+ * search) and the intents that ran degraded.
19
21
  */
20
22
  export function buildRunRetrievalsHandler(ctx: TurnContext): NodeHandler {
21
23
  return async (_input, kctx) => {
@@ -26,8 +28,11 @@ export function buildRunRetrievalsHandler(ctx: TurnContext): NodeHandler {
26
28
  cause: null,
27
29
  });
28
30
  }
29
- const facts = (await recordedRetrievals(ctx, kctx)) ?? (await retrieveLive(ctx));
31
+ const recorded = await recordedRetrievals(ctx, kctx);
32
+ const pass = recorded ?? (await retrieveLive(ctx));
33
+ const facts = pass.facts;
30
34
  ctx.retrieved = facts;
35
+ ctx.recalled = pass.recalled;
31
36
 
32
37
  await emitTurnEvent(ctx.bindings.onEvent, {
33
38
  kind: 'retrieval.completed',
@@ -36,32 +41,52 @@ export function buildRunRetrievalsHandler(ctx: TurnContext): NodeHandler {
36
41
  });
37
42
 
38
43
  if (ctx.provenance !== undefined && ctx.userMessage !== undefined) {
39
- addRetrievalNodes(ctx.provenance, facts, ctx.userMessage);
44
+ addRetrievalNodes(
45
+ ctx.provenance,
46
+ ctx.input.agent.retrieval,
47
+ facts,
48
+ ctx.userMessage,
49
+ pass.recalled,
50
+ );
40
51
  }
41
52
 
42
- // The facts go in the journal: a resumed turn restores them from it
43
- // (`rehydrateTurnContext`) rather than retrieving again.
44
- return { count: facts.length, retrieved: facts };
53
+ // The facts and recalled messages go in the journal: a resumed turn
54
+ // restores them from it (`rehydrateTurnContext`) rather than retrieving again.
55
+ return {
56
+ count: facts.length,
57
+ retrieved: facts,
58
+ ...(pass.recalled.length > 0 && { recalled: pass.recalled }),
59
+ ...(pass.degraded.length > 0 && { degraded: pass.degraded }),
60
+ };
45
61
  };
46
62
  }
47
63
 
48
- /** A replay's retrievals: what the past run retrieved, when the replay binding has it. */
64
+ /**
65
+ * A replay's retrievals: what the past run retrieved and recalled, when
66
+ * the replay binding has them.
67
+ */
49
68
  async function recordedRetrievals(
50
69
  ctx: TurnContext,
51
70
  kctx: NodeContext,
52
- ): Promise<readonly RetrievedFact[] | undefined> {
71
+ ): Promise<RetrievalPass | undefined> {
53
72
  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
- });
73
+ const binding = ctx.bindings.replay;
74
+ if (replay === undefined || binding?.retrievals === undefined) return undefined;
75
+ const ref = { tenantId: ctx.input.tenantId, runId: kctx.runId, replay };
76
+ const facts = await binding.retrievals(ref);
77
+ if (facts === undefined) return undefined;
78
+ const recalled = (await binding.recalled?.(ref)) ?? [];
79
+ return { facts, recalled, degraded: [] };
60
80
  }
61
81
 
62
- async function retrieveLive(ctx: TurnContext): Promise<readonly RetrievedFact[]> {
63
- if (ctx.conversation === undefined) return [];
64
- const retrieved = await runRetrievals(
82
+ async function retrieveLive(ctx: TurnContext): Promise<RetrievalPass> {
83
+ if (ctx.conversation === undefined) return { facts: [], recalled: [], degraded: [] };
84
+ const userId = runUserId(ctx.input.principal);
85
+ const recallsOlder = ctx.input.agent.retrieval.some(
86
+ (i) => i.source === 'conversations' && i.scope === 'same-conversation',
87
+ );
88
+ const historyFrom = recallsOlder ? await historyStart(ctx) : undefined;
89
+ const retrieved = await retrieveForTurn(
65
90
  ctx.input.agent,
66
91
  ctx.conversation,
67
92
  ctx.input.conversationId,
@@ -75,6 +100,14 @@ async function retrieveLive(ctx: TurnContext): Promise<readonly RetrievedFact[]>
75
100
  embeddingModel: ctx.bindings.embeddingModel,
76
101
  }),
77
102
  },
103
+ {
104
+ projectId: ctx.input.projectId,
105
+ ...(ctx.input.orgId !== undefined && { orgId: ctx.input.orgId }),
106
+ ...(ctx.input.participantId !== undefined && { participantId: ctx.input.participantId }),
107
+ ...(userId !== undefined && { userId }),
108
+ ...(ctx.input.segments !== undefined && { segments: ctx.input.segments }),
109
+ ...(historyFrom !== undefined && { historyFrom }),
110
+ },
78
111
  );
79
112
  if (retrieved.kind === 'err') throwAgentTurnFailure(retrieved.error);
80
113
  return retrieved.value;
@@ -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 { withRememberTool } from './remember-tool.js';
24
25
  import { type PinnedBlockVersions, resolveTurnBlocks } from './resolve-blocks.js';
25
26
  import { resolveTurnTools } from './resolve-tools.js';
26
27
  import { type ToolErrorPolicy, effectiveToolErrorPolicy } from './tool-errors.js';
@@ -102,7 +103,7 @@ export async function resolveTurnEnvironment(
102
103
  // This turn's tools come from the tenant's own registry — never a
103
104
  // registry shared across concurrent turns of other tenants.
104
105
  const tenantTools = await ctx.bindings.toolRegistry.forTenant(ctx.input.tenantId);
105
- ctx.tools = resolveTurnTools(tenantTools, ctx.input.agent, pinnedTools);
106
+ ctx.tools = withRememberTool(ctx, resolveTurnTools(tenantTools, ctx.input.agent, pinnedTools));
106
107
  // The data blocks, at the versions pinned (the turn's own on resume).
107
108
  const blocks = await resolveTurnBlocks(ctx, pinnedBlocks);
108
109
  if (blocks !== undefined) ctx.blocks = blocks;
@@ -12,9 +12,16 @@
12
12
  import type { ProvenanceBuilder } from '@kindgi/provenance';
13
13
  import type { Timestamp } from '@kindgi/types';
14
14
 
15
- import type { ConversationMessage, RetrievedFact } from '../types.js';
15
+ import { REMEMBER_TOOL_ID } from '../remember.js';
16
+ import type {
17
+ ConversationMessage,
18
+ RecalledMemory,
19
+ RetrievalIntent,
20
+ RetrievedFact,
21
+ } from '../types.js';
16
22
  import type { TurnContext } from './context.js';
17
23
  import type { GateDecision } from './gate-decision.js';
24
+ import type { RememberToolOutput } from './remember-tool.js';
18
25
 
19
26
  /** The turn's user message. */
20
27
  export function addInputNode(provenance: ProvenanceBuilder, message: ConversationMessage): void {
@@ -26,12 +33,43 @@ export function addInputNode(provenance: ProvenanceBuilder, message: Conversatio
26
33
  });
27
34
  }
28
35
 
29
- /** The facts the turn retrieved for its user message. */
36
+ /**
37
+ * The turn's memory searches and the facts they found for its user
38
+ * message. Each retrieval intent is one `memory-read` node, `operation:
39
+ * search_memory` (the OpenTelemetry GenAI name), `caused-by` the input,
40
+ * with what it searched and the ids it found; each fact is a `retrieval`
41
+ * node `retrieved-from` its search and `influenced-by` the input.
42
+ */
30
43
  export function addRetrievalNodes(
31
44
  provenance: ProvenanceBuilder,
45
+ intents: readonly RetrievalIntent[],
32
46
  retrieved: readonly RetrievedFact[],
33
47
  input: ConversationMessage,
48
+ recalled: readonly RecalledMemory[] = [],
34
49
  ): void {
50
+ const searchIds = intents.map((intent, i) => {
51
+ const id = `memory-read:search:${input.sequence}:${i}`;
52
+ const found = retrieved.filter((r) => sameIntent(r.intent, intent));
53
+ const quoted = recalled.filter((r) => sameIntent(r.intent, intent));
54
+ provenance.addNode({
55
+ id,
56
+ kind: 'memory-read',
57
+ timestamp: input.createdAt,
58
+ attributes: {
59
+ operation: 'search_memory',
60
+ intent: i,
61
+ source: intent.source ?? 'facts',
62
+ ...(intent.types !== undefined && { types: [...intent.types] }),
63
+ scope: intent.scope,
64
+ mode: intent.mode ?? 'list',
65
+ ...(intent.source === 'conversations'
66
+ ? { messages: quoted.map((r) => recallRef(r)) }
67
+ : { factIds: found.map((r) => r.fact.id as unknown as string) }),
68
+ },
69
+ });
70
+ provenance.addEdge({ from: id, to: `input:${input.sequence}`, kind: 'caused-by' });
71
+ return id;
72
+ });
35
73
  for (const r of retrieved) {
36
74
  provenance.addNode({
37
75
  id: `retrieval:${r.fact.id}`,
@@ -43,14 +81,64 @@ export function addRetrievalNodes(
43
81
  factType: r.fact.type,
44
82
  intentScope: r.intent.scope,
45
83
  ...(r.score !== undefined && { score: r.score }),
84
+ ...(r.ranks !== undefined && { ranks: { ...r.ranks } }),
46
85
  },
47
86
  });
87
+ const search = searchIds[intents.findIndex((intent) => sameIntent(r.intent, intent))];
88
+ if (search !== undefined) {
89
+ provenance.addEdge({ from: `retrieval:${r.fact.id}`, to: search, kind: 'retrieved-from' });
90
+ }
48
91
  provenance.addEdge({
49
92
  from: `retrieval:${r.fact.id}`,
50
93
  to: `input:${input.sequence}`,
51
94
  kind: 'influenced-by',
52
95
  });
53
96
  }
97
+ addRecalledNodes(provenance, intents, searchIds, recalled, input);
98
+ }
99
+
100
+ /** A recalled message: its conversation and place in it. */
101
+ function recallRef(r: RecalledMemory): string {
102
+ return `${r.message.conversationId}#${r.message.sequence}`;
103
+ }
104
+
105
+ /**
106
+ * The recalled messages: a `retrieval` node each (source conversations),
107
+ * `retrieved-from` its search and `influenced-by` the input.
108
+ */
109
+ function addRecalledNodes(
110
+ provenance: ProvenanceBuilder,
111
+ intents: readonly RetrievalIntent[],
112
+ searchIds: readonly string[],
113
+ recalled: readonly RecalledMemory[],
114
+ input: ConversationMessage,
115
+ ): void {
116
+ for (const r of recalled) {
117
+ const id = `retrieval:recall:${recallRef(r)}`;
118
+ provenance.addNode({
119
+ id,
120
+ kind: 'retrieval',
121
+ timestamp: input.createdAt,
122
+ attributes: {
123
+ source: 'conversations',
124
+ conversationId: r.message.conversationId,
125
+ sequence: r.message.sequence,
126
+ role: r.message.role,
127
+ intentScope: r.intent.scope,
128
+ ...(r.anotherPerson === true && { anotherPerson: true }),
129
+ ...(r.score !== undefined && { score: r.score }),
130
+ ...(r.ranks !== undefined && { ranks: { ...r.ranks } }),
131
+ },
132
+ });
133
+ const search = searchIds[intents.findIndex((intent) => sameIntent(r.intent, intent))];
134
+ if (search !== undefined) provenance.addEdge({ from: id, to: search, kind: 'retrieved-from' });
135
+ provenance.addEdge({ from: id, to: `input:${input.sequence}`, kind: 'influenced-by' });
136
+ }
137
+ }
138
+
139
+ /** The same intent, whether live or read back from the journal. */
140
+ function sameIntent(a: RetrievalIntent, b: RetrievalIntent): boolean {
141
+ return a === b || JSON.stringify(a) === JSON.stringify(b);
54
142
  }
55
143
 
56
144
  /** One model call: its identity; its usage is the cost ledger's, by `callId`. */
@@ -128,6 +216,45 @@ export function addToolNodes(
128
216
  provenance.addEdge({ from: resultNodeId, to: callNodeId, kind: 'produced' });
129
217
  }
130
218
 
219
+ /**
220
+ * What a `remember` call wrote, from its stored result (so a resumed turn
221
+ * adds the same node): a `memory-write` node, `operation: create_memory`
222
+ * or `update_memory`, the fact's id and version, its actor the agent,
223
+ * `produced` by the call.
224
+ */
225
+ export function addMemoryWriteNode(
226
+ provenance: ProvenanceBuilder,
227
+ result: ConversationMessage,
228
+ agent: { readonly id: string; readonly version: string },
229
+ ): void {
230
+ const call = result.toolCall;
231
+ if (call === undefined || call.toolId !== REMEMBER_TOOL_ID) return;
232
+ const written = rememberedOf(result.content);
233
+ if (written === undefined) return;
234
+ const id = `memory-write:${written.factId}@${written.version}`;
235
+ provenance.addNode({
236
+ id,
237
+ kind: 'memory-write',
238
+ timestamp: result.createdAt,
239
+ actor: `agent:${agent.id}@${agent.version}`,
240
+ attributes: {
241
+ operation: written.outcome === 'superseded' ? 'update_memory' : 'create_memory',
242
+ factId: written.factId,
243
+ version: written.version,
244
+ ...(written.status === 'pending-review' && { review: 'pending' }),
245
+ },
246
+ });
247
+ provenance.addEdge({ from: id, to: `tool-call:${call.invocationId}`, kind: 'produced' });
248
+ }
249
+
250
+ function rememberedOf(content: ConversationMessage['content']): RememberToolOutput | undefined {
251
+ if (typeof content !== 'object' || content === null) return undefined;
252
+ const out = content as Partial<RememberToolOutput>;
253
+ if (out.status !== 'remembered' && out.status !== 'pending-review') return undefined;
254
+ if (typeof out.factId !== 'string' || typeof out.version !== 'number') return undefined;
255
+ return out as RememberToolOutput;
256
+ }
257
+
131
258
  /**
132
259
  * The tool calls of one model step: a node pair for each result the step
133
260
  * stored, and their invocation ids added to the turn's tool results.
@@ -142,6 +269,10 @@ export function addStepToolNodes(
142
269
  const { invocationId, toolId } = result.toolCall;
143
270
  if (ctx.provenance !== undefined) {
144
271
  addToolNodes(ctx.provenance, step, result, versionOf(ctx, toolId));
272
+ addMemoryWriteNode(ctx.provenance, result, {
273
+ id: ctx.input.agent.id as unknown as string,
274
+ version: ctx.input.agent.version as unknown as string,
275
+ });
145
276
  const approval = ctx.toolApprovals?.get(invocationId);
146
277
  if (approval !== undefined) {
147
278
  addToolApprovalNodes(