@kindgi/agents 0.1.3 → 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/define.d.ts +17 -2
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +73 -6
- package/dist/define.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 +10 -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 +51 -1
- 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 +2 -0
- package/dist/handlers/evaluate-guardrails.js.map +1 -1
- package/dist/handlers/model-call.d.ts.map +1 -1
- package/dist/handlers/model-call.js +11 -0
- package/dist/handlers/model-call.js.map +1 -1
- package/dist/handlers/public-types.d.ts +21 -1
- package/dist/handlers/public-types.d.ts.map +1 -1
- package/dist/handlers/rehydrate.d.ts.map +1 -1
- package/dist/handlers/rehydrate.js +6 -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 +34 -17
- 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 +14 -2
- package/dist/handlers/setup.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/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/invoke.d.ts.map +1 -1
- package/dist/invoke.js +3 -0
- 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/run-snapshot-binding.d.ts +5 -0
- package/dist/run-snapshot-binding.d.ts.map +1 -1
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +5 -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 +50 -1
- package/dist/types.d.ts.map +1 -1
- package/migrations/0004_stormy_moondragon.sql +1 -0
- package/migrations/meta/0004_snapshot.json +327 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +15 -15
- package/src/blocks.ts +231 -0
- package/src/define.ts +84 -7
- package/src/handlers/compose-result.ts +3 -0
- package/src/handlers/context.ts +10 -0
- package/src/handlers/dispatch-tools.ts +54 -1
- package/src/handlers/errors.ts +13 -0
- package/src/handlers/evaluate-guardrails.ts +2 -0
- package/src/handlers/model-call.ts +14 -0
- package/src/handlers/public-types.ts +22 -1
- package/src/handlers/rehydrate.ts +21 -6
- 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 +44 -23
- package/src/handlers/run-snapshot.ts +1 -0
- package/src/handlers/setup.ts +13 -2
- package/src/handlers/turn-environment.ts +28 -3
- package/src/index.ts +37 -0
- package/src/invoke.ts +3 -0
- package/src/pins.ts +98 -0
- package/src/prompt.ts +52 -4
- package/src/run-snapshot-binding.ts +6 -0
- package/src/schema.ts +5 -0
- package/src/streaming.ts +5 -0
- package/src/types.ts +53 -1
|
@@ -1,10 +1,11 @@
|
|
|
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';
|
|
@@ -17,7 +18,7 @@ import { addRetrievalNodes } from './turn-provenance.js';
|
|
|
17
18
|
* is wired.
|
|
18
19
|
*/
|
|
19
20
|
export function buildRunRetrievalsHandler(ctx: TurnContext): NodeHandler {
|
|
20
|
-
return async () => {
|
|
21
|
+
return async (_input, kctx) => {
|
|
21
22
|
if (ctx.conversation === undefined || ctx.userMessage === undefined) {
|
|
22
23
|
throwAgentTurnFailure({
|
|
23
24
|
code: 'model-invocation-failed',
|
|
@@ -25,36 +26,56 @@ export function buildRunRetrievalsHandler(ctx: TurnContext): NodeHandler {
|
|
|
25
26
|
cause: null,
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
ctx.conversation,
|
|
31
|
-
ctx.input.conversationId,
|
|
32
|
-
ctx.input.userMessage,
|
|
33
|
-
{
|
|
34
|
-
memory: ctx.bindings.memoryBinding,
|
|
35
|
-
...(ctx.bindings.embeddingRegistry !== undefined && {
|
|
36
|
-
embeddingRegistry: ctx.bindings.embeddingRegistry,
|
|
37
|
-
}),
|
|
38
|
-
...(ctx.bindings.embeddingModel !== undefined && {
|
|
39
|
-
embeddingModel: ctx.bindings.embeddingModel,
|
|
40
|
-
}),
|
|
41
|
-
},
|
|
42
|
-
);
|
|
43
|
-
if (retrieved.kind === 'err') throwAgentTurnFailure(retrieved.error);
|
|
44
|
-
ctx.retrieved = retrieved.value;
|
|
29
|
+
const facts = (await recordedRetrievals(ctx, kctx)) ?? (await retrieveLive(ctx));
|
|
30
|
+
ctx.retrieved = facts;
|
|
45
31
|
|
|
46
32
|
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
47
33
|
kind: 'retrieval.completed',
|
|
48
|
-
count:
|
|
49
|
-
factIds:
|
|
34
|
+
count: facts.length,
|
|
35
|
+
factIds: facts.map((r) => r.fact.id),
|
|
50
36
|
});
|
|
51
37
|
|
|
52
38
|
if (ctx.provenance !== undefined && ctx.userMessage !== undefined) {
|
|
53
|
-
addRetrievalNodes(ctx.provenance,
|
|
39
|
+
addRetrievalNodes(ctx.provenance, facts, ctx.userMessage);
|
|
54
40
|
}
|
|
55
41
|
|
|
56
42
|
// The facts go in the journal: a resumed turn restores them from it
|
|
57
43
|
// (`rehydrateTurnContext`) rather than retrieving again.
|
|
58
|
-
return { count:
|
|
44
|
+
return { count: facts.length, retrieved: facts };
|
|
59
45
|
};
|
|
60
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
|
@@ -12,6 +12,7 @@ import { emitTurnEvent } from '../streaming.js';
|
|
|
12
12
|
import type { TurnContext } from './context.js';
|
|
13
13
|
import { throwAgentTurnFailure } from './errors.js';
|
|
14
14
|
import { SESSION_GATE_SUBJECT, readGateDecision } from './gate-decision.js';
|
|
15
|
+
import { followReplaySessionGate } from './replay.js';
|
|
15
16
|
import { writeRunSnapshot } from './run-snapshot.js';
|
|
16
17
|
import {
|
|
17
18
|
loadTurnConversation,
|
|
@@ -37,7 +38,7 @@ function computeSessionGateWaitToken(input: {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/** The `record` key of the session gate's decision. */
|
|
40
|
-
const SESSION_GATE_RECORD = 'session-hitl-gate';
|
|
41
|
+
export const SESSION_GATE_RECORD = 'session-hitl-gate';
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* The session gate's decision, as `setup` records it the first time
|
|
@@ -140,7 +141,11 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
|
|
|
140
141
|
timeoutMs: effectiveHitl.timeoutMs,
|
|
141
142
|
};
|
|
142
143
|
});
|
|
143
|
-
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) {
|
|
144
149
|
const { waitTokenId, timeoutMs } = gate;
|
|
145
150
|
const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
|
|
146
151
|
|
|
@@ -290,6 +295,12 @@ export function buildSetupHandler(ctx: TurnContext): NodeHandler {
|
|
|
290
295
|
providerId: environment.providerId,
|
|
291
296
|
providerModel: environment.model,
|
|
292
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
|
+
}),
|
|
293
304
|
};
|
|
294
305
|
};
|
|
295
306
|
}
|
|
@@ -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 { type PinnedBlockVersions, resolveTurnBlocks } from './resolve-blocks.js';
|
|
24
25
|
import { resolveTurnTools } from './resolve-tools.js';
|
|
25
26
|
import { type ToolErrorPolicy, effectiveToolErrorPolicy } from './tool-errors.js';
|
|
26
27
|
|
|
@@ -61,16 +62,33 @@ export interface PinnedRoute {
|
|
|
61
62
|
readonly model: string;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The tool version each of the agent's tool references resolved to, by
|
|
67
|
+
* tool id, as `setup` journals it: a resumed turn runs these versions,
|
|
68
|
+
* whatever the registry holds by then.
|
|
69
|
+
*/
|
|
70
|
+
export type PinnedToolVersions = Readonly<Record<string, string>>;
|
|
71
|
+
|
|
64
72
|
/**
|
|
65
73
|
* Resolve the turn's guardrails, tools, tenant policy, tool-error policy
|
|
66
74
|
* and model onto `ctx`. With `pinned`, the route is that provider and
|
|
67
75
|
* model, still under the tenant's current policy; one no longer
|
|
68
|
-
* registered or allowed fails the turn.
|
|
76
|
+
* registered or allowed fails the turn. With `pinnedTools`, each tool is
|
|
77
|
+
* that exact version, not its range resolved again; one no longer
|
|
78
|
+
* registered fails the turn.
|
|
69
79
|
*/
|
|
70
80
|
export async function resolveTurnEnvironment(
|
|
71
81
|
ctx: TurnContext,
|
|
72
82
|
pinned?: PinnedRoute,
|
|
73
|
-
|
|
83
|
+
pinnedTools?: PinnedToolVersions,
|
|
84
|
+
pinnedBlocks?: PinnedBlockVersions,
|
|
85
|
+
): Promise<
|
|
86
|
+
PinnedRoute & {
|
|
87
|
+
readonly toolCount: number;
|
|
88
|
+
readonly toolVersions: PinnedToolVersions;
|
|
89
|
+
readonly blockVersions?: PinnedBlockVersions;
|
|
90
|
+
}
|
|
91
|
+
> {
|
|
74
92
|
const invResolution = resolveGuardrails(ctx.input.agent, ctx.bindings);
|
|
75
93
|
if (invResolution.missing.length > 0) {
|
|
76
94
|
throwAgentTurnFailure({
|
|
@@ -84,7 +102,10 @@ export async function resolveTurnEnvironment(
|
|
|
84
102
|
// This turn's tools come from the tenant's own registry — never a
|
|
85
103
|
// registry shared across concurrent turns of other tenants.
|
|
86
104
|
const tenantTools = await ctx.bindings.toolRegistry.forTenant(ctx.input.tenantId);
|
|
87
|
-
ctx.tools = resolveTurnTools(tenantTools, ctx.input.agent);
|
|
105
|
+
ctx.tools = resolveTurnTools(tenantTools, ctx.input.agent, pinnedTools);
|
|
106
|
+
// The data blocks, at the versions pinned (the turn's own on resume).
|
|
107
|
+
const blocks = await resolveTurnBlocks(ctx, pinnedBlocks);
|
|
108
|
+
if (blocks !== undefined) ctx.blocks = blocks;
|
|
88
109
|
|
|
89
110
|
const capability = ctx.input.agent.capabilities[0];
|
|
90
111
|
if (capability === undefined) {
|
|
@@ -151,6 +172,10 @@ export async function resolveTurnEnvironment(
|
|
|
151
172
|
providerId: routed.value.provider.metadata.id,
|
|
152
173
|
model: routed.value.model.name,
|
|
153
174
|
toolCount: ctx.tools.definitions.length,
|
|
175
|
+
toolVersions: Object.fromEntries(
|
|
176
|
+
[...ctx.tools.byName].map(([id, binding]) => [id, binding.resolvedVersion]),
|
|
177
|
+
),
|
|
178
|
+
...(blocks !== undefined && { blockVersions: blocks.versions }),
|
|
154
179
|
};
|
|
155
180
|
}
|
|
156
181
|
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,22 @@ export type {
|
|
|
17
17
|
RunSnapshotWriteInput,
|
|
18
18
|
} from './run-snapshot-binding.js';
|
|
19
19
|
export { defineAgent } from './define.js';
|
|
20
|
+
export {
|
|
21
|
+
BLOCK_KINDS,
|
|
22
|
+
MODEL_SETTINGS_SCHEMA,
|
|
23
|
+
settingsSchemaIssues,
|
|
24
|
+
validateBlock,
|
|
25
|
+
} from './blocks.js';
|
|
26
|
+
export type {
|
|
27
|
+
BlockDefinition,
|
|
28
|
+
BlockIssue,
|
|
29
|
+
BlockKind,
|
|
30
|
+
BlockReader,
|
|
31
|
+
InvalidBlock,
|
|
32
|
+
ModelSettings,
|
|
33
|
+
PromptBlockContent,
|
|
34
|
+
SettingsBlockContent,
|
|
35
|
+
} from './blocks.js';
|
|
20
36
|
export type { DefineAgentSpec } from './define.js';
|
|
21
37
|
export { resolveEffectiveHitlPolicy } from './hitl-policy.js';
|
|
22
38
|
export {
|
|
@@ -28,6 +44,17 @@ export {
|
|
|
28
44
|
export type { GateDecision, GateDecisionValue } from './handlers/gate-decision.js';
|
|
29
45
|
export type { EffectiveHitlPolicy } from './hitl-policy.js';
|
|
30
46
|
export { agentStepOutput, invokeAgent, resumeAgentTurn } from './invoke.js';
|
|
47
|
+
export { isReadOnlyTool } from './handlers/replay.js';
|
|
48
|
+
export { SESSION_GATE_RECORD } from './handlers/setup.js';
|
|
49
|
+
export type {
|
|
50
|
+
ReplayApproval,
|
|
51
|
+
ReplayBinding,
|
|
52
|
+
ReplayToolDecision,
|
|
53
|
+
ReplayToolInput,
|
|
54
|
+
ReplayToolTrace,
|
|
55
|
+
ReplayTurnRef,
|
|
56
|
+
ReplayTurnReport,
|
|
57
|
+
} from './handlers/replay.js';
|
|
31
58
|
export type {
|
|
32
59
|
AgentStepOutput,
|
|
33
60
|
AgentTurnAbortedError,
|
|
@@ -87,6 +114,14 @@ export type {
|
|
|
87
114
|
RenderFailureError,
|
|
88
115
|
RenderResult,
|
|
89
116
|
} from './prompt.js';
|
|
117
|
+
export { pinChanges, pinsDigest } from './pins.js';
|
|
118
|
+
export type {
|
|
119
|
+
AgentDerivation,
|
|
120
|
+
AgentDerivationReason,
|
|
121
|
+
AgentPins,
|
|
122
|
+
PinChange,
|
|
123
|
+
PinSet,
|
|
124
|
+
} from './pins.js';
|
|
90
125
|
export { createAgentRegistry } from './registry.js';
|
|
91
126
|
export type { AgentRegistry } from './registry.js';
|
|
92
127
|
export {
|
|
@@ -105,12 +140,14 @@ export type {
|
|
|
105
140
|
AgentBindings,
|
|
106
141
|
AgentId,
|
|
107
142
|
AgentOutputSpec,
|
|
143
|
+
BlockRef,
|
|
108
144
|
Conversation,
|
|
109
145
|
ConversationId,
|
|
110
146
|
ConversationMessage,
|
|
111
147
|
ConversationPolicy,
|
|
112
148
|
MessageRole,
|
|
113
149
|
PromptParameter,
|
|
150
|
+
PromptRef,
|
|
114
151
|
RetrievalIntent,
|
|
115
152
|
RetrievedFact,
|
|
116
153
|
ToolRef,
|
package/src/invoke.ts
CHANGED
|
@@ -90,6 +90,8 @@ export async function invokeAgent(
|
|
|
90
90
|
conversationId: input.conversationId,
|
|
91
91
|
},
|
|
92
92
|
...(input.parent !== undefined && { parent: input.parent }),
|
|
93
|
+
// A replay's run says so, and which eval run and past run it is for.
|
|
94
|
+
...(input.replay !== undefined && { replay: input.replay }),
|
|
93
95
|
...(input.dryRun === true && { options: { dryRun: true } }),
|
|
94
96
|
// Authorization — carry principal + authz into the run so every
|
|
95
97
|
// tool invocation inside the agent's turn is checked.
|
|
@@ -323,5 +325,6 @@ export function turnInputFromSnapshot(
|
|
|
323
325
|
snapshot.authz !== null && {
|
|
324
326
|
authz: snapshot.authz as { readonly fgaApiUrl: string },
|
|
325
327
|
}),
|
|
328
|
+
...(snapshot.replay !== undefined && snapshot.replay !== null && { replay: snapshot.replay }),
|
|
326
329
|
};
|
|
327
330
|
}
|
package/src/pins.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
import { canonicalize } from '@kindgi/schema';
|
|
7
|
+
import type { VersionDerivation, VersionDerivationReason } from '@kindgi/types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The exact version of each block an agent version runs: its lockfile.
|
|
11
|
+
*
|
|
12
|
+
* An agent names its tools by range (`{ id: 'acme.lookup', version:
|
|
13
|
+
* '^1.0.0' }`), like `package.json`. When a version of the agent is
|
|
14
|
+
* published, the runtime resolves each range once, and every run of that
|
|
15
|
+
* version uses the versions recorded here. So a new tool version reaches
|
|
16
|
+
* an agent only through a new agent version, and two runs of one agent
|
|
17
|
+
* version always run the same blocks.
|
|
18
|
+
*
|
|
19
|
+
* Set by the runtime at publish, never authored. A version published
|
|
20
|
+
* before pins existed has none and resolves its ranges per run.
|
|
21
|
+
*/
|
|
22
|
+
export interface AgentPins {
|
|
23
|
+
/** Tool id → the exact version this agent version runs. */
|
|
24
|
+
readonly tools: Readonly<Record<string, string>>;
|
|
25
|
+
/** Prompt block id → exact version. Empty until the agent references prompt blocks. */
|
|
26
|
+
readonly prompts: Readonly<Record<string, string>>;
|
|
27
|
+
/** Settings block id → exact version. Empty until the agent references settings blocks. */
|
|
28
|
+
readonly settings: Readonly<Record<string, string>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One string naming a set of pins: `sha256:<hex>` of the pins'
|
|
33
|
+
* canonical JSON (keys sorted, no whitespace). Two agent versions with
|
|
34
|
+
* the same digest run the same blocks; the digest is what a comparison
|
|
35
|
+
* of versions, or a gate, records and compares. The Python SDK's
|
|
36
|
+
* `pins_digest` computes the same string.
|
|
37
|
+
*/
|
|
38
|
+
export function pinsDigest(pins: AgentPins): string {
|
|
39
|
+
const canonical = canonicalize({
|
|
40
|
+
tools: pins.tools,
|
|
41
|
+
prompts: pins.prompts,
|
|
42
|
+
settings: pins.settings,
|
|
43
|
+
});
|
|
44
|
+
return `sha256:${createHash('sha256').update(canonical, 'utf8').digest('hex')}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Why a deploy registered an agent version under another number (`VersionDerivationReason`). */
|
|
48
|
+
export type AgentDerivationReason = VersionDerivationReason;
|
|
49
|
+
|
|
50
|
+
/** The version an agent version was registered in place of, and why. */
|
|
51
|
+
export type AgentDerivation = VersionDerivation;
|
|
52
|
+
|
|
53
|
+
/** One pin that differs between two versions of an agent or a flow. */
|
|
54
|
+
export interface PinChange {
|
|
55
|
+
readonly kind: 'tool' | 'prompt' | 'setting' | 'agent';
|
|
56
|
+
readonly id: string;
|
|
57
|
+
/** The earlier version's pin; absent when it didn't pin this block. */
|
|
58
|
+
readonly from?: string;
|
|
59
|
+
/** The later version's pin; absent when it doesn't pin this block. */
|
|
60
|
+
readonly to?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const PIN_KINDS = [
|
|
64
|
+
['tools', 'tool'],
|
|
65
|
+
['prompts', 'prompt'],
|
|
66
|
+
['settings', 'setting'],
|
|
67
|
+
['agents', 'agent'],
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
/** A version's pins by kind: an agent's (`AgentPins`) or a flow's (`FlowPins`). */
|
|
71
|
+
export type PinSet = Partial<
|
|
72
|
+
Record<(typeof PIN_KINDS)[number][0], Readonly<Record<string, string>>>
|
|
73
|
+
>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The pins that differ from `before` to `after`, by kind then id. With
|
|
77
|
+
* no `before` (a version published before pins), every pin of `after`
|
|
78
|
+
* is a change.
|
|
79
|
+
*/
|
|
80
|
+
export function pinChanges(before: PinSet | undefined, after: PinSet): PinChange[] {
|
|
81
|
+
const changes: PinChange[] = [];
|
|
82
|
+
for (const [key, kind] of PIN_KINDS) {
|
|
83
|
+
const was = before?.[key] ?? {};
|
|
84
|
+
const now = after[key] ?? {};
|
|
85
|
+
for (const id of [...new Set([...Object.keys(was), ...Object.keys(now)])].sort()) {
|
|
86
|
+
const from = was[id];
|
|
87
|
+
const to = now[id];
|
|
88
|
+
if (from === to) continue;
|
|
89
|
+
changes.push({
|
|
90
|
+
kind,
|
|
91
|
+
id,
|
|
92
|
+
...(from !== undefined && { from }),
|
|
93
|
+
...(to !== undefined && { to }),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return changes;
|
|
98
|
+
}
|
package/src/prompt.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import { Liquid } from 'liquidjs';
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { PromptBlockContent } from './blocks.js';
|
|
7
|
+
import type { Agent, PromptParameter, PromptRef } from './types.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Reserved variable namespaces the runtime injects automatically.
|
|
@@ -20,8 +21,18 @@ import type { Agent, PromptParameter } from './types.js';
|
|
|
20
21
|
* - `input` — the turn's structured input (`InvokeAgentInput.input`),
|
|
21
22
|
* e.g. `{{ input.grievance.summary }}`; unset when
|
|
22
23
|
* the turn has none.
|
|
24
|
+
* - `settings` — the agent's settings blocks' values, by block id:
|
|
25
|
+
* `{{ settings["acme.weights"].recency }}` reads
|
|
26
|
+
* block `acme.weights`; unset when it has none.
|
|
23
27
|
*/
|
|
24
|
-
export const AUTO_INJECTED_VARS = [
|
|
28
|
+
export const AUTO_INJECTED_VARS = [
|
|
29
|
+
'today',
|
|
30
|
+
'now',
|
|
31
|
+
'agent',
|
|
32
|
+
'conversation',
|
|
33
|
+
'input',
|
|
34
|
+
'settings',
|
|
35
|
+
] as const;
|
|
25
36
|
|
|
26
37
|
/**
|
|
27
38
|
* Shape passed to `renderInstructions`. Framework auto-vars are
|
|
@@ -37,6 +48,8 @@ export interface RenderContext {
|
|
|
37
48
|
};
|
|
38
49
|
/** The turn's structured input, rendered as `{{ input.* }}`. */
|
|
39
50
|
readonly input?: unknown;
|
|
51
|
+
/** The agent's settings blocks' values, by block id: `{{ settings["<id>"].<key> }}`. */
|
|
52
|
+
readonly settings?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
40
53
|
/**
|
|
41
54
|
* Optional clock override — tests inject a fixed date. Defaults to
|
|
42
55
|
* `new Date()`.
|
|
@@ -84,14 +97,21 @@ export interface RenderFailureError {
|
|
|
84
97
|
* Uses `strictVariables: true` so an unresolved `{{ var }}` is an error
|
|
85
98
|
* — never a silent empty string. This is load-bearing for correctness
|
|
86
99
|
* (a system prompt with a missing firm name is worse than a hard fail).
|
|
100
|
+
*
|
|
101
|
+
* When the instructions come from a prompt block, `prompt` is the
|
|
102
|
+
* version the turn runs (its template and declared parameters); an
|
|
103
|
+
* agent whose instructions are a prompt block can't render without it.
|
|
87
104
|
*/
|
|
88
105
|
export function renderInstructions(
|
|
89
106
|
agent: Agent,
|
|
90
107
|
context: RenderContext,
|
|
108
|
+
prompt?: PromptBlockContent,
|
|
91
109
|
):
|
|
92
110
|
| { readonly ok: true; readonly value: RenderResult }
|
|
93
111
|
| { readonly ok: false; readonly error: PromptRenderError } {
|
|
94
|
-
const
|
|
112
|
+
const source = instructionsSource(agent, prompt);
|
|
113
|
+
if (!source.ok) return source;
|
|
114
|
+
const { template, declared } = source.value;
|
|
95
115
|
const missing = requiredMissing(declared, context.parameters);
|
|
96
116
|
if (missing.length > 0) {
|
|
97
117
|
return {
|
|
@@ -107,7 +127,7 @@ export function renderInstructions(
|
|
|
107
127
|
const merged = mergeContext(agent, declared, context);
|
|
108
128
|
const liquid = new Liquid({ strictVariables: true, strictFilters: true });
|
|
109
129
|
try {
|
|
110
|
-
const rendered = liquid.parseAndRenderSync(
|
|
130
|
+
const rendered = liquid.parseAndRenderSync(template, merged);
|
|
111
131
|
return { ok: true, value: { rendered, context: merged } };
|
|
112
132
|
} catch (cause) {
|
|
113
133
|
// LiquidJS throws UndefinedVariableError for unresolved refs — surface
|
|
@@ -135,6 +155,33 @@ export function renderInstructions(
|
|
|
135
155
|
}
|
|
136
156
|
}
|
|
137
157
|
|
|
158
|
+
/** The template to render and the parameters it declares: the prompt block's, or the agent's own. */
|
|
159
|
+
function instructionsSource(
|
|
160
|
+
agent: Agent,
|
|
161
|
+
prompt: PromptBlockContent | undefined,
|
|
162
|
+
):
|
|
163
|
+
| {
|
|
164
|
+
readonly ok: true;
|
|
165
|
+
readonly value: { readonly template: string; readonly declared: readonly PromptParameter[] };
|
|
166
|
+
}
|
|
167
|
+
| { readonly ok: false; readonly error: PromptRenderError } {
|
|
168
|
+
if (prompt !== undefined) {
|
|
169
|
+
return { ok: true, value: { template: prompt.template, declared: prompt.parameters ?? [] } };
|
|
170
|
+
}
|
|
171
|
+
if (typeof agent.instructions === 'string') {
|
|
172
|
+
return { ok: true, value: { template: agent.instructions, declared: agent.parameters ?? [] } };
|
|
173
|
+
}
|
|
174
|
+
const ref: PromptRef = agent.instructions;
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: {
|
|
178
|
+
code: 'render-failure',
|
|
179
|
+
message: `The instructions come from prompt block "${ref.prompt}" (${ref.version}), which wasn't loaded`,
|
|
180
|
+
cause: null,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
138
185
|
function requiredMissing(
|
|
139
186
|
declared: readonly PromptParameter[],
|
|
140
187
|
supplied: Readonly<Record<string, unknown>>,
|
|
@@ -181,5 +228,6 @@ function mergeContext(
|
|
|
181
228
|
};
|
|
182
229
|
}
|
|
183
230
|
if (context.input !== undefined) values.input = context.input;
|
|
231
|
+
if (context.settings !== undefined) values.settings = context.settings;
|
|
184
232
|
return values;
|
|
185
233
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import type { ProjectId, Result, RunId, Semver, TenantId, Timestamp } from '@kindgi/types';
|
|
5
5
|
|
|
6
|
+
import type { RunReplayRef } from '@kindgi/runtime';
|
|
7
|
+
|
|
6
8
|
import type { PersistenceError } from './errors.js';
|
|
7
9
|
import type { AgentId, ConversationId } from './types.js';
|
|
8
10
|
|
|
@@ -37,6 +39,8 @@ export interface RunSnapshotWriteInput {
|
|
|
37
39
|
* as `principal`.
|
|
38
40
|
*/
|
|
39
41
|
readonly authz?: unknown;
|
|
42
|
+
/** The turn's replay marker (`InvokeAgentInput.replay`): a resumed replay stays one. */
|
|
43
|
+
readonly replay?: RunReplayRef;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/**
|
|
@@ -59,6 +63,8 @@ export interface RunSnapshotRecord {
|
|
|
59
63
|
readonly dryRun: boolean;
|
|
60
64
|
readonly principal?: unknown;
|
|
61
65
|
readonly authz?: unknown;
|
|
66
|
+
/** The turn's replay marker (`InvokeAgentInput.replay`): a resumed replay stays one. */
|
|
67
|
+
readonly replay?: RunReplayRef;
|
|
62
68
|
readonly createdAt: Timestamp;
|
|
63
69
|
}
|
|
64
70
|
|
package/src/schema.ts
CHANGED
|
@@ -148,6 +148,11 @@ export const agentRunSnapshots = pgTable(
|
|
|
148
148
|
* Threaded back into the resumed TurnContext.
|
|
149
149
|
*/
|
|
150
150
|
authz: jsonb('authz'),
|
|
151
|
+
/**
|
|
152
|
+
* The replay marker (`{of, evalRunId}`) of a replay turn, so a resumed
|
|
153
|
+
* replay stays one: its tool calls are still decided by the replay rules.
|
|
154
|
+
*/
|
|
155
|
+
replay: jsonb('replay'),
|
|
151
156
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
152
157
|
},
|
|
153
158
|
(t) => ({
|
package/src/streaming.ts
CHANGED
|
@@ -86,6 +86,11 @@ export interface ToolCompletedEvent {
|
|
|
86
86
|
readonly invocationId: string;
|
|
87
87
|
readonly output: unknown;
|
|
88
88
|
readonly durationMs: number;
|
|
89
|
+
/**
|
|
90
|
+
* In a replay turn: whether the tool ran (`live`), the past run's result
|
|
91
|
+
* was used (`recorded`), or the call was refused (`refused`).
|
|
92
|
+
*/
|
|
93
|
+
readonly replay?: 'live' | 'recorded' | 'refused';
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
export interface ToolFailedEvent {
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { Fact, MemoryScope } from '@kindgi/memory';
|
|
|
6
6
|
import type { ToolErrorsSpec, ToolHitlMode, ToolHitlRule } from '@kindgi/policy-contract';
|
|
7
7
|
import type { Brand, ConversationId, ProjectId, Semver, TenantId, Timestamp } from '@kindgi/types';
|
|
8
8
|
|
|
9
|
+
import type { AgentDerivation, AgentPins } from './pins.js';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Branded agent id. Convention: dotted namespace under the tenant's
|
|
11
13
|
* pack — e.g. `acme.citation-verifier`, `acme.drafting`.
|
|
@@ -121,6 +123,22 @@ export interface ToolRef {
|
|
|
121
123
|
readonly version: string;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
/**
|
|
127
|
+
* A prompt block an agent's instructions come from: its id and a semver
|
|
128
|
+
* range, resolved like a tool's (`pickVersion`) and pinned when the
|
|
129
|
+
* agent version is published.
|
|
130
|
+
*/
|
|
131
|
+
export interface PromptRef {
|
|
132
|
+
readonly prompt: string;
|
|
133
|
+
readonly version: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A settings block an agent reads: its id and a semver range, pinned at publish. */
|
|
137
|
+
export interface BlockRef {
|
|
138
|
+
readonly id: string;
|
|
139
|
+
readonly version: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
124
142
|
/**
|
|
125
143
|
* Per-agent behavior for multi-turn conversations. The agent chooses:
|
|
126
144
|
* - How many prior messages to load (`historyLimit`; unset = all).
|
|
@@ -292,8 +310,13 @@ export interface Agent {
|
|
|
292
310
|
* unresolved reference fails the turn at invoke time
|
|
293
311
|
* (`model-invocation-failed` whose `cause` is the `missing-parameter`
|
|
294
312
|
* render error), never a silent empty string.
|
|
313
|
+
*
|
|
314
|
+
* Or a prompt block, by range (`{ prompt: 'acme.intake-prompt',
|
|
315
|
+
* version: '^1.0.0' }`): its template renders here instead, with the
|
|
316
|
+
* parameters it declares, and the version that runs is pinned when the
|
|
317
|
+
* agent version is published (`pins.prompts`).
|
|
295
318
|
*/
|
|
296
|
-
readonly instructions: string;
|
|
319
|
+
readonly instructions: string | PromptRef;
|
|
297
320
|
/**
|
|
298
321
|
* Typed parameters the caller supplies at invoke time. The UI reads
|
|
299
322
|
* this to build a "configure agent" form; the runtime validates each
|
|
@@ -405,6 +428,35 @@ export interface Agent {
|
|
|
405
428
|
* A tenant's `tool-errors` policy can lower it. See `ToolErrorsSpec`.
|
|
406
429
|
*/
|
|
407
430
|
readonly toolErrors?: ToolErrorsSpec;
|
|
431
|
+
/**
|
|
432
|
+
* The exact block versions this agent version runs, resolved by the
|
|
433
|
+
* runtime when the version was published (see `AgentPins`). Never
|
|
434
|
+
* authored: `defineAgent` doesn't take it. Absent on an agent defined
|
|
435
|
+
* in code and on a version published before pins existed; its tool
|
|
436
|
+
* ranges then resolve per run.
|
|
437
|
+
*/
|
|
438
|
+
readonly pins?: AgentPins;
|
|
439
|
+
/**
|
|
440
|
+
* Settings blocks the agent reads, by range. Each block's values reach
|
|
441
|
+
* its tools as `ToolContext.settings[<block id>]` and its templates as
|
|
442
|
+
* `settings.<block id>.<key>`; the versions are pinned at publish
|
|
443
|
+
* (`pins.settings`).
|
|
444
|
+
*/
|
|
445
|
+
readonly settings?: readonly BlockRef[];
|
|
446
|
+
/**
|
|
447
|
+
* A settings block of model settings (`MODEL_SETTINGS_SCHEMA`:
|
|
448
|
+
* `temperature`, `maxOutputTokens`) the turn's model calls use. Pinned
|
|
449
|
+
* at publish with the other settings.
|
|
450
|
+
*/
|
|
451
|
+
readonly modelSettings?: BlockRef;
|
|
452
|
+
/** `pinsDigest(pins)`, recorded when the version was published. */
|
|
453
|
+
readonly pinsDigest?: string;
|
|
454
|
+
/**
|
|
455
|
+
* Set by the runtime on a version it registered under another number
|
|
456
|
+
* than the definition's, when a deploy couldn't register that number
|
|
457
|
+
* as it was (see `AgentDerivation`). Never authored.
|
|
458
|
+
*/
|
|
459
|
+
readonly derivedFrom?: AgentDerivation;
|
|
408
460
|
}
|
|
409
461
|
|
|
410
462
|
/**
|