@kindgi/agents 0.1.2 → 0.1.4-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocks.d.ts +101 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +149 -0
- package/dist/blocks.js.map +1 -0
- package/dist/conversation-binding.d.ts +9 -1
- package/dist/conversation-binding.d.ts.map +1 -1
- package/dist/define.d.ts +17 -2
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +93 -6
- package/dist/define.js.map +1 -1
- package/dist/guardrails-gate.d.ts +7 -4
- package/dist/guardrails-gate.d.ts.map +1 -1
- package/dist/guardrails-gate.js +5 -2
- package/dist/guardrails-gate.js.map +1 -1
- package/dist/handlers/compose-result.d.ts.map +1 -1
- package/dist/handlers/compose-result.js +3 -0
- package/dist/handlers/compose-result.js.map +1 -1
- package/dist/handlers/context.d.ts +24 -0
- 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 +69 -43
- package/dist/handlers/dispatch-tools.js.map +1 -1
- package/dist/handlers/errors.d.ts +12 -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.map +1 -1
- package/dist/handlers/evaluate-guardrails.js +36 -1
- package/dist/handlers/evaluate-guardrails.js.map +1 -1
- package/dist/handlers/gate-decision.d.ts +62 -0
- package/dist/handlers/gate-decision.d.ts.map +1 -0
- package/dist/handlers/gate-decision.js +55 -0
- package/dist/handlers/gate-decision.js.map +1 -0
- package/dist/handlers/model-call.d.ts +10 -0
- package/dist/handlers/model-call.d.ts.map +1 -1
- package/dist/handlers/model-call.js +120 -34
- package/dist/handlers/model-call.js.map +1 -1
- package/dist/handlers/persist-provenance.d.ts.map +1 -1
- package/dist/handlers/persist-provenance.js +3 -1
- package/dist/handlers/persist-provenance.js.map +1 -1
- package/dist/handlers/persist-user-message.d.ts.map +1 -1
- package/dist/handlers/persist-user-message.js +5 -16
- package/dist/handlers/persist-user-message.js.map +1 -1
- package/dist/handlers/public-types.d.ts +37 -3
- package/dist/handlers/public-types.d.ts.map +1 -1
- package/dist/handlers/rehydrate.d.ts.map +1 -1
- package/dist/handlers/rehydrate.js +91 -1
- package/dist/handlers/rehydrate.js.map +1 -1
- package/dist/handlers/render-prompt.d.ts.map +1 -1
- package/dist/handlers/render-prompt.js +4 -1
- package/dist/handlers/render-prompt.js.map +1 -1
- package/dist/handlers/replay.d.ts +143 -0
- package/dist/handlers/replay.d.ts.map +1 -0
- package/dist/handlers/replay.js +177 -0
- package/dist/handlers/replay.js.map +1 -0
- package/dist/handlers/resolve-blocks.d.ts +32 -0
- package/dist/handlers/resolve-blocks.d.ts.map +1 -0
- package/dist/handlers/resolve-blocks.js +129 -0
- package/dist/handlers/resolve-blocks.js.map +1 -0
- package/dist/handlers/resolve-tools.d.ts +6 -2
- package/dist/handlers/resolve-tools.d.ts.map +1 -1
- package/dist/handlers/resolve-tools.js +59 -27
- package/dist/handlers/resolve-tools.js.map +1 -1
- package/dist/handlers/result-shape.d.ts +7 -0
- 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.map +1 -1
- package/dist/handlers/run-retrievals.js +35 -35
- package/dist/handlers/run-retrievals.js.map +1 -1
- package/dist/handlers/run-snapshot.d.ts.map +1 -1
- package/dist/handlers/run-snapshot.js +1 -0
- package/dist/handlers/run-snapshot.js.map +1 -1
- package/dist/handlers/setup.d.ts +2 -0
- package/dist/handlers/setup.d.ts.map +1 -1
- package/dist/handlers/setup.js +29 -12
- package/dist/handlers/setup.js.map +1 -1
- package/dist/handlers/tool-hitl.d.ts +5 -9
- package/dist/handlers/tool-hitl.d.ts.map +1 -1
- package/dist/handlers/tool-hitl.js +5 -0
- package/dist/handlers/tool-hitl.js.map +1 -1
- package/dist/handlers/turn-environment.d.ts +13 -2
- package/dist/handlers/turn-environment.d.ts.map +1 -1
- package/dist/handlers/turn-environment.js +12 -3
- package/dist/handlers/turn-environment.js.map +1 -1
- package/dist/handlers/turn-provenance.d.ts +67 -0
- package/dist/handlers/turn-provenance.d.ts.map +1 -0
- package/dist/handlers/turn-provenance.js +160 -0
- package/dist/handlers/turn-provenance.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/invoke.d.ts +14 -1
- package/dist/invoke.d.ts.map +1 -1
- package/dist/invoke.js +37 -19
- package/dist/invoke.js.map +1 -1
- package/dist/pins.d.ts +54 -0
- package/dist/pins.d.ts.map +1 -0
- package/dist/pins.js +51 -0
- package/dist/pins.js.map +1 -0
- package/dist/prompt.d.ts +12 -2
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +41 -4
- package/dist/prompt.js.map +1 -1
- package/dist/provenance-emit.d.ts +4 -2
- package/dist/provenance-emit.d.ts.map +1 -1
- package/dist/provenance-emit.js +4 -2
- package/dist/provenance-emit.js.map +1 -1
- package/dist/run-snapshot-binding.d.ts +5 -0
- package/dist/run-snapshot-binding.d.ts.map +1 -1
- package/dist/schema.d.ts +34 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +16 -0
- package/dist/schema.js.map +1 -1
- package/dist/streaming.d.ts +5 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js.map +1 -1
- package/dist/types.d.ts +65 -2
- package/dist/types.d.ts.map +1 -1
- package/migrations/0003_hesitant_captain_cross.sql +2 -0
- package/migrations/0004_stormy_moondragon.sql +1 -0
- package/migrations/meta/0003_snapshot.json +321 -0
- package/migrations/meta/0004_snapshot.json +327 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +15 -15
- package/src/blocks.ts +231 -0
- package/src/conversation-binding.ts +9 -1
- package/src/define.ts +104 -7
- package/src/guardrails-gate.ts +17 -3
- package/src/handlers/compose-result.ts +3 -0
- package/src/handlers/context.ts +24 -0
- package/src/handlers/dispatch-tools.ts +75 -44
- package/src/handlers/errors.ts +13 -0
- package/src/handlers/evaluate-guardrails.ts +41 -1
- package/src/handlers/gate-decision.ts +101 -0
- package/src/handlers/model-call.ts +163 -33
- package/src/handlers/persist-provenance.ts +3 -1
- package/src/handlers/persist-user-message.ts +3 -16
- package/src/handlers/public-types.ts +38 -3
- package/src/handlers/rehydrate.ts +141 -9
- package/src/handlers/render-prompt.ts +13 -7
- package/src/handlers/replay.ts +314 -0
- package/src/handlers/resolve-blocks.ts +188 -0
- package/src/handlers/resolve-tools.ts +76 -28
- package/src/handlers/result-shape.ts +8 -0
- package/src/handlers/run-retrievals.ts +45 -41
- package/src/handlers/run-snapshot.ts +1 -0
- package/src/handlers/setup.ts +30 -22
- package/src/handlers/tool-hitl.ts +6 -10
- package/src/handlers/turn-environment.ts +28 -3
- package/src/handlers/turn-provenance.ts +230 -0
- package/src/index.ts +44 -0
- package/src/invoke.ts +49 -20
- package/src/pins.ts +98 -0
- package/src/prompt.ts +52 -4
- package/src/provenance-emit.ts +4 -1
- package/src/run-snapshot-binding.ts +6 -0
- package/src/schema.ts +16 -0
- package/src/streaming.ts +5 -0
- package/src/types.ts +68 -2
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import { pickVersion } from '@kindgi/tools';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type BlockDefinition,
|
|
8
|
+
MODEL_SETTINGS_SCHEMA,
|
|
9
|
+
type ModelSettings,
|
|
10
|
+
type PromptBlockContent,
|
|
11
|
+
settingsSchemaIssues,
|
|
12
|
+
} from '../blocks.js';
|
|
13
|
+
|
|
14
|
+
import type { TurnContext } from './context.js';
|
|
15
|
+
import { throwAgentTurnFailure } from './errors.js';
|
|
16
|
+
|
|
17
|
+
/** The block versions a turn runs, by kind then id, as `setup` journals them. */
|
|
18
|
+
export interface PinnedBlockVersions {
|
|
19
|
+
readonly prompts: Readonly<Record<string, string>>;
|
|
20
|
+
readonly settings: Readonly<Record<string, string>>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The data blocks a turn runs with, loaded at its version. */
|
|
24
|
+
export interface TurnBlocks {
|
|
25
|
+
/** The prompt block the instructions come from. */
|
|
26
|
+
readonly prompt?: {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly version: string;
|
|
29
|
+
readonly content: PromptBlockContent;
|
|
30
|
+
};
|
|
31
|
+
/** Each settings block's values, by block id (`ToolContext.settings`, `settings` in templates). */
|
|
32
|
+
readonly settings: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
33
|
+
/** The model-settings block's values, for the turn's model calls. */
|
|
34
|
+
readonly modelSettings?: ModelSettings;
|
|
35
|
+
readonly versions: PinnedBlockVersions;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Ref {
|
|
39
|
+
readonly kind: 'prompts' | 'settings';
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly range: string;
|
|
42
|
+
readonly role: 'prompt' | 'settings' | 'model-settings';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load the data blocks the agent references, each at one exact version:
|
|
47
|
+
* the resumed turn's own (`pinned`, from its `setup`), else the agent
|
|
48
|
+
* version's pin (`agent.pins`), else its range resolved now by
|
|
49
|
+
* `pickVersion` (an agent version published before pins). A pinned
|
|
50
|
+
* version that's unregistered still loads; one that's gone, a range
|
|
51
|
+
* nothing satisfies, or a runtime with no blocks fails the turn
|
|
52
|
+
* (`block-unresolvable`). `undefined` when the agent references none.
|
|
53
|
+
*/
|
|
54
|
+
export async function resolveTurnBlocks(
|
|
55
|
+
ctx: TurnContext,
|
|
56
|
+
pinned?: PinnedBlockVersions,
|
|
57
|
+
): Promise<TurnBlocks | undefined> {
|
|
58
|
+
const agent = ctx.input.agent;
|
|
59
|
+
const refs = blockRefs(ctx);
|
|
60
|
+
if (refs.length === 0) return undefined;
|
|
61
|
+
const reader = ctx.bindings.blockReader;
|
|
62
|
+
if (reader === undefined) {
|
|
63
|
+
fail(
|
|
64
|
+
refs[0] as Ref,
|
|
65
|
+
`agent "${agent.id}" references data blocks, but this runtime serves none`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const versions = {
|
|
70
|
+
prompts: {} as Record<string, string>,
|
|
71
|
+
settings: {} as Record<string, string>,
|
|
72
|
+
};
|
|
73
|
+
const loaded = new Map<Ref, BlockDefinition>();
|
|
74
|
+
for (const ref of refs) {
|
|
75
|
+
const version =
|
|
76
|
+
pinned?.[ref.kind][ref.id] ??
|
|
77
|
+
agent.pins?.[ref.kind][ref.id] ??
|
|
78
|
+
(await resolveRange(ctx, ref));
|
|
79
|
+
const block = await reader.getVersion({
|
|
80
|
+
tenantId: ctx.input.tenantId,
|
|
81
|
+
blockId: ref.id,
|
|
82
|
+
version,
|
|
83
|
+
});
|
|
84
|
+
if (block === null) {
|
|
85
|
+
fail(ref, `${describe(ref)} runs version ${version}, which isn't published`);
|
|
86
|
+
}
|
|
87
|
+
if (block.kind !== (ref.role === 'prompt' ? 'prompt' : 'settings')) {
|
|
88
|
+
fail(ref, `${describe(ref)} is a ${block.kind} block`);
|
|
89
|
+
}
|
|
90
|
+
versions[ref.kind][ref.id] = version;
|
|
91
|
+
loaded.set(ref, block);
|
|
92
|
+
}
|
|
93
|
+
return turnBlocks(refs, loaded, versions);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function blockRefs(ctx: TurnContext): Ref[] {
|
|
97
|
+
const agent = ctx.input.agent;
|
|
98
|
+
const refs: Ref[] = [];
|
|
99
|
+
if (typeof agent.instructions === 'object') {
|
|
100
|
+
refs.push({
|
|
101
|
+
kind: 'prompts',
|
|
102
|
+
id: agent.instructions.prompt,
|
|
103
|
+
range: agent.instructions.version,
|
|
104
|
+
role: 'prompt',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
for (const s of agent.settings ?? []) {
|
|
108
|
+
refs.push({ kind: 'settings', id: s.id, range: s.version, role: 'settings' });
|
|
109
|
+
}
|
|
110
|
+
if (agent.modelSettings !== undefined) {
|
|
111
|
+
refs.push({
|
|
112
|
+
kind: 'settings',
|
|
113
|
+
id: agent.modelSettings.id,
|
|
114
|
+
range: agent.modelSettings.version,
|
|
115
|
+
role: 'model-settings',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return refs;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** An unpinned reference's range, resolved over the block's active versions. */
|
|
122
|
+
async function resolveRange(ctx: TurnContext, ref: Ref): Promise<string> {
|
|
123
|
+
const available =
|
|
124
|
+
(await ctx.bindings.blockReader?.activeVersions({
|
|
125
|
+
tenantId: ctx.input.tenantId,
|
|
126
|
+
blockId: ref.id,
|
|
127
|
+
})) ?? [];
|
|
128
|
+
const pick = pickVersion(available, ref.range);
|
|
129
|
+
if (pick.kind === 'ok') return pick.version;
|
|
130
|
+
return fail(
|
|
131
|
+
ref,
|
|
132
|
+
available.length === 0
|
|
133
|
+
? `${describe(ref)} has no published version`
|
|
134
|
+
: `${describe(ref)} has no published version in "${ref.range}" (published: ${available.join(', ')})`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function turnBlocks(
|
|
139
|
+
refs: readonly Ref[],
|
|
140
|
+
loaded: ReadonlyMap<Ref, BlockDefinition>,
|
|
141
|
+
versions: PinnedBlockVersions,
|
|
142
|
+
): TurnBlocks {
|
|
143
|
+
const settings: Record<string, Readonly<Record<string, unknown>>> = {};
|
|
144
|
+
let prompt: TurnBlocks['prompt'];
|
|
145
|
+
let modelSettings: ModelSettings | undefined;
|
|
146
|
+
for (const ref of refs) {
|
|
147
|
+
const block = loaded.get(ref) as BlockDefinition;
|
|
148
|
+
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);
|
|
152
|
+
if (issues.length > 0) {
|
|
153
|
+
fail(
|
|
154
|
+
ref,
|
|
155
|
+
`${describe(ref)} version ${block.version} isn't model settings: ${issues.map((i) => `${i.path} ${i.message}`).join('; ')}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
modelSettings = block.content.values as ModelSettings;
|
|
159
|
+
} else {
|
|
160
|
+
settings[block.id] = block.content.values;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
...(prompt !== undefined && { prompt }),
|
|
165
|
+
settings,
|
|
166
|
+
...(modelSettings !== undefined && { modelSettings }),
|
|
167
|
+
versions,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function describe(ref: Ref): string {
|
|
172
|
+
const what =
|
|
173
|
+
ref.role === 'prompt'
|
|
174
|
+
? 'prompt'
|
|
175
|
+
: ref.role === 'model-settings'
|
|
176
|
+
? 'model-settings'
|
|
177
|
+
: 'settings';
|
|
178
|
+
return `${what} block "${ref.id}"`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fail(ref: Ref, message: string): never {
|
|
182
|
+
return throwAgentTurnFailure({
|
|
183
|
+
code: 'block-unresolvable',
|
|
184
|
+
message,
|
|
185
|
+
blockId: ref.id,
|
|
186
|
+
requestedRange: ref.range,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
4
|
import type { ModelToolDefinition } from '@kindgi/capabilities';
|
|
5
|
-
import type { Tool, ToolRegistry } from '@kindgi/tools';
|
|
5
|
+
import type { Tool, ToolError, ToolRegistry, ToolResolution } from '@kindgi/tools';
|
|
6
|
+
import type { Result, ToolId } from '@kindgi/types';
|
|
6
7
|
|
|
7
|
-
import type { Agent } from '../types.js';
|
|
8
|
+
import type { Agent, ToolRef } from '../types.js';
|
|
8
9
|
|
|
9
10
|
import type { TurnContext } from './context.js';
|
|
10
11
|
import { throwAgentTurnFailure } from './errors.js';
|
|
@@ -12,11 +13,16 @@ import { throwAgentTurnFailure } from './errors.js';
|
|
|
12
13
|
/**
|
|
13
14
|
* Resolve the agent's tool references against one tenant's registry
|
|
14
15
|
* (`ToolRegistry.forTenant`). Throws the turn failure for an unknown
|
|
15
|
-
* tool or an unsatisfiable version range.
|
|
16
|
+
* tool or an unsatisfiable version range. A tool in `pinned` (a resumed
|
|
17
|
+
* turn's, from its `setup`) resolves to exactly that version, and so,
|
|
18
|
+
* otherwise, does one in the agent version's own pins (`agent.pins`, set
|
|
19
|
+
* when it was published): one that's gone fails the turn rather than
|
|
20
|
+
* running another. Without either, the tool's range resolves.
|
|
16
21
|
*/
|
|
17
22
|
export function resolveTurnTools(
|
|
18
23
|
registry: ToolRegistry,
|
|
19
24
|
agent: Agent,
|
|
25
|
+
pinned?: Readonly<Record<string, string>>,
|
|
20
26
|
): NonNullable<TurnContext['tools']> {
|
|
21
27
|
const definitions: ModelToolDefinition[] = [];
|
|
22
28
|
const byName = new Map<
|
|
@@ -29,32 +35,16 @@ export function resolveTurnTools(
|
|
|
29
35
|
// (npm-compatible semver, backed by `maxSatisfying`); capture `resolvedVersion` in
|
|
30
36
|
// the byName map so `dispatch-tools` can emit it in provenance +
|
|
31
37
|
// telemetry — a replay can then pin against the same version.
|
|
32
|
-
const
|
|
38
|
+
const turnPin = pinned?.[ref.id];
|
|
39
|
+
const pin = turnPin ?? agent.pins?.tools[ref.id];
|
|
40
|
+
// A pin names its exact version, which a retired (unregistered) one
|
|
41
|
+
// still serves; a range picks among the active versions only.
|
|
42
|
+
const resolved =
|
|
43
|
+
pin !== undefined
|
|
44
|
+
? exactVersion(registry, ref.id, pin)
|
|
45
|
+
: registry.resolve(ref.id as never, ref.version);
|
|
33
46
|
if (resolved.kind === 'err') {
|
|
34
|
-
|
|
35
|
-
if (err.code === 'tool-not-found') {
|
|
36
|
-
throwAgentTurnFailure({
|
|
37
|
-
code: 'unresolved-tool',
|
|
38
|
-
message: `Tool "${ref.id}" declared by agent "${agent.id}" is not registered`,
|
|
39
|
-
toolId: ref.id,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
if (err.code === 'invalid-version-range' || err.code === 'tool-version-unresolvable') {
|
|
43
|
-
throwAgentTurnFailure({
|
|
44
|
-
code: 'tool-version-unresolvable',
|
|
45
|
-
message: err.message,
|
|
46
|
-
toolId: ref.id,
|
|
47
|
-
requestedRange: ref.version,
|
|
48
|
-
...(err.code === 'tool-version-unresolvable' && {
|
|
49
|
-
availableVersions: err.availableVersions,
|
|
50
|
-
}),
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
throwAgentTurnFailure({
|
|
54
|
-
code: 'unresolved-tool',
|
|
55
|
-
message: err.message,
|
|
56
|
-
toolId: ref.id,
|
|
57
|
-
});
|
|
47
|
+
throwUnresolved(agent, ref, resolved.error, pin, turnPin !== undefined ? 'turn' : 'agent');
|
|
58
48
|
}
|
|
59
49
|
const { tool, resolvedVersion } = resolved.value;
|
|
60
50
|
definitions.push({
|
|
@@ -66,3 +56,61 @@ export function resolveTurnTools(
|
|
|
66
56
|
}
|
|
67
57
|
return { definitions, byName };
|
|
68
58
|
}
|
|
59
|
+
|
|
60
|
+
/** A pinned tool version, retired or not, as a resolution. */
|
|
61
|
+
function exactVersion(
|
|
62
|
+
registry: ToolRegistry,
|
|
63
|
+
id: string,
|
|
64
|
+
version: string,
|
|
65
|
+
): Result<ToolResolution, ToolError> {
|
|
66
|
+
const found = registry.getVersion(id as ToolId, version);
|
|
67
|
+
return found.kind === 'ok'
|
|
68
|
+
? { kind: 'ok', value: { tool: found.value, resolvedVersion: version } }
|
|
69
|
+
: found;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fail the turn for a tool that didn't resolve. A pinned version that's
|
|
74
|
+
* gone names whose pin it was: the turn's own (it started with that
|
|
75
|
+
* version) or the agent version's (it was published with it).
|
|
76
|
+
*/
|
|
77
|
+
function throwUnresolved(
|
|
78
|
+
agent: Agent,
|
|
79
|
+
ref: ToolRef,
|
|
80
|
+
err: ToolError,
|
|
81
|
+
pin: string | undefined,
|
|
82
|
+
pinnedBy: 'turn' | 'agent',
|
|
83
|
+
): never {
|
|
84
|
+
const available = err.code === 'tool-version-unresolvable' && {
|
|
85
|
+
availableVersions: err.availableVersions,
|
|
86
|
+
};
|
|
87
|
+
if (pin !== undefined) {
|
|
88
|
+
throwAgentTurnFailure({
|
|
89
|
+
code: 'tool-version-unresolvable',
|
|
90
|
+
message:
|
|
91
|
+
pinnedBy === 'turn'
|
|
92
|
+
? `Tool "${ref.id}": this turn started with version ${pin}, which is no longer registered; it doesn't run another version mid-turn. ${err.message}`
|
|
93
|
+
: `Tool "${ref.id}": agent "${agent.id}" version ${agent.version} runs version ${pin} (its pins), which isn't registered; it doesn't run another version. ${err.message}`,
|
|
94
|
+
toolId: ref.id,
|
|
95
|
+
requestedRange: ref.version,
|
|
96
|
+
...available,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (err.code === 'tool-not-found') {
|
|
100
|
+
throwAgentTurnFailure({
|
|
101
|
+
code: 'unresolved-tool',
|
|
102
|
+
message: `Tool "${ref.id}" declared by agent "${agent.id}" is not registered`,
|
|
103
|
+
toolId: ref.id,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (err.code === 'invalid-version-range' || err.code === 'tool-version-unresolvable') {
|
|
107
|
+
throwAgentTurnFailure({
|
|
108
|
+
code: 'tool-version-unresolvable',
|
|
109
|
+
message: err.message,
|
|
110
|
+
toolId: ref.id,
|
|
111
|
+
requestedRange: ref.version,
|
|
112
|
+
...available,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
throwAgentTurnFailure({ code: 'unresolved-tool', message: err.message, toolId: ref.id });
|
|
116
|
+
}
|
|
@@ -7,6 +7,8 @@ import type { RunId } from '@kindgi/types';
|
|
|
7
7
|
|
|
8
8
|
import type { ConversationId, ConversationMessage, RetrievedFact } from '../types.js';
|
|
9
9
|
|
|
10
|
+
import type { ReplayTurnReport } from './replay.js';
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* Public shape returned by `invokeAgent` on success. Extracted from
|
|
12
14
|
* `invoke.ts` into this module so handler code can import the type
|
|
@@ -70,6 +72,12 @@ export interface AgentTurnResult {
|
|
|
70
72
|
* `response.content`.
|
|
71
73
|
*/
|
|
72
74
|
readonly status?: 'completed' | 'suspended';
|
|
75
|
+
/**
|
|
76
|
+
* A replay turn's report (`InvokeAgentInput.replay`): each tool call and
|
|
77
|
+
* whether it ran, used the past run's result, or was refused (what the
|
|
78
|
+
* turn would have done), and how the session approval gate went.
|
|
79
|
+
*/
|
|
80
|
+
readonly replay?: ReplayTurnReport;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
/**
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
|
-
import type { NodeHandler } from '@kindgi/handler';
|
|
4
|
+
import type { NodeContext, NodeHandler } from '@kindgi/handler';
|
|
5
5
|
|
|
6
6
|
import { runRetrievals } from '../retrieval.js';
|
|
7
7
|
import { emitTurnEvent } from '../streaming.js';
|
|
8
|
+
import type { RetrievedFact } from '../types.js';
|
|
8
9
|
|
|
9
10
|
import type { TurnContext } from './context.js';
|
|
10
11
|
import { throwAgentTurnFailure } from './errors.js';
|
|
12
|
+
import { addRetrievalNodes } from './turn-provenance.js';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Execute the agent's declared retrieval intents. Populates
|
|
@@ -16,7 +18,7 @@ import { throwAgentTurnFailure } from './errors.js';
|
|
|
16
18
|
* is wired.
|
|
17
19
|
*/
|
|
18
20
|
export function buildRunRetrievalsHandler(ctx: TurnContext): NodeHandler {
|
|
19
|
-
return async () => {
|
|
21
|
+
return async (_input, kctx) => {
|
|
20
22
|
if (ctx.conversation === undefined || ctx.userMessage === undefined) {
|
|
21
23
|
throwAgentTurnFailure({
|
|
22
24
|
code: 'model-invocation-failed',
|
|
@@ -24,54 +26,56 @@ export function buildRunRetrievalsHandler(ctx: TurnContext): NodeHandler {
|
|
|
24
26
|
cause: null,
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
ctx.conversation,
|
|
30
|
-
ctx.input.conversationId,
|
|
31
|
-
ctx.input.userMessage,
|
|
32
|
-
{
|
|
33
|
-
memory: ctx.bindings.memoryBinding,
|
|
34
|
-
...(ctx.bindings.embeddingRegistry !== undefined && {
|
|
35
|
-
embeddingRegistry: ctx.bindings.embeddingRegistry,
|
|
36
|
-
}),
|
|
37
|
-
...(ctx.bindings.embeddingModel !== undefined && {
|
|
38
|
-
embeddingModel: ctx.bindings.embeddingModel,
|
|
39
|
-
}),
|
|
40
|
-
},
|
|
41
|
-
);
|
|
42
|
-
if (retrieved.kind === 'err') throwAgentTurnFailure(retrieved.error);
|
|
43
|
-
ctx.retrieved = retrieved.value;
|
|
29
|
+
const facts = (await recordedRetrievals(ctx, kctx)) ?? (await retrieveLive(ctx));
|
|
30
|
+
ctx.retrieved = facts;
|
|
44
31
|
|
|
45
32
|
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
46
33
|
kind: 'retrieval.completed',
|
|
47
|
-
count:
|
|
48
|
-
factIds:
|
|
34
|
+
count: facts.length,
|
|
35
|
+
factIds: facts.map((r) => r.fact.id),
|
|
49
36
|
});
|
|
50
37
|
|
|
51
38
|
if (ctx.provenance !== undefined && ctx.userMessage !== undefined) {
|
|
52
|
-
|
|
53
|
-
ctx.provenance.addNode({
|
|
54
|
-
id: `retrieval:${r.fact.id}`,
|
|
55
|
-
kind: 'retrieval',
|
|
56
|
-
timestamp: ctx.userMessage.createdAt,
|
|
57
|
-
...(r.fact.contentHash !== undefined && { contentHash: r.fact.contentHash }),
|
|
58
|
-
attributes: {
|
|
59
|
-
factId: r.fact.id,
|
|
60
|
-
factType: r.fact.type,
|
|
61
|
-
intentScope: r.intent.scope,
|
|
62
|
-
...(r.score !== undefined && { score: r.score }),
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
ctx.provenance.addEdge({
|
|
66
|
-
from: `retrieval:${r.fact.id}`,
|
|
67
|
-
to: `input:${ctx.userMessage.sequence}`,
|
|
68
|
-
kind: 'influenced-by',
|
|
69
|
-
});
|
|
70
|
-
}
|
|
39
|
+
addRetrievalNodes(ctx.provenance, facts, ctx.userMessage);
|
|
71
40
|
}
|
|
72
41
|
|
|
73
42
|
// The facts go in the journal: a resumed turn restores them from it
|
|
74
43
|
// (`rehydrateTurnContext`) rather than retrieving again.
|
|
75
|
-
return { count:
|
|
44
|
+
return { count: facts.length, retrieved: facts };
|
|
76
45
|
};
|
|
77
46
|
}
|
|
47
|
+
|
|
48
|
+
/** A replay's retrievals: what the past run retrieved, when the replay binding has it. */
|
|
49
|
+
async function recordedRetrievals(
|
|
50
|
+
ctx: TurnContext,
|
|
51
|
+
kctx: NodeContext,
|
|
52
|
+
): Promise<readonly RetrievedFact[] | undefined> {
|
|
53
|
+
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
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function retrieveLive(ctx: TurnContext): Promise<readonly RetrievedFact[]> {
|
|
63
|
+
if (ctx.conversation === undefined) return [];
|
|
64
|
+
const retrieved = await runRetrievals(
|
|
65
|
+
ctx.input.agent,
|
|
66
|
+
ctx.conversation,
|
|
67
|
+
ctx.input.conversationId,
|
|
68
|
+
ctx.input.userMessage,
|
|
69
|
+
{
|
|
70
|
+
memory: ctx.bindings.memoryBinding,
|
|
71
|
+
...(ctx.bindings.embeddingRegistry !== undefined && {
|
|
72
|
+
embeddingRegistry: ctx.bindings.embeddingRegistry,
|
|
73
|
+
}),
|
|
74
|
+
...(ctx.bindings.embeddingModel !== undefined && {
|
|
75
|
+
embeddingModel: ctx.bindings.embeddingModel,
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
if (retrieved.kind === 'err') throwAgentTurnFailure(retrieved.error);
|
|
80
|
+
return retrieved.value;
|
|
81
|
+
}
|
|
@@ -40,5 +40,6 @@ export async function writeRunSnapshot(ctx: TurnContext, kctx: NodeContext): Pro
|
|
|
40
40
|
...(ctx.input.dryRun === true && { dryRun: true }),
|
|
41
41
|
...(ctx.input.principal !== undefined && { principal: ctx.input.principal }),
|
|
42
42
|
...(ctx.input.authz !== undefined && { authz: ctx.input.authz }),
|
|
43
|
+
...(ctx.input.replay !== undefined && { replay: ctx.input.replay }),
|
|
43
44
|
});
|
|
44
45
|
}
|
package/src/handlers/setup.ts
CHANGED
|
@@ -11,12 +11,15 @@ import { emitTurnEvent } from '../streaming.js';
|
|
|
11
11
|
|
|
12
12
|
import type { TurnContext } from './context.js';
|
|
13
13
|
import { throwAgentTurnFailure } from './errors.js';
|
|
14
|
+
import { SESSION_GATE_SUBJECT, readGateDecision } from './gate-decision.js';
|
|
15
|
+
import { followReplaySessionGate } from './replay.js';
|
|
14
16
|
import { writeRunSnapshot } from './run-snapshot.js';
|
|
15
17
|
import {
|
|
16
18
|
loadTurnConversation,
|
|
17
19
|
resolveTurnEnvironment,
|
|
18
20
|
resolveTurnHitlPolicy,
|
|
19
21
|
} from './turn-environment.js';
|
|
22
|
+
import { decisionOf } from './turn-provenance.js';
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Deterministic waitpoint token — must produce the same value on every
|
|
@@ -35,7 +38,7 @@ function computeSessionGateWaitToken(input: {
|
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/** The `record` key of the session gate's decision. */
|
|
38
|
-
const SESSION_GATE_RECORD = 'session-hitl-gate';
|
|
41
|
+
export const SESSION_GATE_RECORD = 'session-hitl-gate';
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
44
|
* The session gate's decision, as `setup` records it the first time
|
|
@@ -51,16 +54,6 @@ interface SessionGateRecord {
|
|
|
51
54
|
readonly timeoutMs: number;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
/**
|
|
55
|
-
* Shape the setup handler expects when a session-gate waitpoint
|
|
56
|
-
* resolves. Approvals-complete route materializes this from the
|
|
57
|
-
* reviewer's decision (see packages/api/src/routes/approvals.ts).
|
|
58
|
-
*/
|
|
59
|
-
interface SessionGateDecision {
|
|
60
|
-
readonly decided: 'approve' | 'reject';
|
|
61
|
-
readonly rationale?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
57
|
/**
|
|
65
58
|
* The first node in the flow. Runs every precondition check in one
|
|
66
59
|
* gate so the run either commits to a full turn or fails fast before
|
|
@@ -148,7 +141,11 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
|
|
|
148
141
|
timeoutMs: effectiveHitl.timeoutMs,
|
|
149
142
|
};
|
|
150
143
|
});
|
|
151
|
-
if (gate !== undefined) {
|
|
144
|
+
if (gate !== undefined && ctx.input.replay !== undefined) {
|
|
145
|
+
// A replay follows the past run's decision at this gate, or skips it
|
|
146
|
+
// when none was recorded; it never waits for a reviewer.
|
|
147
|
+
await followReplaySessionGate(ctx, kctx);
|
|
148
|
+
} else if (gate !== undefined) {
|
|
152
149
|
const { waitTokenId, timeoutMs } = gate;
|
|
153
150
|
const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
|
|
154
151
|
|
|
@@ -156,7 +153,8 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
|
|
|
156
153
|
try {
|
|
157
154
|
await ctx.bindings.hitl.enqueue({
|
|
158
155
|
tenantId: ctx.input.tenantId,
|
|
159
|
-
|
|
156
|
+
projectId: ctx.input.projectId,
|
|
157
|
+
subjectKind: SESSION_GATE_SUBJECT,
|
|
160
158
|
subjectRef: {
|
|
161
159
|
conversationId: ctx.input.conversationId,
|
|
162
160
|
agentId: ctx.input.agent.id,
|
|
@@ -200,9 +198,11 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
|
|
|
200
198
|
}
|
|
201
199
|
|
|
202
200
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
// Fails closed: only an explicit approve lets the turn go on
|
|
202
|
+
// (`readGateDecision`).
|
|
203
|
+
const decision = readGateDecision(
|
|
204
|
+
await kctx.waitForToken<unknown>(waitTokenId, { timeoutMs }),
|
|
205
|
+
);
|
|
206
206
|
// Post-resume: emit the resume node + resumed-from edge. Only
|
|
207
207
|
// runs on the REPLAY path — the first execution throws
|
|
208
208
|
// SuspensionSignal inside waitForToken and never gets here.
|
|
@@ -211,11 +211,14 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
|
|
|
211
211
|
id: resumeNodeId,
|
|
212
212
|
kind: 'resume',
|
|
213
213
|
timestamp: new Date().toISOString() as never,
|
|
214
|
-
|
|
214
|
+
// Whoever decided; a decision recorded before it named them, the agent.
|
|
215
|
+
actor: decision.decidedBy ?? `agent:${ctx.input.agent.id as unknown as string}`,
|
|
215
216
|
attributes: {
|
|
216
217
|
gate: 'session-hitl',
|
|
217
|
-
decision: decision
|
|
218
|
-
...(decision.
|
|
218
|
+
decision: decisionOf(decision),
|
|
219
|
+
...(!decision.approved &&
|
|
220
|
+
decision.rationale !== undefined && { rationale: decision.rationale }),
|
|
221
|
+
...(decision.approvalId !== undefined && { approvalId: decision.approvalId }),
|
|
219
222
|
},
|
|
220
223
|
});
|
|
221
224
|
ctx.provenance.addEdge({
|
|
@@ -225,7 +228,7 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
|
|
|
225
228
|
});
|
|
226
229
|
}
|
|
227
230
|
|
|
228
|
-
if (decision.
|
|
231
|
+
if (!decision.approved) {
|
|
229
232
|
throwAgentTurnFailure({
|
|
230
233
|
code: 'hitl-rejected',
|
|
231
234
|
message: `Reviewer rejected the session-HITL gate${
|
|
@@ -234,8 +237,7 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
|
|
|
234
237
|
...(decision.rationale !== undefined && { rationale: decision.rationale }),
|
|
235
238
|
} as never);
|
|
236
239
|
}
|
|
237
|
-
//
|
|
238
|
-
// normally through the rest of setup.
|
|
240
|
+
// Approved: the turn proceeds through the rest of setup.
|
|
239
241
|
} catch (cause) {
|
|
240
242
|
if (cause instanceof WaitpointCancelledError) {
|
|
241
243
|
// Emit resume node with cancelled attribute so the DAG still
|
|
@@ -293,6 +295,12 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
|
|
|
293
295
|
providerId: environment.providerId,
|
|
294
296
|
providerModel: environment.model,
|
|
295
297
|
toolCount: environment.toolCount,
|
|
298
|
+
// The version each tool resolved to: a resumed turn runs these.
|
|
299
|
+
toolVersions: environment.toolVersions,
|
|
300
|
+
// And each data block's.
|
|
301
|
+
...(environment.blockVersions !== undefined && {
|
|
302
|
+
blockVersions: environment.blockVersions,
|
|
303
|
+
}),
|
|
296
304
|
};
|
|
297
305
|
};
|
|
298
306
|
}
|
|
@@ -83,6 +83,12 @@ function canonicalStringify(value: unknown): string {
|
|
|
83
83
|
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalStringify(v)}`).join(',')}}`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The `NodeContext.record` key of a tool call's gate, before its call id:
|
|
88
|
+
* the journal names the waitpoint a call parked on under this key.
|
|
89
|
+
*/
|
|
90
|
+
export const TOOL_GATE_RECORD_PREFIX = 'tool-hitl-gate:';
|
|
91
|
+
|
|
86
92
|
/**
|
|
87
93
|
* Deterministic waitpoint token for a tool-call gate. Includes both
|
|
88
94
|
* the model-generated call id (unique per iteration) AND the args hash
|
|
@@ -100,16 +106,6 @@ export function computeToolCallWaitToken(input: {
|
|
|
100
106
|
.slice(0, 40);
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
/**
|
|
104
|
-
* Shape the tool-level waitpoint resolves to when the reviewer decides.
|
|
105
|
-
* Same shape as session-gate decisions — the approvals-complete route
|
|
106
|
-
* materializes it identically.
|
|
107
|
-
*/
|
|
108
|
-
export interface ToolHitlDecision {
|
|
109
|
-
readonly decided: 'approve' | 'reject';
|
|
110
|
-
readonly rationale?: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
109
|
/**
|
|
114
110
|
* In-conversation cache of decisions for `ask_on_first_use`. Persisted
|
|
115
111
|
* on `agent_conversations.metadata.hitlToolDecisions` — a flat map of
|