@kindgi/agents 0.0.0-bootstrap.0 → 0.1.1
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/LICENSE +201 -0
- package/README.md +131 -2
- package/dist/agent-turn-flow.d.ts +57 -0
- package/dist/agent-turn-flow.d.ts.map +1 -0
- package/dist/agent-turn-flow.js +172 -0
- package/dist/agent-turn-flow.js.map +1 -0
- package/dist/conversation-binding.d.ts +111 -0
- package/dist/conversation-binding.d.ts.map +1 -0
- package/dist/conversation-binding.js +4 -0
- package/dist/conversation-binding.js.map +1 -0
- package/dist/define.d.ts +180 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +361 -0
- package/dist/define.js.map +1 -0
- package/dist/errors.d.ts +62 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/guardrails-gate.d.ts +169 -0
- package/dist/guardrails-gate.d.ts.map +1 -0
- package/dist/guardrails-gate.js +202 -0
- package/dist/guardrails-gate.js.map +1 -0
- package/dist/handlers/budget-check.d.ts +22 -0
- package/dist/handlers/budget-check.d.ts.map +1 -0
- package/dist/handlers/budget-check.js +109 -0
- package/dist/handlers/budget-check.js.map +1 -0
- package/dist/handlers/build-initial-messages.d.ts +17 -0
- package/dist/handlers/build-initial-messages.d.ts.map +1 -0
- package/dist/handlers/build-initial-messages.js +86 -0
- package/dist/handlers/build-initial-messages.js.map +1 -0
- package/dist/handlers/compose-result.d.ts +10 -0
- package/dist/handlers/compose-result.d.ts.map +1 -0
- package/dist/handlers/compose-result.js +79 -0
- package/dist/handlers/compose-result.js.map +1 -0
- package/dist/handlers/constants.d.ts +8 -0
- package/dist/handlers/constants.d.ts.map +1 -0
- package/dist/handlers/constants.js +10 -0
- package/dist/handlers/constants.js.map +1 -0
- package/dist/handlers/context.d.ts +210 -0
- package/dist/handlers/context.d.ts.map +1 -0
- package/dist/handlers/context.js +4 -0
- package/dist/handlers/context.js.map +1 -0
- package/dist/handlers/dispatch-tools.d.ts +15 -0
- package/dist/handlers/dispatch-tools.d.ts.map +1 -0
- package/dist/handlers/dispatch-tools.js +511 -0
- package/dist/handlers/dispatch-tools.js.map +1 -0
- package/dist/handlers/errors.d.ts +133 -0
- package/dist/handlers/errors.d.ts.map +1 -0
- package/dist/handlers/errors.js +134 -0
- package/dist/handlers/errors.js.map +1 -0
- package/dist/handlers/evaluate-guardrails.d.ts +14 -0
- package/dist/handlers/evaluate-guardrails.d.ts.map +1 -0
- package/dist/handlers/evaluate-guardrails.js +128 -0
- package/dist/handlers/evaluate-guardrails.js.map +1 -0
- package/dist/handlers/final-iteration.d.ts +7 -0
- package/dist/handlers/final-iteration.d.ts.map +1 -0
- package/dist/handlers/final-iteration.js +26 -0
- package/dist/handlers/final-iteration.js.map +1 -0
- package/dist/handlers/index.d.ts +5 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +41 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/model-call.d.ts +13 -0
- package/dist/handlers/model-call.d.ts.map +1 -0
- package/dist/handlers/model-call.js +136 -0
- package/dist/handlers/model-call.js.map +1 -0
- package/dist/handlers/persist-final-message.d.ts +14 -0
- package/dist/handlers/persist-final-message.d.ts.map +1 -0
- package/dist/handlers/persist-final-message.js +54 -0
- package/dist/handlers/persist-final-message.js.map +1 -0
- package/dist/handlers/persist-provenance.d.ts +12 -0
- package/dist/handlers/persist-provenance.d.ts.map +1 -0
- package/dist/handlers/persist-provenance.js +34 -0
- package/dist/handlers/persist-provenance.js.map +1 -0
- package/dist/handlers/persist-user-message.d.ts +15 -0
- package/dist/handlers/persist-user-message.d.ts.map +1 -0
- package/dist/handlers/persist-user-message.js +59 -0
- package/dist/handlers/persist-user-message.js.map +1 -0
- package/dist/handlers/public-types.d.ts +201 -0
- package/dist/handlers/public-types.d.ts.map +1 -0
- package/dist/handlers/public-types.js +4 -0
- package/dist/handlers/public-types.js.map +1 -0
- package/dist/handlers/rehydrate.d.ts +8 -0
- package/dist/handlers/rehydrate.d.ts.map +1 -0
- package/dist/handlers/rehydrate.js +94 -0
- package/dist/handlers/rehydrate.js.map +1 -0
- package/dist/handlers/render-prompt.d.ts +12 -0
- package/dist/handlers/render-prompt.d.ts.map +1 -0
- package/dist/handlers/render-prompt.js +41 -0
- package/dist/handlers/render-prompt.js.map +1 -0
- package/dist/handlers/resolve-tools.d.ts +10 -0
- package/dist/handlers/resolve-tools.d.ts.map +1 -0
- package/dist/handlers/resolve-tools.js +55 -0
- package/dist/handlers/resolve-tools.js.map +1 -0
- package/dist/handlers/result-shape.d.ts +94 -0
- package/dist/handlers/result-shape.d.ts.map +1 -0
- package/dist/handlers/result-shape.js +19 -0
- package/dist/handlers/result-shape.js.map +1 -0
- package/dist/handlers/run-retrievals.d.ts +10 -0
- package/dist/handlers/run-retrievals.d.ts.map +1 -0
- package/dist/handlers/run-retrievals.js +64 -0
- package/dist/handlers/run-retrievals.js.map +1 -0
- package/dist/handlers/run-snapshot.d.ts +4 -0
- package/dist/handlers/run-snapshot.d.ts.map +1 -0
- package/dist/handlers/run-snapshot.js +26 -0
- package/dist/handlers/run-snapshot.js.map +1 -0
- package/dist/handlers/setup.d.ts +25 -0
- package/dist/handlers/setup.d.ts.map +1 -0
- package/dist/handlers/setup.js +231 -0
- package/dist/handlers/setup.js.map +1 -0
- package/dist/handlers/structured-output.d.ts +38 -0
- package/dist/handlers/structured-output.d.ts.map +1 -0
- package/dist/handlers/structured-output.js +89 -0
- package/dist/handlers/structured-output.js.map +1 -0
- package/dist/handlers/tool-errors.d.ts +56 -0
- package/dist/handlers/tool-errors.d.ts.map +1 -0
- package/dist/handlers/tool-errors.js +73 -0
- package/dist/handlers/tool-errors.js.map +1 -0
- package/dist/handlers/tool-hitl.d.ts +45 -0
- package/dist/handlers/tool-hitl.d.ts.map +1 -0
- package/dist/handlers/tool-hitl.js +81 -0
- package/dist/handlers/tool-hitl.js.map +1 -0
- package/dist/handlers/turn-environment.d.ts +26 -0
- package/dist/handlers/turn-environment.d.ts.map +1 -0
- package/dist/handlers/turn-environment.js +154 -0
- package/dist/handlers/turn-environment.js.map +1 -0
- package/dist/hitl-policy.d.ts +45 -0
- package/dist/hitl-policy.d.ts.map +1 -0
- package/dist/hitl-policy.js +74 -0
- package/dist/hitl-policy.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/invoke.d.ts +36 -0
- package/dist/invoke.d.ts.map +1 -0
- package/dist/invoke.js +228 -0
- package/dist/invoke.js.map +1 -0
- package/dist/migrations-dir.d.ts +11 -0
- package/dist/migrations-dir.d.ts.map +1 -0
- package/dist/migrations-dir.js +14 -0
- package/dist/migrations-dir.js.map +1 -0
- package/dist/project-run-result.d.ts +23 -0
- package/dist/project-run-result.d.ts.map +1 -0
- package/dist/project-run-result.js +116 -0
- package/dist/project-run-result.js.map +1 -0
- package/dist/prompt.d.ts +83 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +119 -0
- package/dist/prompt.js.map +1 -0
- package/dist/provenance-emit.d.ts +44 -0
- package/dist/provenance-emit.d.ts.map +1 -0
- package/dist/provenance-emit.js +51 -0
- package/dist/provenance-emit.js.map +1 -0
- package/dist/registry.d.ts +38 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +125 -0
- package/dist/registry.js.map +1 -0
- package/dist/retrieval.d.ts +47 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +155 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/run-snapshot-binding.d.ts +77 -0
- package/dist/run-snapshot-binding.d.ts.map +1 -0
- package/dist/run-snapshot-binding.js +4 -0
- package/dist/run-snapshot-binding.js.map +1 -0
- package/dist/schema.d.ts +497 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +133 -0
- package/dist/schema.js.map +1 -0
- package/dist/streaming.d.ts +118 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +17 -0
- package/dist/streaming.js.map +1 -0
- package/dist/tenant-policy.d.ts +16 -0
- package/dist/tenant-policy.d.ts.map +1 -0
- package/dist/tenant-policy.js +77 -0
- package/dist/tenant-policy.js.map +1 -0
- package/dist/types.d.ts +435 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/versioning.d.ts +29 -0
- package/dist/versioning.d.ts.map +1 -0
- package/dist/versioning.js +58 -0
- package/dist/versioning.js.map +1 -0
- package/migrations/0000_sparkling_talkback.sql +18 -0
- package/migrations/0001_tired_warhawk.sql +16 -0
- package/migrations/0002_violet_ezekiel.sql +2 -0
- package/migrations/meta/0000_snapshot.json +172 -0
- package/migrations/meta/0001_snapshot.json +275 -0
- package/migrations/meta/0002_snapshot.json +287 -0
- package/migrations/meta/_journal.json +27 -0
- package/package.json +76 -4
- package/src/agent-turn-flow.ts +183 -0
- package/src/conversation-binding.ts +147 -0
- package/src/define.ts +572 -0
- package/src/errors.ts +80 -0
- package/src/guardrails-gate.ts +342 -0
- package/src/handlers/budget-check.ts +143 -0
- package/src/handlers/build-initial-messages.ts +103 -0
- package/src/handlers/compose-result.ts +90 -0
- package/src/handlers/constants.ts +10 -0
- package/src/handlers/context.ts +226 -0
- package/src/handlers/dispatch-tools.ts +633 -0
- package/src/handlers/errors.ts +282 -0
- package/src/handlers/evaluate-guardrails.ts +153 -0
- package/src/handlers/final-iteration.ts +30 -0
- package/src/handlers/index.ts +63 -0
- package/src/handlers/model-call.ts +151 -0
- package/src/handlers/persist-final-message.ts +67 -0
- package/src/handlers/persist-provenance.ts +39 -0
- package/src/handlers/persist-user-message.ts +70 -0
- package/src/handlers/public-types.ts +209 -0
- package/src/handlers/rehydrate.ts +161 -0
- package/src/handlers/render-prompt.ts +46 -0
- package/src/handlers/resolve-tools.ts +68 -0
- package/src/handlers/result-shape.ts +113 -0
- package/src/handlers/run-retrievals.ts +77 -0
- package/src/handlers/run-snapshot.ts +44 -0
- package/src/handlers/setup.ts +269 -0
- package/src/handlers/structured-output.ts +117 -0
- package/src/handlers/tool-errors.ts +122 -0
- package/src/handlers/tool-hitl.ts +126 -0
- package/src/handlers/turn-environment.ts +191 -0
- package/src/hitl-policy.ts +128 -0
- package/src/index.ts +154 -0
- package/src/invoke.ts +299 -0
- package/src/migrations-dir.ts +17 -0
- package/src/project-run-result.ts +131 -0
- package/src/prompt.ts +185 -0
- package/src/provenance-emit.ts +100 -0
- package/src/registry.ts +164 -0
- package/src/retrieval.ts +219 -0
- package/src/run-snapshot-binding.ts +87 -0
- package/src/schema.ts +154 -0
- package/src/streaming.ts +153 -0
- package/src/tenant-policy.ts +78 -0
- package/src/types.ts +453 -0
- package/src/versioning.ts +77 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { ProviderRegistry, TenantPolicy } from '@kindgi/capabilities';
|
|
5
|
+
import type { ComplianceProvider } from '@kindgi/compliance';
|
|
6
|
+
import {
|
|
7
|
+
type CheckRegistry,
|
|
8
|
+
type EvaluationBindings,
|
|
9
|
+
type EvaluationOutcome,
|
|
10
|
+
type EvaluationResult,
|
|
11
|
+
type Guardrail,
|
|
12
|
+
type ModelCallRecord,
|
|
13
|
+
type RunTrace,
|
|
14
|
+
type ToolCallRecord,
|
|
15
|
+
type ToolResultRecord,
|
|
16
|
+
evaluateAll,
|
|
17
|
+
} from '@kindgi/guardrails';
|
|
18
|
+
import type { AgentId, GuardrailId, ProjectId, Result, RunId, TenantId } from '@kindgi/types';
|
|
19
|
+
|
|
20
|
+
import type { Agent, ConversationId, ConversationMessage } from './types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Turn usage snapshot passed into `buildRunTrace`. Local mirror of
|
|
24
|
+
* `AgentTurnUsage` (handlers/result-shape.ts) — kept here to avoid a
|
|
25
|
+
* circular import.
|
|
26
|
+
*/
|
|
27
|
+
interface TurnUsageSnapshot {
|
|
28
|
+
readonly steps: number;
|
|
29
|
+
readonly promptTokens: number;
|
|
30
|
+
readonly completionTokens: number;
|
|
31
|
+
readonly totalCostUsd: number;
|
|
32
|
+
readonly durationMs: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Bindings the guardrail gate consumes. All optional — if `guardrails`
|
|
37
|
+
* is empty or `checks` is undefined, `evaluateGate` is a no-op. An
|
|
38
|
+
* agent that references a guardrail id missing from `guardrails` fails
|
|
39
|
+
* its turn with `unresolved-guardrail`.
|
|
40
|
+
*/
|
|
41
|
+
export interface GuardrailsBindings {
|
|
42
|
+
/** Full guardrail definitions available for this run. Indexed by id. */
|
|
43
|
+
readonly guardrails?: readonly Guardrail[];
|
|
44
|
+
/** Check registry with built-ins + any adapter-registered custom checks. */
|
|
45
|
+
readonly checks?: CheckRegistry;
|
|
46
|
+
/** Optional compliance sink — every failed check emits an evidence record. */
|
|
47
|
+
readonly compliance?: ComplianceProvider;
|
|
48
|
+
/**
|
|
49
|
+
* Provider registry for llm-judge guardrails; the judge model is
|
|
50
|
+
* routed through it under the turn's tenant policy. If absent,
|
|
51
|
+
* llm-judge guardrails error with `judge-missing`; if no provider
|
|
52
|
+
* satisfies the judge's capability, with `judge-routing-failed`.
|
|
53
|
+
*/
|
|
54
|
+
readonly providerRegistry?: ProviderRegistry;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Materialize a `RunTrace` from an agent turn's captured state. The
|
|
59
|
+
* guardrail engine reads only the trace — it doesn't touch memory,
|
|
60
|
+
* kernel journal, or provenance directly (keeps checks pure and
|
|
61
|
+
* dependency-free).
|
|
62
|
+
*/
|
|
63
|
+
export function buildRunTrace(input: {
|
|
64
|
+
/** The kernel run of the turn. */
|
|
65
|
+
readonly runId: RunId;
|
|
66
|
+
readonly tenantId: TenantId;
|
|
67
|
+
/** Lets the engine record compliance evidence for a failed check. */
|
|
68
|
+
readonly projectId: ProjectId;
|
|
69
|
+
readonly conversationId: ConversationId;
|
|
70
|
+
/** 1-based number of this turn in the conversation. */
|
|
71
|
+
readonly turnNumber: number;
|
|
72
|
+
readonly agent: Agent;
|
|
73
|
+
/** The user message that started the turn. */
|
|
74
|
+
readonly userMessage: string;
|
|
75
|
+
/** The turn's structured input (`InvokeAgentInput.input`), when it has one. */
|
|
76
|
+
readonly stepInput?: unknown;
|
|
77
|
+
/** The parsed answer, when the agent declares `output`. */
|
|
78
|
+
readonly structuredOutput?: unknown;
|
|
79
|
+
readonly appended: readonly ConversationMessage[];
|
|
80
|
+
readonly finalResponse: ConversationMessage;
|
|
81
|
+
readonly usage: TurnUsageSnapshot;
|
|
82
|
+
}): RunTrace {
|
|
83
|
+
const toolCalls: ToolCallRecord[] = [];
|
|
84
|
+
const toolResults: ToolResultRecord[] = [];
|
|
85
|
+
const modelCalls: ModelCallRecord[] = [];
|
|
86
|
+
|
|
87
|
+
for (const msg of input.appended) {
|
|
88
|
+
if (msg.role === 'agent' && typeof msg.content === 'object' && msg.content !== null) {
|
|
89
|
+
const structured = msg.content as {
|
|
90
|
+
readonly toolCalls?: readonly {
|
|
91
|
+
readonly id: string;
|
|
92
|
+
readonly name: string;
|
|
93
|
+
readonly arguments: Readonly<Record<string, unknown>>;
|
|
94
|
+
}[];
|
|
95
|
+
};
|
|
96
|
+
if (structured.toolCalls !== undefined) {
|
|
97
|
+
for (const call of structured.toolCalls) {
|
|
98
|
+
toolCalls.push({
|
|
99
|
+
toolId: call.name as never,
|
|
100
|
+
toolName: call.name,
|
|
101
|
+
arguments: call.arguments,
|
|
102
|
+
at: msg.createdAt,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (msg.role === 'tool' && msg.toolCall !== undefined) {
|
|
108
|
+
toolResults.push({
|
|
109
|
+
toolCallId: msg.toolCall.invocationId,
|
|
110
|
+
output: msg.content,
|
|
111
|
+
at: msg.createdAt,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Approximate a model-call record for cost/latency guardrails — a
|
|
117
|
+
// more granular per-call breakdown would need an observability
|
|
118
|
+
// hook.
|
|
119
|
+
modelCalls.push({
|
|
120
|
+
providerId: 'runtime.picked',
|
|
121
|
+
model: 'runtime.picked',
|
|
122
|
+
promptTokens: input.usage.promptTokens,
|
|
123
|
+
completionTokens: input.usage.completionTokens,
|
|
124
|
+
at: input.finalResponse.createdAt,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const output =
|
|
128
|
+
typeof input.finalResponse.content === 'string'
|
|
129
|
+
? input.finalResponse.content
|
|
130
|
+
: JSON.stringify(input.finalResponse.content);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
runId: input.runId,
|
|
134
|
+
tenantId: input.tenantId,
|
|
135
|
+
projectId: input.projectId,
|
|
136
|
+
agentId: input.agent.id as unknown as AgentId,
|
|
137
|
+
output,
|
|
138
|
+
toolCalls,
|
|
139
|
+
toolResults,
|
|
140
|
+
modelCalls,
|
|
141
|
+
mode: 'runtime',
|
|
142
|
+
userInput: input.userMessage,
|
|
143
|
+
conversationId: input.conversationId as unknown as string,
|
|
144
|
+
turnNumber: input.turnNumber,
|
|
145
|
+
totalCostUsd: input.usage.totalCostUsd,
|
|
146
|
+
durationMs: input.usage.durationMs,
|
|
147
|
+
attributes: {
|
|
148
|
+
steps: input.usage.steps,
|
|
149
|
+
...(input.stepInput !== undefined && { stepInput: input.stepInput }),
|
|
150
|
+
...(input.structuredOutput !== undefined && { structuredOutput: input.structuredOutput }),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Filter the bound guardrail list to those referenced by the agent's
|
|
157
|
+
* `guardrails: string[]` declaration. Missing references are collected
|
|
158
|
+
* into `missing` — the caller decides whether that's fatal.
|
|
159
|
+
*/
|
|
160
|
+
export function resolveGuardrails(
|
|
161
|
+
agent: Agent,
|
|
162
|
+
bindings: GuardrailsBindings,
|
|
163
|
+
): { readonly resolved: readonly Guardrail[]; readonly missing: readonly string[] } {
|
|
164
|
+
const available = new Map<string, Guardrail>();
|
|
165
|
+
for (const inv of bindings.guardrails ?? []) available.set(inv.id, inv);
|
|
166
|
+
const resolved: Guardrail[] = [];
|
|
167
|
+
const missing: string[] = [];
|
|
168
|
+
for (const id of agent.guardrails) {
|
|
169
|
+
const inv = available.get(id);
|
|
170
|
+
if (inv !== undefined) resolved.push(inv);
|
|
171
|
+
else missing.push(id);
|
|
172
|
+
}
|
|
173
|
+
return { resolved, missing };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Evaluate every resolved guardrail against the turn's trace. Returns
|
|
178
|
+
* the raw outcomes so the caller can decide how to react — the gate
|
|
179
|
+
* doesn't halt on its own. `tenantPolicy` (the policy the turn was
|
|
180
|
+
* routed under) also governs which models llm-judge guardrails may use;
|
|
181
|
+
* `abortSignal` (the turn's) reaches every check, so a slow judge or
|
|
182
|
+
* pack check stops when the turn does.
|
|
183
|
+
*/
|
|
184
|
+
export async function evaluateGate(
|
|
185
|
+
guardrails: readonly Guardrail[],
|
|
186
|
+
trace: RunTrace,
|
|
187
|
+
bindings: GuardrailsBindings,
|
|
188
|
+
tenantPolicy?: TenantPolicy,
|
|
189
|
+
abortSignal?: AbortSignal,
|
|
190
|
+
): Promise<readonly EvaluationOutcome[]> {
|
|
191
|
+
if (guardrails.length === 0 || bindings.checks === undefined) return [];
|
|
192
|
+
const evalBindings: EvaluationBindings = {
|
|
193
|
+
...(bindings.providerRegistry !== undefined && { providerRegistry: bindings.providerRegistry }),
|
|
194
|
+
...(bindings.compliance !== undefined && { compliance: bindings.compliance }),
|
|
195
|
+
...(tenantPolicy !== undefined && { tenantPolicy }),
|
|
196
|
+
...(abortSignal !== undefined && { abortSignal }),
|
|
197
|
+
};
|
|
198
|
+
return await evaluateAll(guardrails, bindings.checks, trace, evalBindings);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Sort evaluation outcomes by the failed guardrail's action. `halt` is
|
|
203
|
+
* blocking — the turn fails with `guardrail-violation`. `log-only` and
|
|
204
|
+
* `noop` are warnings; every other action (`retry`, `escalate`,
|
|
205
|
+
* `compensate`, or a custom action — actions are open strings) lands in
|
|
206
|
+
* `other`. Warnings and `other` are attached to the successful turn
|
|
207
|
+
* result under `result.violations`; the turn does not carry out those
|
|
208
|
+
* actions. Evaluation errors (a check that could not run) are collected
|
|
209
|
+
* in `errors`.
|
|
210
|
+
*/
|
|
211
|
+
export function categorizeOutcomes(outcomes: readonly EvaluationOutcome[]): {
|
|
212
|
+
readonly blocking: readonly EvaluationResult[];
|
|
213
|
+
readonly warnings: readonly EvaluationResult[];
|
|
214
|
+
readonly other: readonly EvaluationResult[];
|
|
215
|
+
readonly errors: readonly { readonly guardrailId: string; readonly message: string }[];
|
|
216
|
+
} {
|
|
217
|
+
const blocking: EvaluationResult[] = [];
|
|
218
|
+
const warnings: EvaluationResult[] = [];
|
|
219
|
+
const other: EvaluationResult[] = [];
|
|
220
|
+
const errors: { guardrailId: string; message: string }[] = [];
|
|
221
|
+
for (const outcome of outcomes) {
|
|
222
|
+
if (outcome.kind === 'err') {
|
|
223
|
+
errors.push({
|
|
224
|
+
guardrailId:
|
|
225
|
+
'guardrailId' in outcome.error && typeof outcome.error.guardrailId === 'string'
|
|
226
|
+
? outcome.error.guardrailId
|
|
227
|
+
: '<unknown>',
|
|
228
|
+
message: outcome.error.message,
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (outcome.kind === 'skip') continue;
|
|
233
|
+
const evalResult = outcome.value;
|
|
234
|
+
if (evalResult.result.passed) continue;
|
|
235
|
+
if (evalResult.action === 'halt') blocking.push(evalResult);
|
|
236
|
+
else if (evalResult.action === 'log-only' || evalResult.action === 'noop') {
|
|
237
|
+
warnings.push(evalResult);
|
|
238
|
+
} else other.push(evalResult);
|
|
239
|
+
}
|
|
240
|
+
return { blocking, warnings, other, errors };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Message for a turn stopped by blocking violations — names each
|
|
245
|
+
* guardrail that fired, with its check's reason when it gave one:
|
|
246
|
+
*
|
|
247
|
+
* Turn blocked by guardrail 'no-pii': Response contains an email address
|
|
248
|
+
* Turn blocked by 2 guardrails: 'no-pii' (Response contains …); 'max-length'
|
|
249
|
+
*/
|
|
250
|
+
export function describeBlockingViolations(blocking: readonly EvaluationResult[]): string {
|
|
251
|
+
const reasonOf = (v: EvaluationResult): string | undefined => {
|
|
252
|
+
const reason = v.result.reason?.trim();
|
|
253
|
+
return reason === undefined || reason === '' ? undefined : reason;
|
|
254
|
+
};
|
|
255
|
+
const [only] = blocking;
|
|
256
|
+
if (blocking.length === 1 && only !== undefined) {
|
|
257
|
+
const reason = reasonOf(only);
|
|
258
|
+
return `Turn blocked by guardrail '${only.guardrailId}'${reason !== undefined ? `: ${reason}` : ''}`;
|
|
259
|
+
}
|
|
260
|
+
const named = blocking.map((v) => {
|
|
261
|
+
const reason = reasonOf(v);
|
|
262
|
+
return `'${v.guardrailId}'${reason !== undefined ? ` (${reason})` : ''}`;
|
|
263
|
+
});
|
|
264
|
+
return `Turn blocked by ${blocking.length} guardrails: ${named.join('; ')}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Result of a session-gate check performed BEFORE running a turn:
|
|
269
|
+
* the HITL turn threshold (`conversationPolicy.hitl.afterTurns` or
|
|
270
|
+
* `conversationPolicy.hitlAfterTurns`).
|
|
271
|
+
*/
|
|
272
|
+
export type SessionGateResult =
|
|
273
|
+
| { readonly kind: 'ok' }
|
|
274
|
+
| { readonly kind: 'hitl-required'; readonly reason: string; readonly threshold: number };
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Evaluate session-level gates against the conversation state before
|
|
278
|
+
* running the next turn: the HITL threshold from
|
|
279
|
+
* `conversationPolicy.hitl.afterTurns` or `conversationPolicy.hitlAfterTurns`.
|
|
280
|
+
*
|
|
281
|
+
* Overloaded call shapes:
|
|
282
|
+
* - `evaluateSessionGate(agent, turnCount)` — reads the agent's policy
|
|
283
|
+
* directly.
|
|
284
|
+
* - `evaluateSessionGate({ afterTurns? }, turnCount)` — reads a
|
|
285
|
+
* pre-resolved effective policy (the setup handler passes
|
|
286
|
+
* `resolveEffectiveHitlPolicy` output).
|
|
287
|
+
*/
|
|
288
|
+
export function evaluateSessionGate(
|
|
289
|
+
input: Agent | { readonly afterTurns?: number },
|
|
290
|
+
currentTurnCount: number,
|
|
291
|
+
): SessionGateResult {
|
|
292
|
+
const asAgent = input as Agent;
|
|
293
|
+
const asPolicy = input as { readonly afterTurns?: number };
|
|
294
|
+
const threshold: number | undefined =
|
|
295
|
+
'conversationPolicy' in asAgent
|
|
296
|
+
? (asAgent.conversationPolicy?.hitl?.afterTurns ?? asAgent.conversationPolicy?.hitlAfterTurns)
|
|
297
|
+
: asPolicy.afterTurns;
|
|
298
|
+
if (threshold !== undefined && currentTurnCount >= threshold) {
|
|
299
|
+
return {
|
|
300
|
+
kind: 'hitl-required',
|
|
301
|
+
reason: `Conversation reached HITL threshold: ${currentTurnCount} turns >= ${threshold}`,
|
|
302
|
+
threshold,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return { kind: 'ok' };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Discriminated error emitted when blocking guardrails fail. */
|
|
309
|
+
export interface GuardrailViolationError {
|
|
310
|
+
readonly code: 'guardrail-violation';
|
|
311
|
+
readonly message: string;
|
|
312
|
+
readonly violations: readonly EvaluationResult[];
|
|
313
|
+
/** Errors from checks that couldn't even evaluate (missing check, bad config). */
|
|
314
|
+
readonly evaluationErrors: readonly { readonly guardrailId: string; readonly message: string }[];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Error shape for a turn blocked by the session HITL threshold.
|
|
319
|
+
* `approvalId` is set when the caller supplied a `bindings.hitl` sink and
|
|
320
|
+
* the enqueue succeeded — downstream code can poll or subscribe on that
|
|
321
|
+
* id. The agent turn itself does not return this code: it parks on a
|
|
322
|
+
* HITL waitpoint instead (`AgentTurnResult.status: 'suspended'`).
|
|
323
|
+
*/
|
|
324
|
+
export interface HitlRequiredError {
|
|
325
|
+
readonly code: 'hitl-required';
|
|
326
|
+
readonly message: string;
|
|
327
|
+
readonly threshold: number;
|
|
328
|
+
readonly approvalId?: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Emitted when the agent references guardrail IDs not in the registry. */
|
|
332
|
+
export interface UnresolvedGuardrailError {
|
|
333
|
+
readonly code: 'unresolved-guardrail';
|
|
334
|
+
readonly message: string;
|
|
335
|
+
readonly guardrailIds: readonly string[];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Silence unused-type lint (GuardrailId is used by callers via type inference).
|
|
339
|
+
void 0 as unknown as GuardrailId;
|
|
340
|
+
|
|
341
|
+
// Discriminated Result helper to keep call sites tidy.
|
|
342
|
+
export type GateResult<T, E> = Result<T, E>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { ModelMessage } from '@kindgi/capabilities';
|
|
5
|
+
import type { NodeHandler } from '@kindgi/handler';
|
|
6
|
+
|
|
7
|
+
import type { AgentOutputSpec } from '../types.js';
|
|
8
|
+
import { DEFAULT_MAX_STEPS } from './constants.js';
|
|
9
|
+
import type { AgentTurnIterationOutput, TurnContext } from './context.js';
|
|
10
|
+
import { throwAgentTurnFailure } from './errors.js';
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_MAX_REPAIRS,
|
|
13
|
+
type OutputChecker,
|
|
14
|
+
outputChecker,
|
|
15
|
+
repairMessage,
|
|
16
|
+
repairsSoFar,
|
|
17
|
+
} from './structured-output.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Loop-body node #3. The final gate before `$loop-end`. Decides
|
|
21
|
+
* whether the iteration finalizes the turn based on:
|
|
22
|
+
*
|
|
23
|
+
* - The model's `finishReason` (anything other than `tool-use` with
|
|
24
|
+
* calls terminates).
|
|
25
|
+
* - `agent.budget.maxSteps` — enforced against `ctx.usage.steps`.
|
|
26
|
+
* - `agent.budget.maxCostUsd` — enforced against cumulative cost.
|
|
27
|
+
* - Cooperative abort from `ctx.turnAbort`.
|
|
28
|
+
* - `agent.output` — a final answer that isn't JSON matching the
|
|
29
|
+
* schema keeps the turn going with a repair message, while repairs
|
|
30
|
+
* (and steps) are left; after that the turn fails with
|
|
31
|
+
* `output-schema-violation`. Skipped on a dry run.
|
|
32
|
+
*
|
|
33
|
+
* Returns the fully-shaped `AgentTurnIterationOutput` — this is the
|
|
34
|
+
* iteration's `$loop-end` output that the loop node's `outputSchema`
|
|
35
|
+
* validates against.
|
|
36
|
+
*/
|
|
37
|
+
export function buildBudgetCheckHandler(ctx: TurnContext): NodeHandler {
|
|
38
|
+
let checker: OutputChecker | undefined;
|
|
39
|
+
return async (input: unknown, kctx) => {
|
|
40
|
+
if (ctx.turnAbort.signal.aborted) {
|
|
41
|
+
throwAgentTurnFailure({
|
|
42
|
+
code: 'agent-turn-aborted',
|
|
43
|
+
message: 'Agent turn aborted before budget-check',
|
|
44
|
+
reason: ctx.abortReason ?? 'timeout',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const partial = input as {
|
|
49
|
+
readonly step: number;
|
|
50
|
+
readonly finishReason: AgentTurnIterationOutput['finishReason'];
|
|
51
|
+
readonly message: ModelMessage;
|
|
52
|
+
readonly iterationAppended: readonly AgentTurnIterationOutput['iterationAppended'][number][];
|
|
53
|
+
readonly iterationUsage: AgentTurnIterationOutput['iterationUsage'];
|
|
54
|
+
readonly provider: AgentTurnIterationOutput['provider'];
|
|
55
|
+
readonly nextMessages: readonly ModelMessage[];
|
|
56
|
+
readonly hasToolCalls: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const maxSteps = ctx.input.agent.budget?.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
60
|
+
const maxCostUsd = ctx.input.agent.budget?.maxCostUsd;
|
|
61
|
+
|
|
62
|
+
// Cost cap is a hard failure — the caller sees `budget-exceeded`
|
|
63
|
+
// before the turn tries another iteration.
|
|
64
|
+
if (maxCostUsd !== undefined && ctx.usage.totalCostUsd > maxCostUsd) {
|
|
65
|
+
throwAgentTurnFailure({
|
|
66
|
+
code: 'budget-exceeded',
|
|
67
|
+
message: `Agent turn cost budget exceeded (limit ${maxCostUsd}, observed ${ctx.usage.totalCostUsd})`,
|
|
68
|
+
kind: 'cost',
|
|
69
|
+
limit: maxCostUsd,
|
|
70
|
+
observed: ctx.usage.totalCostUsd,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Step cap — if the model still wants another tool call but we've
|
|
75
|
+
// used the allotted steps, fail the run.
|
|
76
|
+
if (partial.hasToolCalls && ctx.usage.steps >= maxSteps) {
|
|
77
|
+
throwAgentTurnFailure({
|
|
78
|
+
code: 'budget-exceeded',
|
|
79
|
+
message: `Agent turn steps budget exceeded (limit ${maxSteps}, observed ${ctx.usage.steps})`,
|
|
80
|
+
kind: 'steps',
|
|
81
|
+
limit: maxSteps,
|
|
82
|
+
observed: ctx.usage.steps,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// `finishedTurn` semantics: true when the model's finishReason is
|
|
87
|
+
// terminal (i.e. not a live tool-use) — the current iteration's
|
|
88
|
+
// assistant message is the final answer.
|
|
89
|
+
let finishedTurn = !partial.hasToolCalls;
|
|
90
|
+
let nextMessages = partial.nextMessages;
|
|
91
|
+
|
|
92
|
+
const spec = ctx.input.agent.output;
|
|
93
|
+
if (finishedTurn && spec !== undefined && !kctx.dryRun) {
|
|
94
|
+
checker ??= outputChecker(spec);
|
|
95
|
+
const repair = outputRepair(spec, checker, partial.message, partial.nextMessages, {
|
|
96
|
+
stepsLeft: ctx.usage.steps < maxSteps,
|
|
97
|
+
});
|
|
98
|
+
if (repair !== undefined) {
|
|
99
|
+
finishedTurn = false;
|
|
100
|
+
nextMessages = repair;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const output: AgentTurnIterationOutput = {
|
|
105
|
+
finishReason: partial.finishReason,
|
|
106
|
+
message: partial.message,
|
|
107
|
+
iterationAppended: partial.iterationAppended,
|
|
108
|
+
iterationUsage: partial.iterationUsage,
|
|
109
|
+
provider: partial.provider,
|
|
110
|
+
finishedTurn,
|
|
111
|
+
nextMessages,
|
|
112
|
+
};
|
|
113
|
+
return output;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check a final answer against the agent's output schema. A valid
|
|
119
|
+
* answer returns `undefined` (the turn finishes). An invalid one with
|
|
120
|
+
* repairs and steps left returns the next model call's messages: the
|
|
121
|
+
* answer, then a repair message listing what's wrong. Otherwise the
|
|
122
|
+
* turn fails with `output-schema-violation`.
|
|
123
|
+
*/
|
|
124
|
+
function outputRepair(
|
|
125
|
+
spec: AgentOutputSpec,
|
|
126
|
+
checker: OutputChecker,
|
|
127
|
+
answer: ModelMessage,
|
|
128
|
+
sent: readonly ModelMessage[],
|
|
129
|
+
budget: { readonly stepsLeft: boolean },
|
|
130
|
+
): readonly ModelMessage[] | undefined {
|
|
131
|
+
const checked = checker.check(answer.content);
|
|
132
|
+
if (checked.kind === 'ok') return undefined;
|
|
133
|
+
const repairs = repairsSoFar(sent);
|
|
134
|
+
if (repairs < (spec.maxRepairs ?? DEFAULT_MAX_REPAIRS) && budget.stepsLeft) {
|
|
135
|
+
return [...sent, answer, repairMessage(spec, checked.errors)];
|
|
136
|
+
}
|
|
137
|
+
throwAgentTurnFailure({
|
|
138
|
+
code: 'output-schema-violation',
|
|
139
|
+
message: `The agent's answer does not match its ${spec.name ?? 'output'} schema after ${repairs} repair${repairs === 1 ? '' : 's'}: ${checked.errors.join('; ')}`,
|
|
140
|
+
errors: checked.errors,
|
|
141
|
+
attempts: repairs + 1,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { ModelMessage, ModelToolCall } from '@kindgi/capabilities';
|
|
5
|
+
import type { NodeHandler } from '@kindgi/handler';
|
|
6
|
+
|
|
7
|
+
import { formatRetrievedForPrompt } from '../retrieval.js';
|
|
8
|
+
import type { ConversationMessage } from '../types.js';
|
|
9
|
+
|
|
10
|
+
import type { TurnContext } from './context.js';
|
|
11
|
+
import { throwAgentTurnFailure } from './errors.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compose the initial `modelMessages` array the loop's first iteration
|
|
15
|
+
* feeds to the model:
|
|
16
|
+
*
|
|
17
|
+
* [ system: rendered prompt,
|
|
18
|
+
* system: retrieved context (if any),
|
|
19
|
+
* ...history,
|
|
20
|
+
* user: current message ]
|
|
21
|
+
*
|
|
22
|
+
* Output shape: `{ nextMessages: ModelMessage[] }` — matches the loop
|
|
23
|
+
* iteration output's `nextMessages` field so the loop body can treat
|
|
24
|
+
* iteration 0's input identically to subsequent iterations'.
|
|
25
|
+
*/
|
|
26
|
+
export function buildBuildInitialMessagesHandler(ctx: TurnContext): NodeHandler {
|
|
27
|
+
return async (input: unknown) => {
|
|
28
|
+
if (ctx.conversation === undefined || ctx.retrieved === undefined) {
|
|
29
|
+
throwAgentTurnFailure({
|
|
30
|
+
code: 'model-invocation-failed',
|
|
31
|
+
message: 'build-initial-messages invoked before setup / retrievals completed',
|
|
32
|
+
cause: null,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// `input` is `run-retrievals`'s output; discarded — the rendered
|
|
36
|
+
// prompt is read from setup's sibling output via ctx (retained on
|
|
37
|
+
// the closure by render-prompt through the local field `renderedPrompt`).
|
|
38
|
+
void input;
|
|
39
|
+
|
|
40
|
+
const historyLimit = ctx.input.agent.conversationPolicy?.historyLimit;
|
|
41
|
+
const messages = await ctx.bindings.conversationBinding.readMessages({
|
|
42
|
+
tenantId: ctx.input.tenantId,
|
|
43
|
+
conversationId: ctx.input.conversationId,
|
|
44
|
+
});
|
|
45
|
+
if (messages.kind === 'err') throwAgentTurnFailure(messages.error);
|
|
46
|
+
// The user message just appended is included in the history read;
|
|
47
|
+
// drop it because the composer adds it explicitly as the last
|
|
48
|
+
// element.
|
|
49
|
+
const historyRaw = messages.value.filter((m) => m.sequence !== ctx.userMessage?.sequence);
|
|
50
|
+
const history =
|
|
51
|
+
historyLimit === undefined
|
|
52
|
+
? historyRaw
|
|
53
|
+
: historyRaw.slice(Math.max(0, historyRaw.length - historyLimit));
|
|
54
|
+
|
|
55
|
+
const contextBlock = formatRetrievedForPrompt(ctx.retrieved);
|
|
56
|
+
const contextMessage: ModelMessage | undefined =
|
|
57
|
+
contextBlock.length > 0 ? { role: 'system', content: contextBlock } : undefined;
|
|
58
|
+
|
|
59
|
+
const rendered = ctx.renderedPrompt;
|
|
60
|
+
if (rendered === undefined) {
|
|
61
|
+
throwAgentTurnFailure({
|
|
62
|
+
code: 'model-invocation-failed',
|
|
63
|
+
message: 'build-initial-messages invoked before render-prompt completed',
|
|
64
|
+
cause: null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const modelMessages: ModelMessage[] = [
|
|
69
|
+
{ role: 'system', content: rendered },
|
|
70
|
+
...(contextMessage !== undefined ? [contextMessage] : []),
|
|
71
|
+
...history.map(conversationToModelMessage),
|
|
72
|
+
{ role: 'user', content: ctx.input.userMessage },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
return { nextMessages: modelMessages };
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function conversationToModelMessage(msg: ConversationMessage): ModelMessage {
|
|
80
|
+
if (msg.role === 'tool') {
|
|
81
|
+
return {
|
|
82
|
+
role: 'tool',
|
|
83
|
+
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
|
|
84
|
+
...(msg.toolCall !== undefined && { toolCallId: msg.toolCall.invocationId }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (msg.role === 'agent' && typeof msg.content === 'object' && msg.content !== null) {
|
|
88
|
+
const structured = msg.content as {
|
|
89
|
+
readonly text?: string;
|
|
90
|
+
readonly toolCalls?: readonly ModelToolCall[];
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
role: 'assistant',
|
|
94
|
+
content: structured.text ?? '',
|
|
95
|
+
...(structured.toolCalls !== undefined && { toolCalls: structured.toolCalls }),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const role: ModelMessage['role'] = msg.role === 'agent' ? 'assistant' : msg.role;
|
|
99
|
+
return {
|
|
100
|
+
role,
|
|
101
|
+
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { NodeHandler } from '@kindgi/handler';
|
|
5
|
+
|
|
6
|
+
import { emitTurnEvent } from '../streaming.js';
|
|
7
|
+
import type { AgentTurnResult, AgentTurnUsage } from './result-shape.js';
|
|
8
|
+
|
|
9
|
+
import type { TurnContext } from './context.js';
|
|
10
|
+
import { throwAgentTurnFailure } from './errors.js';
|
|
11
|
+
import { parseJsonAnswer } from './structured-output.js';
|
|
12
|
+
|
|
13
|
+
/** The final answer as JSON — `budget-check` already validated it against the schema. */
|
|
14
|
+
function structuredAnswer(content: unknown): unknown {
|
|
15
|
+
const parsed = parseJsonAnswer(content);
|
|
16
|
+
return parsed.kind === 'ok' ? parsed.value : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The terminal node — builds the caller-facing `AgentTurnResult` from
|
|
21
|
+
* accumulated `TurnContext` state, emits `turn.completed`, and returns
|
|
22
|
+
* the fully-shaped result as the flow's output. `projectRunResult`
|
|
23
|
+
* unwraps this on the way out.
|
|
24
|
+
*/
|
|
25
|
+
export function buildComposeResultHandler(ctx: TurnContext): NodeHandler {
|
|
26
|
+
return async (_input, kctx) => {
|
|
27
|
+
if (ctx.finalMessage === undefined || ctx.conversation === undefined) {
|
|
28
|
+
throwAgentTurnFailure({
|
|
29
|
+
code: 'model-invocation-failed',
|
|
30
|
+
message: 'compose-result invoked before final message + conversation ready',
|
|
31
|
+
cause: null,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const durationMs = Date.now() - ctx.startedAt;
|
|
35
|
+
const usage: AgentTurnUsage = {
|
|
36
|
+
steps: ctx.usage.steps,
|
|
37
|
+
promptTokens: ctx.usage.promptTokens,
|
|
38
|
+
completionTokens: ctx.usage.completionTokens,
|
|
39
|
+
totalCostUsd: ctx.usage.totalCostUsd,
|
|
40
|
+
durationMs,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const provider = ctx.lastProvider ?? {
|
|
44
|
+
id: ctx.provider?.metadata.id ?? 'unknown',
|
|
45
|
+
model: ctx.model?.name ?? 'unknown',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const result: AgentTurnResult = {
|
|
49
|
+
runId: kctx.runId,
|
|
50
|
+
conversationId: ctx.input.conversationId,
|
|
51
|
+
// Dry-run must not advance the conversation's turn counter (no
|
|
52
|
+
// persistence happened). Real runs report the next turn number.
|
|
53
|
+
turnNumber: kctx.dryRun ? ctx.conversation.turnCount : ctx.conversation.turnCount + 1,
|
|
54
|
+
appended: ctx.appended,
|
|
55
|
+
response: ctx.finalMessage,
|
|
56
|
+
retrieved: ctx.retrieved ?? [],
|
|
57
|
+
violations: ctx.nonBlockingViolations ?? [],
|
|
58
|
+
usage,
|
|
59
|
+
provider,
|
|
60
|
+
...(ctx.provider?.metadata.fallback === true && {
|
|
61
|
+
warnings: [
|
|
62
|
+
{
|
|
63
|
+
code: 'fallback-provider',
|
|
64
|
+
message: `Answered by "${provider.id}", a fallback provider: no other registered provider satisfies agent "${ctx.input.agent.id}".`,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
}),
|
|
68
|
+
...(ctx.input.agent.output !== undefined && {
|
|
69
|
+
output: kctx.dryRun ? null : structuredAnswer(ctx.finalMessage.content),
|
|
70
|
+
}),
|
|
71
|
+
...(ctx.persistedProvenance !== undefined && { provenance: ctx.persistedProvenance }),
|
|
72
|
+
...(kctx.dryRun && { dryRun: true }),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
76
|
+
kind: 'turn.completed',
|
|
77
|
+
conversationId: ctx.input.conversationId,
|
|
78
|
+
turnNumber: ctx.conversation.turnCount + 1,
|
|
79
|
+
response: ctx.finalMessage,
|
|
80
|
+
retrieved: ctx.retrieved ?? [],
|
|
81
|
+
durationMs,
|
|
82
|
+
totalCostUsd: ctx.usage.totalCostUsd,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Wrapped explicitly: the runtime reads a returned object with an
|
|
86
|
+
// `output` key as `{ output, stateDelta }`, and a typed turn's
|
|
87
|
+
// result has one.
|
|
88
|
+
return { output: result };
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Defaults shared across handler modules. Kept in a single tiny module
|
|
6
|
+
* so the loop-body handlers can import them without pulling in the
|
|
7
|
+
* whole invoke.ts orchestration surface.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_MAX_STEPS = 8;
|
|
10
|
+
export const DEFAULT_MAX_WALL_MS = 120_000;
|