@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,101 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The reviewer's answer at an agent turn's approval gates: the tool-call
|
|
6
|
+
* gate (`dispatch-tools`) and the session gate (`setup`). A gate parks the
|
|
7
|
+
* turn on a waitpoint; the approvals route resumes it with the decision
|
|
8
|
+
* (`GateDecisionValue`): approve or reject, the reviewer's rationale, and
|
|
9
|
+
* who decided which approval.
|
|
10
|
+
*
|
|
11
|
+
* A gate fails closed. Only an explicit approve lets the tool run (or the
|
|
12
|
+
* session go on). Anything else blocks: a reject, and also a resume value
|
|
13
|
+
* that isn't a decision at all (a `value` that replaced it, a malformed
|
|
14
|
+
* payload), so no answer can be read as consent by omission.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The approval subject an agent's tool-call gate parks on. */
|
|
18
|
+
export const TOOL_CALL_GATE_SUBJECT = 'tool-call:pending';
|
|
19
|
+
|
|
20
|
+
/** The approval subject an agent's session gate (`afterTurns`) parks on. */
|
|
21
|
+
export const SESSION_GATE_SUBJECT = 'agent-turn:session-hitl-gate';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The approval subjects an agent turn parks on. Their resume value is the
|
|
25
|
+
* reviewer's decision and nothing else, so the approvals route takes no
|
|
26
|
+
* `value` for them.
|
|
27
|
+
*/
|
|
28
|
+
export const AGENT_GATE_SUBJECTS: ReadonlySet<string> = new Set([
|
|
29
|
+
TOOL_CALL_GATE_SUBJECT,
|
|
30
|
+
SESSION_GATE_SUBJECT,
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A gate's resume value: the reviewer's decision, as the approvals route
|
|
35
|
+
* (and the runtime, delivering a decision whose delivery was lost) completes
|
|
36
|
+
* the gate's waitpoint with it. The run's journal keeps it as it came.
|
|
37
|
+
*/
|
|
38
|
+
export interface GateDecisionValue {
|
|
39
|
+
readonly decided: 'approve' | 'reject';
|
|
40
|
+
readonly rationale?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Who decided, as an actor: `user:<userId>`, the reviewer's user. Absent
|
|
43
|
+
* from values written before it was recorded.
|
|
44
|
+
*/
|
|
45
|
+
readonly decidedBy?: string;
|
|
46
|
+
/** The approval decided. Absent from values written before it was recorded. */
|
|
47
|
+
readonly approvalId?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Who decided a gate, and which approval, when its value says. */
|
|
51
|
+
interface GateDecider {
|
|
52
|
+
readonly decidedBy?: string;
|
|
53
|
+
readonly approvalId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What a gate's resume value decides. */
|
|
57
|
+
export type GateDecision = GateDecider &
|
|
58
|
+
(
|
|
59
|
+
| { readonly approved: true }
|
|
60
|
+
| {
|
|
61
|
+
readonly approved: false;
|
|
62
|
+
/** `rejected`: the reviewer said no. `unreadable`: the answer wasn't a decision. */
|
|
63
|
+
readonly reason: 'rejected' | 'unreadable';
|
|
64
|
+
/** The reviewer's rationale, or why an unreadable answer blocks. */
|
|
65
|
+
readonly rationale: string | undefined;
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/** Why a gate blocks on an answer that isn't a decision. */
|
|
70
|
+
export const UNREADABLE_DECISION =
|
|
71
|
+
"the approval's answer wasn't a decision (approve or reject), so it counts as a rejection";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The decision in a gate's resume value. Fails closed: only
|
|
75
|
+
* `{ decided: 'approve' }` approves.
|
|
76
|
+
*/
|
|
77
|
+
export function readGateDecision(value: unknown): GateDecision {
|
|
78
|
+
if (typeof value !== 'object' || value === null) {
|
|
79
|
+
return { approved: false, reason: 'unreadable', rationale: UNREADABLE_DECISION };
|
|
80
|
+
}
|
|
81
|
+
const { decided, rationale, decidedBy, approvalId } = value as {
|
|
82
|
+
decided?: unknown;
|
|
83
|
+
rationale?: unknown;
|
|
84
|
+
decidedBy?: unknown;
|
|
85
|
+
approvalId?: unknown;
|
|
86
|
+
};
|
|
87
|
+
const decider: GateDecider = {
|
|
88
|
+
...(typeof decidedBy === 'string' && { decidedBy }),
|
|
89
|
+
...(typeof approvalId === 'string' && { approvalId }),
|
|
90
|
+
};
|
|
91
|
+
if (decided === 'approve') return { approved: true, ...decider };
|
|
92
|
+
if (decided === 'reject') {
|
|
93
|
+
return {
|
|
94
|
+
approved: false,
|
|
95
|
+
reason: 'rejected',
|
|
96
|
+
rationale: typeof rationale === 'string' ? rationale : undefined,
|
|
97
|
+
...decider,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { approved: false, reason: 'unreadable', rationale: UNREADABLE_DECISION };
|
|
101
|
+
}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type ModelCallInput,
|
|
8
|
+
type ModelMessage,
|
|
9
|
+
type ModelProvider,
|
|
10
|
+
type ModelUsageRecord,
|
|
11
|
+
recordModelUsage,
|
|
12
|
+
} from '@kindgi/capabilities';
|
|
13
|
+
import { attemptsOf } from '@kindgi/capabilities/attempts';
|
|
14
|
+
import type { NodeContext, NodeHandler } from '@kindgi/handler';
|
|
15
|
+
import type { Timestamp } from '@kindgi/types';
|
|
6
16
|
|
|
7
17
|
import { emitTurnEvent } from '../streaming.js';
|
|
8
18
|
|
|
9
19
|
import type { AgentTurnIterationOutput, TurnContext } from './context.js';
|
|
10
20
|
import { throwAgentTurnFailure } from './errors.js';
|
|
21
|
+
import { replayTag } from './replay.js';
|
|
22
|
+
import { addModelCallNode } from './turn-provenance.js';
|
|
11
23
|
|
|
12
24
|
/**
|
|
13
25
|
* Loop-body node #1. Invoke the model with the current
|
|
@@ -15,6 +27,16 @@ import { throwAgentTurnFailure } from './errors.js';
|
|
|
15
27
|
* `model.call.completed`, records provenance for the model-call node,
|
|
16
28
|
* and accumulates usage on `ctx.usage`.
|
|
17
29
|
*
|
|
30
|
+
* Every call is recorded in the usage sink (`InvokeAgentBindings.usage`,
|
|
31
|
+
* the runtime's cost ledger) before the step goes on, a call that threw
|
|
32
|
+
* included. A sink that fails is tried again (a record is idempotent by
|
|
33
|
+
* call id); one that still fails fails the step when the call succeeded,
|
|
34
|
+
* so no answered call is left unrecorded. A call that failed keeps its
|
|
35
|
+
* own failure, which says it couldn't be recorded too. The call's id goes into
|
|
36
|
+
* the step's output and its provenance node, which keeps the call's
|
|
37
|
+
* identity (provider, model) while its usage lives in the ledger. A dry
|
|
38
|
+
* run spends nothing and records nothing.
|
|
39
|
+
*
|
|
18
40
|
* Output is a partial `AgentTurnIterationOutput` — `dispatch-tools`
|
|
19
41
|
* receives it and finishes composing the iteration output.
|
|
20
42
|
*/
|
|
@@ -31,6 +53,8 @@ export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
|
|
|
31
53
|
const nextMessages: readonly ModelMessage[] = shaped?.nextMessages ?? [];
|
|
32
54
|
|
|
33
55
|
ctx.usage.steps += 1;
|
|
56
|
+
const step = ctx.usage.steps;
|
|
57
|
+
const callId = randomUUID();
|
|
34
58
|
|
|
35
59
|
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
36
60
|
kind: 'model.call.started',
|
|
@@ -60,28 +84,17 @@ export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
|
|
|
60
84
|
...(ctx.tools.definitions.length > 0 && {
|
|
61
85
|
tools: ctx.tools.definitions,
|
|
62
86
|
}),
|
|
87
|
+
...modelSettingsOf(ctx),
|
|
63
88
|
abortSignal: ctx.turnAbort.signal,
|
|
64
89
|
};
|
|
65
90
|
|
|
91
|
+
const startedAt = Date.now();
|
|
66
92
|
try {
|
|
67
93
|
callResult = await ctx.provider.invoke(callInput);
|
|
68
94
|
} catch (cause) {
|
|
69
|
-
|
|
70
|
-
throwAgentTurnFailure({
|
|
71
|
-
code: 'agent-turn-aborted',
|
|
72
|
-
message: `Agent turn aborted: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
73
|
-
reason: ctx.abortReason ?? 'timeout',
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
// The provider's own words (a 401's "invalid x-api-key", a 429)
|
|
77
|
-
// are what the caller needs; they're in `cause` too, but callers
|
|
78
|
-
// show `message`.
|
|
79
|
-
throwAgentTurnFailure({
|
|
80
|
-
code: 'model-invocation-failed',
|
|
81
|
-
message: `Model call to ${ctx.provider.metadata.id} (${ctx.model.name}) failed: ${describeCause(cause)}`,
|
|
82
|
-
cause,
|
|
83
|
-
});
|
|
95
|
+
return await failCall(ctx, kctx, { callId, step, cause, startedAt });
|
|
84
96
|
}
|
|
97
|
+
await recordAnswer(ctx, kctx, callId, step, callResult);
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
ctx.usage.promptTokens += callResult.usage.promptTokens;
|
|
@@ -100,32 +113,27 @@ export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
|
|
|
100
113
|
});
|
|
101
114
|
|
|
102
115
|
if (ctx.provenance !== undefined && ctx.userMessage !== undefined) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
kind: 'model-call',
|
|
107
|
-
timestamp: new Date().toISOString() as never,
|
|
108
|
-
modelVersion: `${callResult.provider.id}/${callResult.provider.model}`,
|
|
109
|
-
attributes: {
|
|
116
|
+
addModelCallNode(
|
|
117
|
+
ctx.provenance,
|
|
118
|
+
{
|
|
110
119
|
step: ctx.usage.steps,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
costUsd: callResult.costUsd,
|
|
120
|
+
callId,
|
|
121
|
+
provider: callResult.provider,
|
|
114
122
|
finishReason: callResult.finishReason,
|
|
123
|
+
at: new Date().toISOString() as Timestamp,
|
|
115
124
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
to: `input:${ctx.userMessage.sequence}`,
|
|
120
|
-
kind: 'caused-by',
|
|
121
|
-
});
|
|
125
|
+
ctx.userMessage.sequence,
|
|
126
|
+
ctx.toolResultIds ?? [],
|
|
127
|
+
);
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
// Partial iteration output — `dispatch-tools` finishes it.
|
|
125
131
|
const partial: Omit<AgentTurnIterationOutput, 'iterationAppended' | 'finishedTurn'> & {
|
|
126
132
|
readonly step: number;
|
|
133
|
+
readonly callId: string;
|
|
127
134
|
} = {
|
|
128
135
|
step: ctx.usage.steps,
|
|
136
|
+
callId,
|
|
129
137
|
finishReason: callResult.finishReason,
|
|
130
138
|
message: callResult.message,
|
|
131
139
|
iterationUsage: {
|
|
@@ -140,6 +148,117 @@ export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
|
|
|
140
148
|
};
|
|
141
149
|
}
|
|
142
150
|
|
|
151
|
+
/**
|
|
152
|
+
* A call that answered: record it before the step goes on. A sink that
|
|
153
|
+
* still fails after its retries fails the step: an answered call isn't
|
|
154
|
+
* left unrecorded.
|
|
155
|
+
*/
|
|
156
|
+
async function recordAnswer(
|
|
157
|
+
ctx: TurnContext,
|
|
158
|
+
kctx: NodeContext,
|
|
159
|
+
callId: string,
|
|
160
|
+
step: number,
|
|
161
|
+
answer: Awaited<ReturnType<ModelProvider['invoke']>>,
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
const { message: _answer, ...result } = answer;
|
|
164
|
+
const unrecorded = await recordCall(ctx, kctx, {
|
|
165
|
+
callId,
|
|
166
|
+
step,
|
|
167
|
+
status: 'ok',
|
|
168
|
+
result,
|
|
169
|
+
durationMs: answer.durationMs,
|
|
170
|
+
});
|
|
171
|
+
if (unrecorded !== undefined) {
|
|
172
|
+
throwAgentTurnFailure({
|
|
173
|
+
code: 'persistence-error',
|
|
174
|
+
message: `The model call couldn't be recorded: ${describeCause(unrecorded)}`,
|
|
175
|
+
cause: unrecorded,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A call that threw: record it, then fail the step with the call's own
|
|
182
|
+
* failure (aborted, or the provider's words). A record that failed too
|
|
183
|
+
* is said after it.
|
|
184
|
+
*/
|
|
185
|
+
async function failCall(
|
|
186
|
+
ctx: TurnContext,
|
|
187
|
+
kctx: NodeContext,
|
|
188
|
+
failed: {
|
|
189
|
+
readonly callId: string;
|
|
190
|
+
readonly step: number;
|
|
191
|
+
readonly cause: unknown;
|
|
192
|
+
readonly startedAt: number;
|
|
193
|
+
},
|
|
194
|
+
): Promise<never> {
|
|
195
|
+
const { cause } = failed;
|
|
196
|
+
const attempts = attemptsOf(cause);
|
|
197
|
+
const unrecorded = await recordCall(ctx, kctx, {
|
|
198
|
+
callId: failed.callId,
|
|
199
|
+
step: failed.step,
|
|
200
|
+
status: 'failed',
|
|
201
|
+
error: { message: describeCause(cause), ...(attempts !== undefined && { attempts }) },
|
|
202
|
+
durationMs: Date.now() - failed.startedAt,
|
|
203
|
+
});
|
|
204
|
+
const andUnrecorded =
|
|
205
|
+
unrecorded === undefined
|
|
206
|
+
? ''
|
|
207
|
+
: ` (and the failed call couldn't be recorded: ${describeCause(unrecorded)})`;
|
|
208
|
+
if (ctx.turnAbort.signal.aborted) {
|
|
209
|
+
throwAgentTurnFailure({
|
|
210
|
+
code: 'agent-turn-aborted',
|
|
211
|
+
message: `Agent turn aborted: ${cause instanceof Error ? cause.message : String(cause)}${andUnrecorded}`,
|
|
212
|
+
reason: ctx.abortReason ?? 'timeout',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// The provider's own words (a 401's "invalid x-api-key", a 429) are
|
|
216
|
+
// what the caller needs; they're in `cause` too, but callers show
|
|
217
|
+
// `message`.
|
|
218
|
+
throwAgentTurnFailure({
|
|
219
|
+
code: 'model-invocation-failed',
|
|
220
|
+
message: `Model call to ${ctx.provider?.metadata.id} (${ctx.model?.name}) failed: ${describeCause(cause)}${andUnrecorded}`,
|
|
221
|
+
cause,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** What the call came to, for `recordCall`. */
|
|
226
|
+
type CallOutcome = Pick<
|
|
227
|
+
ModelUsageRecord,
|
|
228
|
+
'callId' | 'step' | 'status' | 'result' | 'error' | 'durationMs'
|
|
229
|
+
>;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Record a model call in the usage sink, when the turn has one, trying
|
|
233
|
+
* a failing sink again. Resolves with the sink's last failure when it
|
|
234
|
+
* couldn't record; the caller decides what that means for the step.
|
|
235
|
+
*/
|
|
236
|
+
async function recordCall(
|
|
237
|
+
ctx: TurnContext,
|
|
238
|
+
kctx: NodeContext,
|
|
239
|
+
call: CallOutcome,
|
|
240
|
+
): Promise<unknown | undefined> {
|
|
241
|
+
const sink = ctx.bindings.usage;
|
|
242
|
+
if (sink === undefined || ctx.provider === undefined || ctx.model === undefined) return;
|
|
243
|
+
const { input } = ctx;
|
|
244
|
+
const recorded = await recordModelUsage(sink, {
|
|
245
|
+
...call,
|
|
246
|
+
tenantId: input.tenantId,
|
|
247
|
+
projectId: input.projectId,
|
|
248
|
+
runId: kctx.runId,
|
|
249
|
+
agentId: input.agent.id as unknown as string,
|
|
250
|
+
agentVersion: input.agent.version,
|
|
251
|
+
conversationId: input.conversationId as unknown as string,
|
|
252
|
+
nodeId: kctx.nodeId as unknown as string,
|
|
253
|
+
providerId: ctx.provider.metadata.id,
|
|
254
|
+
model: ctx.model.name,
|
|
255
|
+
...(ctx.provider.metadata.fallback === true && { fallback: true }),
|
|
256
|
+
...(input.replay !== undefined && { replay: replayTag(input.replay) }),
|
|
257
|
+
occurredAt: new Date().toISOString(),
|
|
258
|
+
});
|
|
259
|
+
return recorded.kind === 'err' ? (recorded.error ?? new Error('no detail')) : undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
143
262
|
/** Longest cause text a failure message carries. */
|
|
144
263
|
const MAX_CAUSE_CHARS = 500;
|
|
145
264
|
|
|
@@ -149,3 +268,14 @@ function describeCause(cause: unknown): string {
|
|
|
149
268
|
if (text.length === 0) return cause instanceof Error ? cause.name : 'no detail';
|
|
150
269
|
return text.length > MAX_CAUSE_CHARS ? `${text.slice(0, MAX_CAUSE_CHARS - 1)}…` : text;
|
|
151
270
|
}
|
|
271
|
+
|
|
272
|
+
/** The pinned model-settings block's values, as model-call fields; none when it has none. */
|
|
273
|
+
function modelSettingsOf(
|
|
274
|
+
ctx: TurnContext,
|
|
275
|
+
): Pick<ModelCallInput, 'temperature' | 'maxOutputTokens'> {
|
|
276
|
+
const settings = ctx.blocks?.modelSettings;
|
|
277
|
+
return {
|
|
278
|
+
...(settings?.temperature !== undefined && { temperature: settings.temperature }),
|
|
279
|
+
...(settings?.maxOutputTokens !== undefined && { maxOutputTokens: settings.maxOutputTokens }),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -28,7 +28,9 @@ export function buildPersistProvenanceHandler(ctx: TurnContext): NodeHandler {
|
|
|
28
28
|
// want).
|
|
29
29
|
return { persisted: false };
|
|
30
30
|
}
|
|
31
|
-
const persisted = await persistProvenance(ctx.provenance, ctx.provenanceBindings
|
|
31
|
+
const persisted = await persistProvenance(ctx.provenance, ctx.provenanceBindings, {
|
|
32
|
+
projectId: ctx.input.projectId,
|
|
33
|
+
});
|
|
32
34
|
if (persisted.kind === 'ok') {
|
|
33
35
|
ctx.persistedProvenance = persisted.value;
|
|
34
36
|
return { persisted: true };
|
|
@@ -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 { addInputNode } from './turn-provenance.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Persist the caller's user message BEFORE the model is called. A
|
|
@@ -33,14 +34,7 @@ export function buildPersistUserMessageHandler(ctx: TurnContext): NodeHandler {
|
|
|
33
34
|
ctx.userMessage = synthetic;
|
|
34
35
|
ctx.appended.push(synthetic);
|
|
35
36
|
|
|
36
|
-
if (ctx.provenance !== undefined)
|
|
37
|
-
ctx.provenance.addNode({
|
|
38
|
-
id: `input:${synthetic.sequence}`,
|
|
39
|
-
kind: 'input',
|
|
40
|
-
timestamp: synthetic.createdAt,
|
|
41
|
-
...(synthetic.actor !== undefined && { actor: synthetic.actor }),
|
|
42
|
-
});
|
|
43
|
-
}
|
|
37
|
+
if (ctx.provenance !== undefined) addInputNode(ctx.provenance, synthetic);
|
|
44
38
|
return { sequence: synthetic.sequence };
|
|
45
39
|
}
|
|
46
40
|
|
|
@@ -56,14 +50,7 @@ export function buildPersistUserMessageHandler(ctx: TurnContext): NodeHandler {
|
|
|
56
50
|
ctx.userMessage = persisted.value;
|
|
57
51
|
ctx.appended.push(persisted.value);
|
|
58
52
|
|
|
59
|
-
if (ctx.provenance !== undefined)
|
|
60
|
-
ctx.provenance.addNode({
|
|
61
|
-
id: `input:${persisted.value.sequence}`,
|
|
62
|
-
kind: 'input',
|
|
63
|
-
timestamp: persisted.value.createdAt,
|
|
64
|
-
...(persisted.value.actor !== undefined && { actor: persisted.value.actor }),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
53
|
+
if (ctx.provenance !== undefined) addInputNode(ctx.provenance, persisted.value);
|
|
67
54
|
|
|
68
55
|
return { sequence: persisted.value.sequence };
|
|
69
56
|
};
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
4
|
import type { Principal } from '@kindgi/authz';
|
|
5
|
-
import type { ProviderRegistry, TenantPolicy } from '@kindgi/capabilities';
|
|
5
|
+
import type { ProviderRegistry, TenantPolicy, UsageSink } from '@kindgi/capabilities';
|
|
6
6
|
import type { EmbeddingProviderRegistry } from '@kindgi/embedding';
|
|
7
7
|
import type { MemoryQueryBinding } from '@kindgi/memory';
|
|
8
8
|
import type { PolicyRegistry } from '@kindgi/policy-contract';
|
|
9
|
-
import type { ParentRunRef, RunBinding } from '@kindgi/runtime';
|
|
9
|
+
import type { ParentRunRef, RunBinding, RunReplayRef } from '@kindgi/runtime';
|
|
10
10
|
import type { ToolRegistry, ToolSecretRef } from '@kindgi/tools';
|
|
11
|
-
import type { ProjectId, ProvenanceId, RunId, TenantId, Timestamp } from '@kindgi/types';
|
|
11
|
+
import type { OrgId, ProjectId, ProvenanceId, RunId, TenantId, Timestamp } from '@kindgi/types';
|
|
12
12
|
|
|
13
|
+
import type { BlockReader } from '../blocks.js';
|
|
13
14
|
import type { ConversationBinding } from '../conversation-binding.js';
|
|
14
15
|
import type { GuardrailsBindings } from '../guardrails-gate.js';
|
|
15
16
|
import type { ProvenanceBindings } from '../provenance-emit.js';
|
|
@@ -17,6 +18,8 @@ import type { RunSnapshotBinding } from '../run-snapshot-binding.js';
|
|
|
17
18
|
import type { OnTurnEvent } from '../streaming.js';
|
|
18
19
|
import type { Agent, ConversationId } from '../types.js';
|
|
19
20
|
|
|
21
|
+
import type { ReplayBinding } from './replay.js';
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* The user-facing input to `invokeAgent`. Split out from `invoke.ts` so
|
|
22
25
|
* the handler modules can depend on the shape without pulling in the
|
|
@@ -32,6 +35,12 @@ export interface InvokeAgentInput {
|
|
|
32
35
|
* at the caller layer.
|
|
33
36
|
*/
|
|
34
37
|
readonly projectId: ProjectId;
|
|
38
|
+
/**
|
|
39
|
+
* The project's org, when it belongs to one: the runtime resolves it
|
|
40
|
+
* from the project, never from input. The turn's tools get it as
|
|
41
|
+
* `ToolContext.orgId`, its guardrails as `trace.orgId`.
|
|
42
|
+
*/
|
|
43
|
+
readonly orgId?: OrgId;
|
|
35
44
|
readonly agent: Agent;
|
|
36
45
|
readonly conversationId: ConversationId;
|
|
37
46
|
readonly userMessage: string;
|
|
@@ -48,6 +57,13 @@ export interface InvokeAgentInput {
|
|
|
48
57
|
* step. Recorded on the turn's run, so the flow can find its child.
|
|
49
58
|
*/
|
|
50
59
|
readonly parent?: ParentRunRef;
|
|
60
|
+
/**
|
|
61
|
+
* Marks the turn as a replay: an eval run re-running a past run (`of`)
|
|
62
|
+
* on this agent version. Recorded on the turn's run and in its snapshot.
|
|
63
|
+
* Its tool calls are decided by `InvokeAgentBindings.replay` (every call
|
|
64
|
+
* is refused when that isn't wired), and only a read-only tool can run.
|
|
65
|
+
*/
|
|
66
|
+
readonly replay?: RunReplayRef;
|
|
51
67
|
readonly participantId?: string;
|
|
52
68
|
readonly abortSignal?: AbortSignal;
|
|
53
69
|
/**
|
|
@@ -95,6 +111,12 @@ export interface InvokeAgentBindings extends GuardrailsBindings {
|
|
|
95
111
|
* sees another tenant's tools.
|
|
96
112
|
*/
|
|
97
113
|
readonly toolRegistry: ToolRegistry;
|
|
114
|
+
/**
|
|
115
|
+
* Data blocks (prompts and settings). Setup loads the blocks the agent
|
|
116
|
+
* references at their pinned versions. Optional: an agent that
|
|
117
|
+
* references blocks fails its turn (`block-unresolvable`) without it.
|
|
118
|
+
*/
|
|
119
|
+
readonly blockReader?: BlockReader;
|
|
98
120
|
/**
|
|
99
121
|
* Caller-plugged data-access surface for memory reads.
|
|
100
122
|
* The Kindgi runtime supplies a Postgres-backed implementation;
|
|
@@ -153,6 +175,12 @@ export interface InvokeAgentBindings extends GuardrailsBindings {
|
|
|
153
175
|
readonly onEvent?: OnTurnEvent;
|
|
154
176
|
readonly provenance?: ProvenanceBindings;
|
|
155
177
|
readonly hitl?: HitlBindings;
|
|
178
|
+
/**
|
|
179
|
+
* Where the turn records each model call (the runtime's cost ledger):
|
|
180
|
+
* its usage, provider, model, run, agent and step, before the step
|
|
181
|
+
* that made it goes on. Absent: calls aren't recorded.
|
|
182
|
+
*/
|
|
183
|
+
readonly usage?: UsageSink;
|
|
156
184
|
/**
|
|
157
185
|
* Declarative HTTP tools: optional secret resolver populated
|
|
158
186
|
* from the deployment's tenant-scoped `SecretBinding`. When present,
|
|
@@ -163,6 +191,11 @@ export interface InvokeAgentBindings extends GuardrailsBindings {
|
|
|
163
191
|
* unaffected.
|
|
164
192
|
*/
|
|
165
193
|
readonly resolveSecret?: (ref: ToolSecretRef) => Promise<string>;
|
|
194
|
+
/**
|
|
195
|
+
* How replay turns (`InvokeAgentInput.replay`) decide their tool calls,
|
|
196
|
+
* retrievals and session approval. Consulted only for a replay turn.
|
|
197
|
+
*/
|
|
198
|
+
readonly replay?: ReplayBinding;
|
|
166
199
|
}
|
|
167
200
|
|
|
168
201
|
export interface HitlBindings {
|
|
@@ -171,6 +204,8 @@ export interface HitlBindings {
|
|
|
171
204
|
|
|
172
205
|
export interface HitlEnqueueInput {
|
|
173
206
|
readonly tenantId: TenantId;
|
|
207
|
+
/** The turn's project: approval lists filter by it. */
|
|
208
|
+
readonly projectId?: ProjectId;
|
|
174
209
|
readonly subjectKind: string;
|
|
175
210
|
readonly subjectRef: Readonly<Record<string, unknown>>;
|
|
176
211
|
readonly requiredRole?: 'standard' | 'senior' | 'admin';
|