@kindgi/agents 0.1.4 → 0.1.5-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.
- package/README.md +1 -1
- package/dist/blocks.d.ts +19 -0
- package/dist/blocks.d.ts.map +1 -1
- package/dist/blocks.js +59 -1
- package/dist/blocks.js.map +1 -1
- package/dist/conversation-binding.d.ts +49 -3
- package/dist/conversation-binding.d.ts.map +1 -1
- package/dist/define.d.ts +7 -1
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +132 -7
- package/dist/define.js.map +1 -1
- package/dist/drafted-template.d.ts +34 -0
- package/dist/drafted-template.d.ts.map +1 -0
- package/dist/drafted-template.js +95 -0
- package/dist/drafted-template.js.map +1 -0
- package/dist/guardrails-gate.d.ts +28 -13
- package/dist/guardrails-gate.d.ts.map +1 -1
- package/dist/guardrails-gate.js +59 -21
- package/dist/guardrails-gate.js.map +1 -1
- package/dist/handlers/build-initial-messages.d.ts +8 -2
- package/dist/handlers/build-initial-messages.d.ts.map +1 -1
- package/dist/handlers/build-initial-messages.js +23 -21
- package/dist/handlers/build-initial-messages.js.map +1 -1
- package/dist/handlers/compose-result.d.ts.map +1 -1
- package/dist/handlers/compose-result.js +2 -0
- package/dist/handlers/compose-result.js.map +1 -1
- package/dist/handlers/context.d.ts +6 -1
- package/dist/handlers/context.d.ts.map +1 -1
- package/dist/handlers/dispatch-tools.d.ts.map +1 -1
- package/dist/handlers/dispatch-tools.js +17 -5
- package/dist/handlers/dispatch-tools.js.map +1 -1
- package/dist/handlers/errors.d.ts +14 -1
- package/dist/handlers/errors.d.ts.map +1 -1
- package/dist/handlers/errors.js.map +1 -1
- package/dist/handlers/evaluate-guardrails.d.ts +6 -1
- package/dist/handlers/evaluate-guardrails.d.ts.map +1 -1
- package/dist/handlers/evaluate-guardrails.js +46 -4
- package/dist/handlers/evaluate-guardrails.js.map +1 -1
- package/dist/handlers/history.d.ts +24 -0
- package/dist/handlers/history.d.ts.map +1 -0
- package/dist/handlers/history.js +52 -0
- package/dist/handlers/history.js.map +1 -0
- package/dist/handlers/persist-final-message.d.ts.map +1 -1
- package/dist/handlers/persist-final-message.js +2 -0
- package/dist/handlers/persist-final-message.js.map +1 -1
- package/dist/handlers/persist-user-message.d.ts.map +1 -1
- package/dist/handlers/persist-user-message.js +2 -0
- package/dist/handlers/persist-user-message.js.map +1 -1
- package/dist/handlers/public-types.d.ts +12 -2
- package/dist/handlers/public-types.d.ts.map +1 -1
- package/dist/handlers/rehydrate.d.ts.map +1 -1
- package/dist/handlers/rehydrate.js +7 -4
- package/dist/handlers/rehydrate.js.map +1 -1
- package/dist/handlers/remember-tool.d.ts +20 -0
- package/dist/handlers/remember-tool.d.ts.map +1 -0
- package/dist/handlers/remember-tool.js +149 -0
- package/dist/handlers/remember-tool.js.map +1 -0
- package/dist/handlers/replay.d.ts +55 -1
- package/dist/handlers/replay.d.ts.map +1 -1
- package/dist/handlers/replay.js +23 -6
- package/dist/handlers/replay.js.map +1 -1
- package/dist/handlers/resolve-blocks.d.ts.map +1 -1
- package/dist/handlers/resolve-blocks.js +19 -8
- package/dist/handlers/resolve-blocks.js.map +1 -1
- package/dist/handlers/result-shape.d.ts +3 -1
- package/dist/handlers/result-shape.d.ts.map +1 -1
- package/dist/handlers/result-shape.js.map +1 -1
- package/dist/handlers/run-retrievals.d.ts +2 -1
- package/dist/handlers/run-retrievals.d.ts.map +1 -1
- package/dist/handlers/run-retrievals.js +42 -16
- package/dist/handlers/run-retrievals.js.map +1 -1
- package/dist/handlers/turn-environment.d.ts.map +1 -1
- package/dist/handlers/turn-environment.js +2 -1
- package/dist/handlers/turn-environment.js.map +1 -1
- package/dist/handlers/turn-provenance.d.ts +19 -3
- package/dist/handlers/turn-provenance.d.ts.map +1 -1
- package/dist/handlers/turn-provenance.js +116 -2
- package/dist/handlers/turn-provenance.js.map +1 -1
- package/dist/index.d.ts +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/invoke.d.ts +1 -1
- package/dist/invoke.d.ts.map +1 -1
- package/dist/invoke.js +2 -0
- package/dist/invoke.js.map +1 -1
- package/dist/remember.d.ts +49 -0
- package/dist/remember.d.ts.map +1 -0
- package/dist/remember.js +89 -0
- package/dist/remember.js.map +1 -0
- package/dist/retrieval.d.ts +111 -28
- package/dist/retrieval.d.ts.map +1 -1
- package/dist/retrieval.js +432 -104
- package/dist/retrieval.js.map +1 -1
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +6 -0
- package/dist/schema.js.map +1 -1
- package/dist/streaming.d.ts +16 -1
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js.map +1 -1
- package/dist/types.d.ts +131 -10
- package/dist/types.d.ts.map +1 -1
- package/migrations/0005_condemned_hellcat.sql +1 -0
- package/migrations/meta/0005_snapshot.json +333 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +15 -15
- package/src/blocks.ts +75 -1
- package/src/conversation-binding.ts +53 -3
- package/src/define.ts +144 -9
- package/src/drafted-template.ts +118 -0
- package/src/guardrails-gate.ts +90 -26
- package/src/handlers/build-initial-messages.ts +29 -22
- package/src/handlers/compose-result.ts +2 -0
- package/src/handlers/context.ts +12 -1
- package/src/handlers/dispatch-tools.ts +19 -5
- package/src/handlers/errors.ts +16 -1
- package/src/handlers/evaluate-guardrails.ts +47 -4
- package/src/handlers/history.ts +57 -0
- package/src/handlers/persist-final-message.ts +2 -0
- package/src/handlers/persist-user-message.ts +2 -0
- package/src/handlers/public-types.ts +18 -2
- package/src/handlers/rehydrate.ts +12 -8
- package/src/handlers/remember-tool.ts +200 -0
- package/src/handlers/replay.ts +80 -8
- package/src/handlers/resolve-blocks.ts +21 -7
- package/src/handlers/result-shape.ts +8 -1
- package/src/handlers/run-retrievals.ts +52 -19
- package/src/handlers/turn-environment.ts +2 -1
- package/src/handlers/turn-provenance.ts +133 -2
- package/src/index.ts +33 -2
- package/src/invoke.ts +3 -0
- package/src/remember.ts +135 -0
- package/src/retrieval.ts +587 -125
- package/src/schema.ts +6 -0
- package/src/streaming.ts +17 -0
- package/src/types.ts +129 -10
package/src/handlers/replay.ts
CHANGED
|
@@ -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
|
-
| {
|
|
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,
|
|
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`
|
|
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
|
-
|
|
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')
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 =
|
|
172
|
+
modelSettings = values as ModelSettings;
|
|
159
173
|
} else {
|
|
160
|
-
settings[block.id] =
|
|
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 {
|
|
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
|
|
|
@@ -47,6 +52,8 @@ export interface AgentTurnResult {
|
|
|
47
52
|
readonly appended: readonly ConversationMessage[];
|
|
48
53
|
readonly response: ConversationMessage;
|
|
49
54
|
readonly retrieved: readonly RetrievedFact[];
|
|
55
|
+
/** Messages of earlier conversations the turn recalled (intents over conversations). */
|
|
56
|
+
readonly recalled?: readonly RecalledMemory[];
|
|
50
57
|
readonly violations: readonly EvaluationResult[];
|
|
51
58
|
readonly usage: AgentTurnUsage;
|
|
52
59
|
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 {
|
|
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
|
|
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(
|
|
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
|
|
43
|
-
// (`rehydrateTurnContext`) rather than retrieving again.
|
|
44
|
-
return {
|
|
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
|
-
/**
|
|
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<
|
|
71
|
+
): Promise<RetrievalPass | undefined> {
|
|
53
72
|
const replay = ctx.input.replay;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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<
|
|
63
|
-
if (ctx.conversation === undefined) return [];
|
|
64
|
-
const
|
|
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
|
|
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
|
-
/**
|
|
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(
|
package/src/index.ts
CHANGED
|
@@ -16,11 +16,15 @@ export type {
|
|
|
16
16
|
RunSnapshotRecord,
|
|
17
17
|
RunSnapshotWriteInput,
|
|
18
18
|
} from './run-snapshot-binding.js';
|
|
19
|
+
export { DRAFTED_TEMPLATE_MAX, checkDraftedTemplate } from './drafted-template.js';
|
|
20
|
+
export type { DraftedTemplateContext } from './drafted-template.js';
|
|
19
21
|
export { defineAgent } from './define.js';
|
|
20
22
|
export {
|
|
21
23
|
BLOCK_KINDS,
|
|
22
24
|
MODEL_SETTINGS_SCHEMA,
|
|
25
|
+
TUNABLE_MARKER,
|
|
23
26
|
settingsSchemaIssues,
|
|
27
|
+
tunableKeys,
|
|
24
28
|
validateBlock,
|
|
25
29
|
} from './blocks.js';
|
|
26
30
|
export type {
|
|
@@ -32,6 +36,7 @@ export type {
|
|
|
32
36
|
ModelSettings,
|
|
33
37
|
PromptBlockContent,
|
|
34
38
|
SettingsBlockContent,
|
|
39
|
+
TunableKey,
|
|
35
40
|
} from './blocks.js';
|
|
36
41
|
export type { DefineAgentSpec } from './define.js';
|
|
37
42
|
export { resolveEffectiveHitlPolicy } from './hitl-policy.js';
|
|
@@ -45,11 +50,12 @@ export type { GateDecision, GateDecisionValue } from './handlers/gate-decision.j
|
|
|
45
50
|
export type { EffectiveHitlPolicy } from './hitl-policy.js';
|
|
46
51
|
export { agentStepOutput, invokeAgent, resumeAgentTurn } from './invoke.js';
|
|
47
52
|
export { parseFailureMessage, turnFailureMessage } from './handlers/errors.js';
|
|
48
|
-
export { isReadOnlyTool } from './handlers/replay.js';
|
|
53
|
+
export { isComputeOnlyTool, isReadOnlyTool } from './handlers/replay.js';
|
|
49
54
|
export { SESSION_GATE_RECORD } from './handlers/setup.js';
|
|
50
55
|
export type {
|
|
51
56
|
ReplayApproval,
|
|
52
57
|
ReplayBinding,
|
|
58
|
+
ReplayOverrides,
|
|
53
59
|
ReplayToolDecision,
|
|
54
60
|
ReplayToolInput,
|
|
55
61
|
ReplayToolTrace,
|
|
@@ -71,6 +77,7 @@ export type {
|
|
|
71
77
|
OutputSchemaViolationError,
|
|
72
78
|
ResumeAgentTurnInput,
|
|
73
79
|
RunSnapshotError,
|
|
80
|
+
SemanticUnavailableError,
|
|
74
81
|
ToolInvocationError,
|
|
75
82
|
UnresolvedToolError,
|
|
76
83
|
} from './invoke.js';
|
|
@@ -92,6 +99,7 @@ export { persistProvenance } from './provenance-emit.js';
|
|
|
92
99
|
export type { ProvenanceBindings } from './provenance-emit.js';
|
|
93
100
|
export type {
|
|
94
101
|
AgentMessageEvent,
|
|
102
|
+
GuardrailErrorEvent,
|
|
95
103
|
GuardrailViolatedEvent,
|
|
96
104
|
ModelCallCompletedEvent,
|
|
97
105
|
ModelCallStartedEvent,
|
|
@@ -106,7 +114,26 @@ export type {
|
|
|
106
114
|
TurnStartedEvent,
|
|
107
115
|
} from './streaming.js';
|
|
108
116
|
export { AUTO_INJECTED_VARS, renderInstructions } from './prompt.js';
|
|
109
|
-
export {
|
|
117
|
+
export {
|
|
118
|
+
EARLIER_ANSWER_NOTE,
|
|
119
|
+
MEMORY_DATA_RULE,
|
|
120
|
+
RECALL_DEFAULT_ROLES,
|
|
121
|
+
formatPoliciesForPrompt,
|
|
122
|
+
formatRetrievedForPrompt,
|
|
123
|
+
isPolicyFact,
|
|
124
|
+
retrieveForTurn,
|
|
125
|
+
runMemoryReaders,
|
|
126
|
+
runRetrievals,
|
|
127
|
+
} from './retrieval.js';
|
|
128
|
+
export type { DegradedIntent, RetrievalPass, RetrievalRun } from './retrieval.js';
|
|
129
|
+
export {
|
|
130
|
+
DEFAULT_REMEMBER_DAYS,
|
|
131
|
+
MAX_REMEMBER_DAYS,
|
|
132
|
+
REMEMBER_TOOL_ID,
|
|
133
|
+
REMEMBER_TOOL_VERSION,
|
|
134
|
+
looksLikeInstruction,
|
|
135
|
+
} from './remember.js';
|
|
136
|
+
export type { RememberToolOutput } from './handlers/remember-tool.js';
|
|
110
137
|
export type { RetrievalBindings } from './retrieval.js';
|
|
111
138
|
export type {
|
|
112
139
|
MissingParameterError,
|
|
@@ -140,6 +167,7 @@ export type {
|
|
|
140
167
|
Agent,
|
|
141
168
|
AgentBindings,
|
|
142
169
|
AgentId,
|
|
170
|
+
AgentMemoryPolicy,
|
|
143
171
|
AgentOutputSpec,
|
|
144
172
|
BlockRef,
|
|
145
173
|
Conversation,
|
|
@@ -149,6 +177,9 @@ export type {
|
|
|
149
177
|
MessageRole,
|
|
150
178
|
PromptParameter,
|
|
151
179
|
PromptRef,
|
|
180
|
+
RecalledMemory,
|
|
181
|
+
RememberPolicy,
|
|
182
|
+
RememberScope,
|
|
152
183
|
RetrievalIntent,
|
|
153
184
|
RetrievedFact,
|
|
154
185
|
ToolRef,
|