@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
|
@@ -5,7 +5,7 @@ import type { ModelMessage, ModelToolCall } from '@kindgi/capabilities';
|
|
|
5
5
|
import type { NodeContext, NodeHandler } from '@kindgi/handler';
|
|
6
6
|
import { WaitpointCancelledError } from '@kindgi/handler';
|
|
7
7
|
import { stricterToolHitlRule } from '@kindgi/policy-contract';
|
|
8
|
-
import { invokeTool } from '@kindgi/tools';
|
|
8
|
+
import { invokeTool, toolCallRecordKey } from '@kindgi/tools';
|
|
9
9
|
import type { Tool, ToolContext } from '@kindgi/tools';
|
|
10
10
|
|
|
11
11
|
import { emitTurnEvent } from '../streaming.js';
|
|
@@ -259,7 +259,7 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
|
259
259
|
arguments: call.arguments,
|
|
260
260
|
gated: gate !== undefined,
|
|
261
261
|
});
|
|
262
|
-
if (replayed !== undefined && replayed.kind !== 'live') {
|
|
262
|
+
if (replayed !== undefined && replayed.kind !== 'live' && replayed.kind !== 'recomputed') {
|
|
263
263
|
const replayStarted = Date.now();
|
|
264
264
|
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
265
265
|
kind: 'tool.started',
|
|
@@ -386,7 +386,9 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
|
386
386
|
continue;
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
|
|
389
|
+
// A replay's live call reads the env the past run's call saw.
|
|
390
|
+
const replayEnv = replayed?.kind === 'live' ? replayed.env : undefined;
|
|
391
|
+
const dispatched = await dispatchOne(ctx, tool, call, kctx, replayEnv);
|
|
390
392
|
if (dispatched.kind === 'err') {
|
|
391
393
|
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
392
394
|
kind: 'tool.failed',
|
|
@@ -421,7 +423,10 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
|
421
423
|
invocationId: call.id,
|
|
422
424
|
output: dispatched.value.persisted.content,
|
|
423
425
|
durationMs: Date.now() - toolStarted,
|
|
424
|
-
|
|
426
|
+
// A recomputed call ran, as a live one does.
|
|
427
|
+
...(replayed !== undefined && {
|
|
428
|
+
replay: replayed.kind === 'recomputed' ? ('live' as const) : replayed.kind,
|
|
429
|
+
}),
|
|
425
430
|
});
|
|
426
431
|
}
|
|
427
432
|
|
|
@@ -575,7 +580,8 @@ async function dispatchOne(
|
|
|
575
580
|
ctx: TurnContext,
|
|
576
581
|
tool: Tool,
|
|
577
582
|
call: ModelToolCall,
|
|
578
|
-
|
|
583
|
+
kctx: NodeContext,
|
|
584
|
+
replayEnv?: Readonly<Record<string, string>>,
|
|
579
585
|
): Promise<
|
|
580
586
|
| {
|
|
581
587
|
readonly kind: 'ok';
|
|
@@ -596,6 +602,8 @@ async function dispatchOne(
|
|
|
596
602
|
};
|
|
597
603
|
}
|
|
598
604
|
> {
|
|
605
|
+
const runId = kctx.runId as unknown as string;
|
|
606
|
+
const toolId = tool.id as unknown as string;
|
|
599
607
|
const toolCtx: ToolContext = {
|
|
600
608
|
tenantId: ctx.input.tenantId,
|
|
601
609
|
runId,
|
|
@@ -610,6 +618,12 @@ async function dispatchOne(
|
|
|
610
618
|
...(ctx.bindings.resolveSecret !== undefined && { resolveSecret: ctx.bindings.resolveSecret }),
|
|
611
619
|
// The pinned settings blocks' values, by block id.
|
|
612
620
|
...(ctx.blocks !== undefined && { settings: ctx.blocks.settings }),
|
|
621
|
+
// The call's durable decisions (its resolved env): this step's own
|
|
622
|
+
// record, keyed by the call and the tool (`toolCallRecordKey`).
|
|
623
|
+
record: (key, decide) =>
|
|
624
|
+
kctx.record(toolCallRecordKey({ toolId, key, callId: call.id }), decide),
|
|
625
|
+
// A replay's live call: the past run's env values for the tool.
|
|
626
|
+
...(replayEnv !== undefined && { env: replayEnv }),
|
|
613
627
|
};
|
|
614
628
|
const result = await invokeTool(tool, call.arguments, toolCtx);
|
|
615
629
|
if (result.kind === 'err') {
|
package/src/handlers/errors.ts
CHANGED
|
@@ -27,7 +27,22 @@ export type InvokeAgentError =
|
|
|
27
27
|
| UnresolvedGuardrailError
|
|
28
28
|
| OutputSchemaViolationError
|
|
29
29
|
| TenantPolicyUnavailableError
|
|
30
|
-
| RunSnapshotError
|
|
30
|
+
| RunSnapshotError
|
|
31
|
+
| SemanticUnavailableError;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A retrieval intent searches by meaning (`mode: 'semantic'`) and the
|
|
35
|
+
* runtime has no embeddings. The turn fails rather than retrieve nothing
|
|
36
|
+
* without a word; the operator turns embeddings on
|
|
37
|
+
* (`KINDGI_MEMORY_EMBEDDINGS`), or the agent asks for `both` (which runs
|
|
38
|
+
* its keyword half without them).
|
|
39
|
+
*/
|
|
40
|
+
export interface SemanticUnavailableError {
|
|
41
|
+
readonly code: 'semantic-unavailable';
|
|
42
|
+
readonly message: string;
|
|
43
|
+
/** The intent's position in the agent's `retrieval`. */
|
|
44
|
+
readonly intent: number;
|
|
45
|
+
}
|
|
31
46
|
|
|
32
47
|
/**
|
|
33
48
|
* A turn being resumed can't be rebuilt from its snapshot:
|
|
@@ -27,7 +27,12 @@ import { parseJsonAnswer } from './structured-output.js';
|
|
|
27
27
|
* `halt` throws, so the run fails and the response never reaches the
|
|
28
28
|
* conversation; failures with any other action are stashed on `ctx` for
|
|
29
29
|
* `compose-result` to surface in `AgentTurnResult.violations`. Every
|
|
30
|
-
* failure emits a `guardrail.violated` event.
|
|
30
|
+
* failure emits a `guardrail.violated` event. A guardrail whose check
|
|
31
|
+
* couldn't run (no such check, a bad configuration) emits
|
|
32
|
+
* `guardrail.error` and gets a provenance node of its own; a `halt`
|
|
33
|
+
* guardrail's error fails the turn too (it fails closed, as a
|
|
34
|
+
* `guardrail-violation` whose `evaluationErrors` say why), and the step's
|
|
35
|
+
* output lists every error, so the journal shows it. Also records the
|
|
31
36
|
* response's `model-output` provenance node, which the guardrail checks
|
|
32
37
|
* link to.
|
|
33
38
|
*/
|
|
@@ -100,7 +105,7 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
|
|
|
100
105
|
judgeUsageSink(ctx, kctx),
|
|
101
106
|
);
|
|
102
107
|
throwIfJudgeCallsUnrecorded(outcomes);
|
|
103
|
-
const categorized = categorizeOutcomes(outcomes);
|
|
108
|
+
const categorized = categorizeOutcomes(outcomes, ctx.guardrails);
|
|
104
109
|
|
|
105
110
|
const allViolations = [...categorized.blocking, ...categorized.warnings, ...categorized.other];
|
|
106
111
|
for (const v of allViolations) {
|
|
@@ -132,8 +137,39 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
|
|
|
132
137
|
}
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
for (const e of categorized.errors) {
|
|
141
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
142
|
+
kind: 'guardrail.error',
|
|
143
|
+
guardrailId: e.guardrailId,
|
|
144
|
+
...(e.action !== undefined && { action: e.action }),
|
|
145
|
+
...(e.severity !== undefined && { severity: e.severity }),
|
|
146
|
+
code: e.code,
|
|
147
|
+
message: e.message,
|
|
148
|
+
});
|
|
149
|
+
if (ctx.provenance !== undefined) {
|
|
150
|
+
ctx.provenance.addNode({
|
|
151
|
+
id: `guardrail-check:${e.guardrailId}`,
|
|
152
|
+
kind: 'guardrail-check',
|
|
153
|
+
timestamp: evaluatedAt,
|
|
154
|
+
attributes: {
|
|
155
|
+
guardrailId: e.guardrailId,
|
|
156
|
+
...(e.action !== undefined && { action: e.action }),
|
|
157
|
+
...(e.severity !== undefined && { severity: e.severity }),
|
|
158
|
+
evaluated: false,
|
|
159
|
+
error: e.code,
|
|
160
|
+
reason: e.message,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
ctx.provenance.addEdge({
|
|
164
|
+
from: `guardrail-check:${e.guardrailId}`,
|
|
165
|
+
to: `model-output:${ctx.usage.steps}`,
|
|
166
|
+
kind: 'influenced-by',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (categorized.blocking.length > 0 || categorized.blockingErrors.length > 0) {
|
|
172
|
+
const message = describeBlockingViolations(categorized.blocking, categorized.blockingErrors);
|
|
137
173
|
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
138
174
|
kind: 'turn.failed',
|
|
139
175
|
conversationId: ctx.input.conversationId,
|
|
@@ -154,6 +190,13 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
|
|
|
154
190
|
blocking: categorized.blocking.length,
|
|
155
191
|
warnings: categorized.warnings.length,
|
|
156
192
|
other: categorized.other.length,
|
|
193
|
+
// The journal records the step's output: a check that couldn't run shows there.
|
|
194
|
+
errors: categorized.errors.map((e) => ({
|
|
195
|
+
guardrailId: e.guardrailId,
|
|
196
|
+
...(e.action !== undefined && { action: e.action }),
|
|
197
|
+
code: e.code,
|
|
198
|
+
message: e.message,
|
|
199
|
+
})),
|
|
157
200
|
};
|
|
158
201
|
};
|
|
159
202
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { AppendMessageInput } from '../conversation-binding.js';
|
|
5
|
+
import { runUserId } from '../remember.js';
|
|
6
|
+
import type { ConversationMessage } from '../types.js';
|
|
7
|
+
|
|
8
|
+
import type { TurnContext } from './context.js';
|
|
9
|
+
import { throwAgentTurnFailure } from './errors.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The messages a turn's prompt carries as history: the newest
|
|
13
|
+
* `conversationPolicy.historyLimit` before its user message, or all of
|
|
14
|
+
* them without a limit. The binding reads only those (`beforeSequence`,
|
|
15
|
+
* `last`); one that ignores the bounds returns every message, and the
|
|
16
|
+
* window is kept here.
|
|
17
|
+
*/
|
|
18
|
+
export async function readHistory(ctx: TurnContext): Promise<readonly ConversationMessage[]> {
|
|
19
|
+
const historyLimit = ctx.input.agent.conversationPolicy?.historyLimit;
|
|
20
|
+
const before = ctx.userMessage?.sequence;
|
|
21
|
+
const messages = await ctx.bindings.conversationBinding.readMessages({
|
|
22
|
+
tenantId: ctx.input.tenantId,
|
|
23
|
+
conversationId: ctx.input.conversationId,
|
|
24
|
+
...(before !== undefined && { beforeSequence: before }),
|
|
25
|
+
...(historyLimit !== undefined && { last: historyLimit }),
|
|
26
|
+
});
|
|
27
|
+
if (messages.kind === 'err') throwAgentTurnFailure(messages.error);
|
|
28
|
+
// The user message just appended is the composer's last element, not history.
|
|
29
|
+
const earlier = messages.value.filter((m) => before === undefined || m.sequence < before);
|
|
30
|
+
return historyLimit === undefined
|
|
31
|
+
? earlier
|
|
32
|
+
: earlier.slice(Math.max(0, earlier.length - historyLimit));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where the prompt's history starts: `same-conversation` recall reads
|
|
37
|
+
* only older messages. `undefined` when the prompt carries the whole
|
|
38
|
+
* conversation (no `historyLimit`).
|
|
39
|
+
*/
|
|
40
|
+
export async function historyStart(ctx: TurnContext): Promise<number | undefined> {
|
|
41
|
+
if (ctx.input.agent.conversationPolicy?.historyLimit === undefined) return undefined;
|
|
42
|
+
const history = await readHistory(ctx);
|
|
43
|
+
return history[0]?.sequence ?? ctx.userMessage?.sequence;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* What the conversation-recall index keeps with a message this turn
|
|
48
|
+
* appends (`AppendMessageInput.recall`): the user it acts for and the
|
|
49
|
+
* run's segment path, which the conversation row doesn't have.
|
|
50
|
+
*/
|
|
51
|
+
export function recallOf(ctx: TurnContext): NonNullable<AppendMessageInput['recall']> {
|
|
52
|
+
const userId = runUserId(ctx.input.principal);
|
|
53
|
+
return {
|
|
54
|
+
...(userId !== undefined && { userId: userId as unknown as string }),
|
|
55
|
+
...(ctx.input.segments !== undefined && { segments: ctx.input.segments }),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -11,6 +11,7 @@ import type { ConversationMessage } from '../types.js';
|
|
|
11
11
|
import type { TurnContext } from './context.js';
|
|
12
12
|
import { throwAgentTurnFailure } from './errors.js';
|
|
13
13
|
import { finalIteration } from './final-iteration.js';
|
|
14
|
+
import { recallOf } from './history.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Persist the turn's terminal assistant message (non-intermediate —
|
|
@@ -47,6 +48,7 @@ export function buildPersistFinalMessageHandler(ctx: TurnContext): NodeHandler {
|
|
|
47
48
|
role: 'agent',
|
|
48
49
|
content: assistantMsg.content ?? '',
|
|
49
50
|
actor: ctx.input.agent.id,
|
|
51
|
+
recall: recallOf(ctx),
|
|
50
52
|
});
|
|
51
53
|
if (persist.kind === 'err') throwAgentTurnFailure(persist.error);
|
|
52
54
|
finalMessage = persist.value;
|
|
@@ -8,6 +8,7 @@ import type { ConversationMessage } from '../types.js';
|
|
|
8
8
|
|
|
9
9
|
import type { TurnContext } from './context.js';
|
|
10
10
|
import { throwAgentTurnFailure } from './errors.js';
|
|
11
|
+
import { recallOf } from './history.js';
|
|
11
12
|
import { addInputNode } from './turn-provenance.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -44,6 +45,7 @@ export function buildPersistUserMessageHandler(ctx: TurnContext): NodeHandler {
|
|
|
44
45
|
role: 'user',
|
|
45
46
|
content: ctx.input.userMessage,
|
|
46
47
|
...(ctx.input.participantId !== undefined && { actor: ctx.input.participantId }),
|
|
48
|
+
recall: recallOf(ctx),
|
|
47
49
|
});
|
|
48
50
|
if (persisted.kind === 'err') throwAgentTurnFailure(persisted.error);
|
|
49
51
|
|
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
import type { Principal } from '@kindgi/authz';
|
|
5
5
|
import type { ProviderRegistry, TenantPolicy, UsageSink } from '@kindgi/capabilities';
|
|
6
6
|
import type { EmbeddingProviderRegistry } from '@kindgi/embedding';
|
|
7
|
-
import type { MemoryQueryBinding } from '@kindgi/memory';
|
|
7
|
+
import type { MemoryQueryBinding, MemoryRememberBinding } from '@kindgi/memory';
|
|
8
8
|
import type { PolicyRegistry } from '@kindgi/policy-contract';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ParentRunRef,
|
|
11
|
+
RunBinding,
|
|
12
|
+
RunIdempotencyKey,
|
|
13
|
+
RunReplayRef,
|
|
14
|
+
RunTriggerRef,
|
|
15
|
+
} from '@kindgi/runtime';
|
|
10
16
|
import type { ToolRegistry, ToolSecretRef } from '@kindgi/tools';
|
|
11
17
|
import type {
|
|
12
18
|
AgentVersionVia,
|
|
@@ -80,6 +86,10 @@ export interface InvokeAgentInput {
|
|
|
80
86
|
* is refused when that isn't wired), and only a read-only tool can run.
|
|
81
87
|
*/
|
|
82
88
|
readonly replay?: RunReplayRef;
|
|
89
|
+
/** Start the turn's run at most once per key (`RunIdempotencyKey`). */
|
|
90
|
+
readonly idempotencyKey?: RunIdempotencyKey;
|
|
91
|
+
/** Set when a trigger starts the turn; its run records it (`RunTriggerRef`). */
|
|
92
|
+
readonly trigger?: RunTriggerRef;
|
|
83
93
|
readonly participantId?: string;
|
|
84
94
|
readonly abortSignal?: AbortSignal;
|
|
85
95
|
/**
|
|
@@ -142,6 +152,12 @@ export interface InvokeAgentBindings extends GuardrailsBindings {
|
|
|
142
152
|
* touches a database client directly for memory operations.
|
|
143
153
|
*/
|
|
144
154
|
readonly memoryBinding: MemoryQueryBinding;
|
|
155
|
+
/**
|
|
156
|
+
* Where an agent that declares `memory.remember` stores what it
|
|
157
|
+
* remembers (the `kindgi_remember` tool). Absent: the tool still
|
|
158
|
+
* shows, and a call answers that this host can't remember.
|
|
159
|
+
*/
|
|
160
|
+
readonly memoryWriter?: MemoryRememberBinding;
|
|
145
161
|
/**
|
|
146
162
|
* Caller-plugged conversation store. Every open / get / list /
|
|
147
163
|
* close / delete / appendMessage / readMessages inside the agent
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - the messages the turn stored come back from the conversation, from
|
|
17
17
|
* the turn's user message (`persist-user-message` journals its
|
|
18
18
|
* sequence);
|
|
19
|
-
* - the retrieved facts come from `run-retrievals`' journaled output;
|
|
19
|
+
* - the retrieved facts and recalled messages come from `run-retrievals`' journaled output;
|
|
20
20
|
* - usage is summed from the journaled model calls, so the step and
|
|
21
21
|
* cost budgets count the whole turn.
|
|
22
22
|
*
|
|
@@ -44,7 +44,7 @@ import type { LoopContext } from '@kindgi/handler';
|
|
|
44
44
|
import { type JournalEntry, type ValueRecordedPayload, bodyStepKey } from '@kindgi/runtime';
|
|
45
45
|
import type { Timestamp } from '@kindgi/types';
|
|
46
46
|
|
|
47
|
-
import type { ConversationMessage, RetrievedFact } from '../types.js';
|
|
47
|
+
import type { ConversationMessage, RecalledMemory, RetrievedFact } from '../types.js';
|
|
48
48
|
import type { TurnContext } from './context.js';
|
|
49
49
|
import { throwAgentTurnFailure } from './errors.js';
|
|
50
50
|
import { readGateDecision } from './gate-decision.js';
|
|
@@ -142,11 +142,12 @@ export async function rehydrateTurnContext(
|
|
|
142
142
|
ctx.storedBeforePark = storedBeforePark(ctx, steps);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
const retrievals = outputOf<{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
145
|
+
const retrievals = outputOf<{
|
|
146
|
+
readonly retrieved?: readonly RetrievedFact[];
|
|
147
|
+
readonly recalled?: readonly RecalledMemory[];
|
|
148
|
+
}>(steps, 'run-retrievals');
|
|
149
149
|
if (retrievals?.retrieved !== undefined) ctx.retrieved = retrievals.retrieved;
|
|
150
|
+
if (retrievals?.recalled !== undefined) ctx.recalled = retrievals.recalled;
|
|
150
151
|
|
|
151
152
|
for (const s of steps.filter((s) => s.nodeId === 'model-call' && s.inLoop)) {
|
|
152
153
|
const out = s.output as {
|
|
@@ -167,7 +168,7 @@ export async function rehydrateTurnContext(
|
|
|
167
168
|
addModelWarnings(ctx, out.warnings);
|
|
168
169
|
}
|
|
169
170
|
ctx.toolApprovals = toolApprovalsOf(journal);
|
|
170
|
-
rebuildProvenance(ctx, steps, retrievals?.retrieved);
|
|
171
|
+
rebuildProvenance(ctx, steps, retrievals?.retrieved, retrievals?.recalled);
|
|
171
172
|
rehydrateReplay(ctx, journal);
|
|
172
173
|
return true;
|
|
173
174
|
}
|
|
@@ -236,11 +237,14 @@ function rebuildProvenance(
|
|
|
236
237
|
ctx: TurnContext,
|
|
237
238
|
steps: readonly StepRecord[],
|
|
238
239
|
retrieved: readonly RetrievedFact[] | undefined,
|
|
240
|
+
recalled: readonly RecalledMemory[] | undefined,
|
|
239
241
|
): void {
|
|
240
242
|
const input = ctx.userMessage;
|
|
241
243
|
if (ctx.provenance === undefined || input === undefined) return;
|
|
242
244
|
addInputNode(ctx.provenance, input);
|
|
243
|
-
if (retrieved !== undefined)
|
|
245
|
+
if (retrieved !== undefined) {
|
|
246
|
+
addRetrievalNodes(ctx.provenance, ctx.input.agent.retrieval, retrieved, input, recalled ?? []);
|
|
247
|
+
}
|
|
244
248
|
|
|
245
249
|
const storedByStep = new Map<number, readonly ConversationMessage[]>();
|
|
246
250
|
for (const s of steps.filter((s) => s.nodeId === 'dispatch-tools' && s.inLoop)) {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The built-in `kindgi_remember` tool, built for each turn of an
|
|
6
|
+
* agent that declares `memory.remember`. It dispatches like any tool (the
|
|
7
|
+
* agent's `hitl.tools` policy, replay and the tool error policy apply), but
|
|
8
|
+
* it isn't in the tool registry: its input schema lists the agent's own
|
|
9
|
+
* fact types, and its handler places the fact from the run, not from the
|
|
10
|
+
* model's arguments.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
|
|
15
|
+
import type { ModelToolDefinition } from '@kindgi/capabilities';
|
|
16
|
+
import { type Tool, type ToolContext, defineTool } from '@kindgi/tools';
|
|
17
|
+
import type { Timestamp, ToolId } from '@kindgi/types';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_REMEMBER_DAYS,
|
|
21
|
+
MAX_REMEMBER_KEY,
|
|
22
|
+
MAX_REMEMBER_TEXT,
|
|
23
|
+
REMEMBER_TOOL_ID,
|
|
24
|
+
REMEMBER_TOOL_VERSION,
|
|
25
|
+
rememberTarget,
|
|
26
|
+
reviewReasons,
|
|
27
|
+
runUserId,
|
|
28
|
+
} from '../remember.js';
|
|
29
|
+
import type { RememberPolicy } from '../types.js';
|
|
30
|
+
|
|
31
|
+
import type { TurnContext } from './context.js';
|
|
32
|
+
|
|
33
|
+
/** What the model sends. */
|
|
34
|
+
interface RememberArgs {
|
|
35
|
+
readonly type: string;
|
|
36
|
+
readonly content: string;
|
|
37
|
+
readonly key?: string;
|
|
38
|
+
readonly validUntil?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What the model reads back, and what the turn's provenance is rebuilt from. */
|
|
42
|
+
export interface RememberToolOutput {
|
|
43
|
+
/**
|
|
44
|
+
* `remembered`: stored and used from now on. `pending-review`: stored,
|
|
45
|
+
* but no read sees it until a person approves it. `not-remembered`:
|
|
46
|
+
* nothing was stored (`reason` says why).
|
|
47
|
+
*/
|
|
48
|
+
readonly status: 'remembered' | 'pending-review' | 'not-remembered';
|
|
49
|
+
readonly factId?: string;
|
|
50
|
+
readonly version?: number;
|
|
51
|
+
/** `superseded` when it replaced the value this agent kept for the same `key`. */
|
|
52
|
+
readonly outcome?: 'created' | 'superseded' | 'replayed';
|
|
53
|
+
readonly reason?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type TurnTools = NonNullable<TurnContext['tools']>;
|
|
57
|
+
|
|
58
|
+
/** The turn's tools, with `remember` added when the agent declares it. */
|
|
59
|
+
export function withRememberTool(ctx: TurnContext, tools: TurnTools): TurnTools {
|
|
60
|
+
const policy = ctx.input.agent.memory?.remember;
|
|
61
|
+
if (policy === undefined) return tools;
|
|
62
|
+
const tool = rememberTool(ctx, policy);
|
|
63
|
+
const definition: ModelToolDefinition = {
|
|
64
|
+
name: tool.id,
|
|
65
|
+
description: tool.description,
|
|
66
|
+
inputSchema: tool.input as Readonly<Record<string, unknown>>,
|
|
67
|
+
};
|
|
68
|
+
const byName = new Map(tools.byName);
|
|
69
|
+
byName.set(tool.id, {
|
|
70
|
+
tool,
|
|
71
|
+
resolvedVersion: REMEMBER_TOOL_VERSION,
|
|
72
|
+
requestedRange: REMEMBER_TOOL_VERSION,
|
|
73
|
+
});
|
|
74
|
+
return { definitions: [...tools.definitions, definition], byName };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function rememberTool(ctx: TurnContext, policy: RememberPolicy): Tool {
|
|
78
|
+
const defined = defineTool<RememberArgs, RememberToolOutput>({
|
|
79
|
+
id: REMEMBER_TOOL_ID as ToolId,
|
|
80
|
+
version: REMEMBER_TOOL_VERSION,
|
|
81
|
+
description: description(policy),
|
|
82
|
+
input: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
required: ['type', 'content'],
|
|
86
|
+
properties: {
|
|
87
|
+
type: { type: 'string', enum: [...policy.types], description: 'What kind of fact it is.' },
|
|
88
|
+
content: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
minLength: 1,
|
|
91
|
+
maxLength: MAX_REMEMBER_TEXT,
|
|
92
|
+
description: 'The fact, in one or two plain sentences.',
|
|
93
|
+
},
|
|
94
|
+
key: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
minLength: 1,
|
|
97
|
+
maxLength: MAX_REMEMBER_KEY,
|
|
98
|
+
description:
|
|
99
|
+
'Optional name for what it is about (e.g. "preferred-language"). A new fact with the same type and key replaces the one you remembered before.',
|
|
100
|
+
},
|
|
101
|
+
validUntil: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
format: 'date-time',
|
|
104
|
+
description: 'Optional: when it stops being true.',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
output: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
required: ['status'],
|
|
111
|
+
properties: {
|
|
112
|
+
status: { type: 'string', enum: ['remembered', 'pending-review', 'not-remembered'] },
|
|
113
|
+
factId: { type: 'string' },
|
|
114
|
+
version: { type: 'integer' },
|
|
115
|
+
outcome: { type: 'string', enum: ['created', 'superseded', 'replayed'] },
|
|
116
|
+
reason: { type: 'string' },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
mutating: true,
|
|
120
|
+
effects: [{ kind: 'writes', resource: 'memory:facts' }],
|
|
121
|
+
handler: (args, toolCtx) => remember(ctx, policy, args, toolCtx),
|
|
122
|
+
});
|
|
123
|
+
// The schemas are fixed here: a failure is a bug, not an input problem.
|
|
124
|
+
if (defined.kind === 'err') {
|
|
125
|
+
throw new Error(`the built-in remember tool failed to build: ${defined.error.message}`);
|
|
126
|
+
}
|
|
127
|
+
return defined.value as Tool;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function description(policy: RememberPolicy): string {
|
|
131
|
+
const whose =
|
|
132
|
+
policy.scope === 'same-user'
|
|
133
|
+
? 'the person you are talking with'
|
|
134
|
+
: policy.scope === 'same-conversation'
|
|
135
|
+
? 'this conversation'
|
|
136
|
+
: policy.scope === 'same-project'
|
|
137
|
+
? 'everyone in this project, after a person approves it'
|
|
138
|
+
: 'everyone, after a person approves it';
|
|
139
|
+
return `Use ${REMEMBER_TOOL_ID} to remember a fact for later conversations, for ${whose}. Use it for lasting facts and preferences you were told, not for this turn's working notes. It is stored as unverified.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function remember(
|
|
143
|
+
ctx: TurnContext,
|
|
144
|
+
policy: RememberPolicy,
|
|
145
|
+
args: RememberArgs,
|
|
146
|
+
toolCtx: ToolContext,
|
|
147
|
+
): Promise<RememberToolOutput> {
|
|
148
|
+
const writer = ctx.bindings.memoryWriter;
|
|
149
|
+
if (writer === undefined) {
|
|
150
|
+
return notRemembered('Not remembered: this runtime cannot store agent memories.');
|
|
151
|
+
}
|
|
152
|
+
const userId = runUserId(ctx.input.principal);
|
|
153
|
+
const participantId = ctx.conversation?.participantId ?? ctx.input.participantId;
|
|
154
|
+
const target = rememberTarget(policy.scope, {
|
|
155
|
+
tenantId: ctx.input.tenantId,
|
|
156
|
+
projectId: ctx.input.projectId,
|
|
157
|
+
conversationId: ctx.input.conversationId,
|
|
158
|
+
...(participantId !== undefined && { participantId }),
|
|
159
|
+
...(userId !== undefined && { userId }),
|
|
160
|
+
});
|
|
161
|
+
if (target.kind === 'refused') return notRemembered(target.reason);
|
|
162
|
+
const toolIds = [...(ctx.tools?.byName.keys() ?? [])].filter((id) => id !== REMEMBER_TOOL_ID);
|
|
163
|
+
const reasons = reviewReasons(policy, args.content, toolIds);
|
|
164
|
+
const written = await writer.remember({
|
|
165
|
+
tenantId: ctx.input.tenantId,
|
|
166
|
+
scope: target.scope,
|
|
167
|
+
type: args.type,
|
|
168
|
+
content: { text: args.content, ...(args.key !== undefined && { key: args.key }) },
|
|
169
|
+
subjects: target.subjects,
|
|
170
|
+
agent: {
|
|
171
|
+
id: ctx.input.agent.id as unknown as string,
|
|
172
|
+
version: ctx.input.agent.version as unknown as string,
|
|
173
|
+
},
|
|
174
|
+
generatedBy: {
|
|
175
|
+
runId: toolCtx.runId as unknown as string,
|
|
176
|
+
stepId: `step:${ctx.usage.steps}`,
|
|
177
|
+
// Dispatch always passes the call's id; without one, the write can't
|
|
178
|
+
// be matched to an earlier attempt, but never to another call's.
|
|
179
|
+
toolCallId: toolCtx.requestId ?? randomUUID(),
|
|
180
|
+
},
|
|
181
|
+
keepDays: policy.keepDays ?? DEFAULT_REMEMBER_DAYS,
|
|
182
|
+
...(args.validUntil !== undefined && { validUntil: args.validUntil as Timestamp }),
|
|
183
|
+
...(reasons.length > 0 && { review: { reasons } }),
|
|
184
|
+
});
|
|
185
|
+
// The store failed: the turn's tool error policy decides what happens.
|
|
186
|
+
if (written.kind === 'err') throw new Error(written.error.message);
|
|
187
|
+
const { fact, outcome } = written.value;
|
|
188
|
+
const pending = fact.review === 'pending';
|
|
189
|
+
return {
|
|
190
|
+
status: pending ? 'pending-review' : 'remembered',
|
|
191
|
+
factId: fact.id as unknown as string,
|
|
192
|
+
version: fact.version,
|
|
193
|
+
outcome,
|
|
194
|
+
...(pending && { reason: 'A person reviews it before it is used.' }),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function notRemembered(reason: string): RememberToolOutput {
|
|
199
|
+
return { status: 'not-remembered', reason };
|
|
200
|
+
}
|