@kindgi/agents 0.0.0-bootstrap.0 → 0.1.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/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,282 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { EvaluationResult } from '@kindgi/guardrails';
|
|
5
|
+
import type { PolicyKind } from '@kindgi/policy-contract';
|
|
6
|
+
|
|
7
|
+
import type { AgentError } from '../errors.js';
|
|
8
|
+
import type {
|
|
9
|
+
GuardrailViolationError,
|
|
10
|
+
HitlRequiredError,
|
|
11
|
+
UnresolvedGuardrailError,
|
|
12
|
+
} from '../guardrails-gate.js';
|
|
13
|
+
|
|
14
|
+
/** Structured error returned to the caller when an agent turn fails. */
|
|
15
|
+
export type InvokeAgentError =
|
|
16
|
+
| AgentError
|
|
17
|
+
| UnresolvedToolError
|
|
18
|
+
| ToolVersionUnresolvableError
|
|
19
|
+
| CapabilityRoutingError
|
|
20
|
+
| ModelInvocationError
|
|
21
|
+
| ToolInvocationError
|
|
22
|
+
| BudgetExceededError
|
|
23
|
+
| AgentTurnAbortedError
|
|
24
|
+
| HitlRequiredError
|
|
25
|
+
| GuardrailViolationError
|
|
26
|
+
| UnresolvedGuardrailError
|
|
27
|
+
| OutputSchemaViolationError
|
|
28
|
+
| TenantPolicyUnavailableError;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A tenant policy the turn must apply couldn't be: its spec doesn't
|
|
32
|
+
* validate, or the policy registry failed. The turn doesn't run without
|
|
33
|
+
* it — skipping a tenant's `hitl` policy would skip its approvals.
|
|
34
|
+
*/
|
|
35
|
+
export interface TenantPolicyUnavailableError {
|
|
36
|
+
readonly code: 'tenant-policy-unavailable';
|
|
37
|
+
readonly message: string;
|
|
38
|
+
readonly policyKind: PolicyKind;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface UnresolvedToolError {
|
|
42
|
+
readonly code: 'unresolved-tool';
|
|
43
|
+
readonly message: string;
|
|
44
|
+
readonly toolId: string;
|
|
45
|
+
/**
|
|
46
|
+
* Failed tool calls sent back to the model this turn before this one
|
|
47
|
+
* ended it (see `Agent.toolErrors`). Present when the failure is a
|
|
48
|
+
* kind the turn retries and its retries ran out.
|
|
49
|
+
*/
|
|
50
|
+
readonly toolRetries?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The agent references a tool that is registered but whose semver range
|
|
55
|
+
* does not match any registered version, or whose range is grammatically
|
|
56
|
+
* broken. Distinct from `unresolved-tool` (no id registered).
|
|
57
|
+
*/
|
|
58
|
+
export interface ToolVersionUnresolvableError {
|
|
59
|
+
readonly code: 'tool-version-unresolvable';
|
|
60
|
+
readonly message: string;
|
|
61
|
+
readonly toolId: string;
|
|
62
|
+
readonly requestedRange: string;
|
|
63
|
+
readonly availableVersions?: readonly string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CapabilityRoutingError {
|
|
67
|
+
readonly code: 'capability-routing-failed';
|
|
68
|
+
readonly message: string;
|
|
69
|
+
readonly cause: unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ModelInvocationError {
|
|
73
|
+
readonly code: 'model-invocation-failed';
|
|
74
|
+
readonly message: string;
|
|
75
|
+
readonly cause: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ToolInvocationError {
|
|
79
|
+
readonly code: 'tool-invocation-failed';
|
|
80
|
+
readonly message: string;
|
|
81
|
+
readonly toolId: string;
|
|
82
|
+
readonly cause: unknown;
|
|
83
|
+
/**
|
|
84
|
+
* When the inner failure was an AJV input-schema validation, the
|
|
85
|
+
* validator's `errors[]` array. Hoisted from `cause` to the top
|
|
86
|
+
* level so it survives `toWireError` (which filters `cause` to
|
|
87
|
+
* avoid Error-instance / cycle serialization hazards). Consumers
|
|
88
|
+
* inspecting *which* field failed *how* read this; `fromWire` in
|
|
89
|
+
* `@kindgi/client` projects it back onto the client-side error.
|
|
90
|
+
*/
|
|
91
|
+
readonly validationIssues?: readonly Readonly<Record<string, unknown>>[];
|
|
92
|
+
/**
|
|
93
|
+
* The exact payload the model produced that failed validation.
|
|
94
|
+
* Included so authors debugging LLM-tool-arg drift can see whether
|
|
95
|
+
* the model sent `{}`, `{ location: "" }`, `{ city: "…" }`
|
|
96
|
+
* (wrong field name), etc. without instrumenting their handler.
|
|
97
|
+
* Size-capped at ~4KiB stringified — larger payloads truncate
|
|
98
|
+
* with a trailing `…`; sensitive tools should sanitize inputs
|
|
99
|
+
* upstream.
|
|
100
|
+
*/
|
|
101
|
+
readonly receivedInput?: unknown;
|
|
102
|
+
/**
|
|
103
|
+
* Failed tool calls sent back to the model this turn before this one
|
|
104
|
+
* ended it (see `Agent.toolErrors`). Present when the failure is a
|
|
105
|
+
* kind the turn retries and its retries ran out.
|
|
106
|
+
*/
|
|
107
|
+
readonly toolRetries?: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface BudgetExceededError {
|
|
111
|
+
readonly code: 'budget-exceeded';
|
|
112
|
+
readonly message: string;
|
|
113
|
+
readonly kind: 'steps' | 'cost' | 'wall-time';
|
|
114
|
+
readonly limit: number;
|
|
115
|
+
readonly observed: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The agent declares an `output` schema and its final answer still
|
|
120
|
+
* didn't fit after the allowed repairs. `errors` lists the last
|
|
121
|
+
* attempt's problems; `attempts` counts the answers checked.
|
|
122
|
+
*/
|
|
123
|
+
export interface OutputSchemaViolationError {
|
|
124
|
+
readonly code: 'output-schema-violation';
|
|
125
|
+
readonly message: string;
|
|
126
|
+
readonly errors: readonly string[];
|
|
127
|
+
readonly attempts: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface AgentTurnAbortedError {
|
|
131
|
+
readonly code: 'agent-turn-aborted';
|
|
132
|
+
readonly message: string;
|
|
133
|
+
readonly reason: 'external' | 'timeout';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Thrown by a node handler when a precondition fails or a runtime step
|
|
138
|
+
* short-circuits with a structured error. The kernel captures
|
|
139
|
+
* `Error.message` into `step.failed.payload.message`; we serialize the
|
|
140
|
+
* full structured error as JSON so `projectRunResult` can recover the
|
|
141
|
+
* discriminated union shape on the way out.
|
|
142
|
+
*
|
|
143
|
+
* Deliberately narrowed to `Error` subclass so `cause` becomes a plain
|
|
144
|
+
* property (JSON-round-trippable) instead of the native ErrorOptions
|
|
145
|
+
* behavior.
|
|
146
|
+
*/
|
|
147
|
+
export class AgentTurnFailure extends Error {
|
|
148
|
+
readonly payload: InvokeAgentError;
|
|
149
|
+
constructor(payload: InvokeAgentError) {
|
|
150
|
+
super(serializeError(payload));
|
|
151
|
+
this.name = 'AgentTurnFailure';
|
|
152
|
+
this.payload = payload;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const FAILURE_SENTINEL = '__agent_turn_failure__' as const;
|
|
157
|
+
|
|
158
|
+
interface SerializedFailure {
|
|
159
|
+
readonly [FAILURE_SENTINEL]: true;
|
|
160
|
+
readonly error: InvokeAgentError;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Encode an `InvokeAgentError` as a JSON string carried in an Error's
|
|
165
|
+
* message. The sentinel field lets `parseFailureMessage` distinguish
|
|
166
|
+
* our structured failures from other error messages that happen to be
|
|
167
|
+
* JSON-shaped.
|
|
168
|
+
*
|
|
169
|
+
* `cause` fields on inner errors are downgraded to their message form
|
|
170
|
+
* so the payload is JSON-safe.
|
|
171
|
+
*/
|
|
172
|
+
function serializeError(error: InvokeAgentError): string {
|
|
173
|
+
return JSON.stringify(
|
|
174
|
+
{
|
|
175
|
+
[FAILURE_SENTINEL]: true as const,
|
|
176
|
+
error: stripCauseFunctions(error),
|
|
177
|
+
} satisfies SerializedFailure,
|
|
178
|
+
(_key, value) => {
|
|
179
|
+
if (value instanceof Error) {
|
|
180
|
+
return { code: (value as { code?: string }).code, message: value.message };
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function stripCauseFunctions(error: InvokeAgentError): InvokeAgentError {
|
|
188
|
+
// JSON.stringify replacer above handles Error instances; nothing more
|
|
189
|
+
// to do here. A pass-through hook for error variants that embed
|
|
190
|
+
// callbacks.
|
|
191
|
+
return error;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Attempt to reconstruct a structured `InvokeAgentError` from a run's
|
|
196
|
+
* `failureMessage`. Returns `undefined` when the message is not one of
|
|
197
|
+
* ours (e.g. a raw kernel-level exception message).
|
|
198
|
+
*/
|
|
199
|
+
export function parseFailureMessage(raw: string | undefined): InvokeAgentError | undefined {
|
|
200
|
+
if (raw === undefined || raw.length === 0) return undefined;
|
|
201
|
+
// Fast path: the raw message IS the single serialized failure (one
|
|
202
|
+
// step.failed for the whole turn, which is the common case).
|
|
203
|
+
const whole = tryParseChunk(raw);
|
|
204
|
+
if (whole !== undefined) return whole;
|
|
205
|
+
// Fallback: the kernel joins multiple step.failed messages with '; '.
|
|
206
|
+
// Recover by extracting balanced-brace substrings and trying each.
|
|
207
|
+
for (const chunk of extractJsonCandidates(raw)) {
|
|
208
|
+
const parsed = tryParseChunk(chunk);
|
|
209
|
+
if (parsed !== undefined) return parsed;
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Extract substrings that look like well-formed JSON objects by
|
|
216
|
+
* scanning for balanced braces. Only single-level scan — inner strings
|
|
217
|
+
* are respected. Cheap parser used for the rare multi-failure case.
|
|
218
|
+
*/
|
|
219
|
+
function extractJsonCandidates(raw: string): string[] {
|
|
220
|
+
const out: string[] = [];
|
|
221
|
+
let depth = 0;
|
|
222
|
+
let start = -1;
|
|
223
|
+
let inString = false;
|
|
224
|
+
let escaped = false;
|
|
225
|
+
for (let i = 0; i < raw.length; i++) {
|
|
226
|
+
const ch = raw[i];
|
|
227
|
+
if (escaped) {
|
|
228
|
+
escaped = false;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (ch === '\\') {
|
|
232
|
+
escaped = true;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (ch === '"') {
|
|
236
|
+
inString = !inString;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (inString) continue;
|
|
240
|
+
if (ch === '{') {
|
|
241
|
+
if (depth === 0) start = i;
|
|
242
|
+
depth += 1;
|
|
243
|
+
} else if (ch === '}') {
|
|
244
|
+
depth -= 1;
|
|
245
|
+
if (depth === 0 && start >= 0) {
|
|
246
|
+
out.push(raw.slice(start, i + 1));
|
|
247
|
+
start = -1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function tryParseChunk(chunk: string): InvokeAgentError | undefined {
|
|
255
|
+
const trimmed = chunk.trim();
|
|
256
|
+
if (!trimmed.startsWith('{')) return undefined;
|
|
257
|
+
try {
|
|
258
|
+
const doc = JSON.parse(trimmed) as SerializedFailure | Record<string, unknown>;
|
|
259
|
+
if (
|
|
260
|
+
typeof doc === 'object' &&
|
|
261
|
+
doc !== null &&
|
|
262
|
+
(doc as SerializedFailure)[FAILURE_SENTINEL] === true
|
|
263
|
+
) {
|
|
264
|
+
return (doc as SerializedFailure).error;
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// Not our JSON — probably a raw JS Error.message. Fall through.
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Helper for handlers to short-circuit with a structured error. */
|
|
273
|
+
export function throwAgentTurnFailure(error: InvokeAgentError): never {
|
|
274
|
+
throw new AgentTurnFailure(error);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Placeholder to satisfy TS-strict "declared but unused" on
|
|
279
|
+
* `EvaluationResult` in ambient imports elsewhere; the type is used
|
|
280
|
+
* transitively via `GuardrailViolationError`.
|
|
281
|
+
*/
|
|
282
|
+
export type _EvaluationResultAlias = EvaluationResult;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { NodeHandler } from '@kindgi/handler';
|
|
5
|
+
import type { Timestamp } from '@kindgi/types';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildRunTrace,
|
|
9
|
+
categorizeOutcomes,
|
|
10
|
+
describeBlockingViolations,
|
|
11
|
+
evaluateGate,
|
|
12
|
+
} from '../guardrails-gate.js';
|
|
13
|
+
import { emitTurnEvent } from '../streaming.js';
|
|
14
|
+
import type { ConversationMessage } from '../types.js';
|
|
15
|
+
|
|
16
|
+
import type { TurnContext } from './context.js';
|
|
17
|
+
import { throwAgentTurnFailure } from './errors.js';
|
|
18
|
+
import { finalIteration } from './final-iteration.js';
|
|
19
|
+
import { parseJsonAnswer } from './structured-output.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Guardrail gate on the turn's final response, before it is stored.
|
|
23
|
+
* Receives the agent loop's output. A failed guardrail whose action is
|
|
24
|
+
* `halt` throws, so the run fails and the response never reaches the
|
|
25
|
+
* conversation; failures with any other action are stashed on `ctx` for
|
|
26
|
+
* `compose-result` to surface in `AgentTurnResult.violations`. Every
|
|
27
|
+
* failure emits a `guardrail.violated` event. Also records the
|
|
28
|
+
* response's `model-output` provenance node, which the guardrail checks
|
|
29
|
+
* link to.
|
|
30
|
+
*/
|
|
31
|
+
export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
|
|
32
|
+
return async (input: unknown, kctx) => {
|
|
33
|
+
if (ctx.guardrails === undefined) {
|
|
34
|
+
throwAgentTurnFailure({
|
|
35
|
+
code: 'model-invocation-failed',
|
|
36
|
+
message: 'evaluate-guardrails invoked before setup completed',
|
|
37
|
+
cause: null,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const final = finalIteration(input, 'evaluate-guardrails');
|
|
41
|
+
const evaluatedAt = new Date().toISOString() as Timestamp;
|
|
42
|
+
// The response as it would be stored; not persisted yet.
|
|
43
|
+
const response: ConversationMessage = {
|
|
44
|
+
sequence: -1,
|
|
45
|
+
role: 'agent',
|
|
46
|
+
content: final.message.content ?? '',
|
|
47
|
+
createdAt: evaluatedAt,
|
|
48
|
+
actor: ctx.input.agent.id,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (ctx.provenance !== undefined) {
|
|
52
|
+
ctx.provenance.addNode({
|
|
53
|
+
id: `model-output:${ctx.usage.steps}`,
|
|
54
|
+
kind: 'model-output',
|
|
55
|
+
timestamp: evaluatedAt,
|
|
56
|
+
modelVersion: `${final.provider.id}/${final.provider.model}`,
|
|
57
|
+
});
|
|
58
|
+
ctx.provenance.addEdge({
|
|
59
|
+
from: `model-output:${ctx.usage.steps}`,
|
|
60
|
+
to: `model-call:${ctx.usage.steps}`,
|
|
61
|
+
kind: 'produced',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const finalUsage = {
|
|
66
|
+
steps: ctx.usage.steps,
|
|
67
|
+
promptTokens: ctx.usage.promptTokens,
|
|
68
|
+
completionTokens: ctx.usage.completionTokens,
|
|
69
|
+
totalCostUsd: ctx.usage.totalCostUsd,
|
|
70
|
+
durationMs: Date.now() - ctx.startedAt,
|
|
71
|
+
};
|
|
72
|
+
const structured =
|
|
73
|
+
ctx.input.agent.output !== undefined && !kctx.dryRun
|
|
74
|
+
? parseJsonAnswer(final.message.content)
|
|
75
|
+
: undefined;
|
|
76
|
+
const trace = buildRunTrace({
|
|
77
|
+
runId: kctx.runId,
|
|
78
|
+
tenantId: ctx.input.tenantId,
|
|
79
|
+
projectId: ctx.input.projectId,
|
|
80
|
+
conversationId: ctx.input.conversationId,
|
|
81
|
+
turnNumber: (ctx.conversation?.turnCount ?? 0) + 1,
|
|
82
|
+
agent: ctx.input.agent,
|
|
83
|
+
userMessage: ctx.input.userMessage,
|
|
84
|
+
...(ctx.input.input !== undefined && { stepInput: ctx.input.input }),
|
|
85
|
+
...(structured?.kind === 'ok' && { structuredOutput: structured.value }),
|
|
86
|
+
appended: ctx.appended,
|
|
87
|
+
finalResponse: response,
|
|
88
|
+
usage: finalUsage,
|
|
89
|
+
});
|
|
90
|
+
const outcomes = await evaluateGate(
|
|
91
|
+
ctx.guardrails,
|
|
92
|
+
trace,
|
|
93
|
+
ctx.bindings,
|
|
94
|
+
ctx.tenantPolicy,
|
|
95
|
+
ctx.turnAbort.signal,
|
|
96
|
+
);
|
|
97
|
+
const categorized = categorizeOutcomes(outcomes);
|
|
98
|
+
|
|
99
|
+
const allViolations = [...categorized.blocking, ...categorized.warnings, ...categorized.other];
|
|
100
|
+
for (const v of allViolations) {
|
|
101
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
102
|
+
kind: 'guardrail.violated',
|
|
103
|
+
action: v.action,
|
|
104
|
+
severity: v.severity,
|
|
105
|
+
guardrailId: v.guardrailId,
|
|
106
|
+
...(v.result.reason !== undefined && { reason: v.result.reason }),
|
|
107
|
+
});
|
|
108
|
+
if (ctx.provenance !== undefined) {
|
|
109
|
+
ctx.provenance.addNode({
|
|
110
|
+
id: `guardrail-check:${v.guardrailId}`,
|
|
111
|
+
kind: 'guardrail-check',
|
|
112
|
+
timestamp: v.at,
|
|
113
|
+
attributes: {
|
|
114
|
+
guardrailId: v.guardrailId,
|
|
115
|
+
action: v.action,
|
|
116
|
+
severity: v.severity,
|
|
117
|
+
passed: false,
|
|
118
|
+
...(v.result.reason !== undefined && { reason: v.result.reason }),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
ctx.provenance.addEdge({
|
|
122
|
+
from: `guardrail-check:${v.guardrailId}`,
|
|
123
|
+
to: `model-output:${ctx.usage.steps}`,
|
|
124
|
+
kind: 'influenced-by',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (categorized.blocking.length > 0) {
|
|
130
|
+
const message = describeBlockingViolations(categorized.blocking);
|
|
131
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
132
|
+
kind: 'turn.failed',
|
|
133
|
+
conversationId: ctx.input.conversationId,
|
|
134
|
+
errorCode: 'guardrail-violation',
|
|
135
|
+
message,
|
|
136
|
+
});
|
|
137
|
+
throwAgentTurnFailure({
|
|
138
|
+
code: 'guardrail-violation',
|
|
139
|
+
message,
|
|
140
|
+
violations: categorized.blocking,
|
|
141
|
+
evaluationErrors: categorized.errors,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
ctx.nonBlockingViolations = [...categorized.warnings, ...categorized.other];
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
blocking: categorized.blocking.length,
|
|
149
|
+
warnings: categorized.warnings.length,
|
|
150
|
+
other: categorized.other.length,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { LoopNodeOutput } from '@kindgi/handler';
|
|
5
|
+
|
|
6
|
+
import type { AgentTurnIterationOutput } from './context.js';
|
|
7
|
+
import { throwAgentTurnFailure } from './errors.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The agent loop's final iteration — the response the turn ends with.
|
|
11
|
+
* `nodeName` names the reading node in the failure message.
|
|
12
|
+
*/
|
|
13
|
+
export function finalIteration(loopOutput: unknown, nodeName: string): AgentTurnIterationOutput {
|
|
14
|
+
const output = loopOutput as LoopNodeOutput<AgentTurnIterationOutput> | undefined;
|
|
15
|
+
if (output === undefined) {
|
|
16
|
+
throwAgentTurnFailure({
|
|
17
|
+
code: 'model-invocation-failed',
|
|
18
|
+
message: `${nodeName} received undefined loop output`,
|
|
19
|
+
cause: null,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (output.finalOutput === undefined) {
|
|
23
|
+
throwAgentTurnFailure({
|
|
24
|
+
code: 'model-invocation-failed',
|
|
25
|
+
message: `${nodeName} received loop output without finalOutput`,
|
|
26
|
+
cause: null,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return output.finalOutput;
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { HandlerRegistry, NodeHandler } from '@kindgi/handler';
|
|
5
|
+
import type { NodeId } from '@kindgi/types';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
AGENT_LOOP_NODE,
|
|
9
|
+
BUDGET_CHECK_NODE,
|
|
10
|
+
BUILD_INITIAL_MESSAGES_NODE,
|
|
11
|
+
COMPOSE_RESULT_NODE,
|
|
12
|
+
DISPATCH_TOOLS_NODE,
|
|
13
|
+
EVALUATE_GUARDRAILS_NODE,
|
|
14
|
+
MODEL_CALL_NODE,
|
|
15
|
+
PERSIST_FINAL_MESSAGE_NODE,
|
|
16
|
+
PERSIST_PROVENANCE_NODE,
|
|
17
|
+
PERSIST_USER_MESSAGE_NODE,
|
|
18
|
+
RENDER_PROMPT_NODE,
|
|
19
|
+
RUN_RETRIEVALS_NODE,
|
|
20
|
+
SETUP_NODE,
|
|
21
|
+
} from '../agent-turn-flow.js';
|
|
22
|
+
|
|
23
|
+
import { buildBudgetCheckHandler } from './budget-check.js';
|
|
24
|
+
import { buildBuildInitialMessagesHandler } from './build-initial-messages.js';
|
|
25
|
+
import { buildComposeResultHandler } from './compose-result.js';
|
|
26
|
+
import type { TurnContext } from './context.js';
|
|
27
|
+
import { buildDispatchToolsHandler } from './dispatch-tools.js';
|
|
28
|
+
import { buildEvaluateGuardrailsHandler } from './evaluate-guardrails.js';
|
|
29
|
+
import { buildModelCallHandler } from './model-call.js';
|
|
30
|
+
import { buildPersistFinalMessageHandler } from './persist-final-message.js';
|
|
31
|
+
import { buildPersistProvenanceHandler } from './persist-provenance.js';
|
|
32
|
+
import { buildPersistUserMessageHandler } from './persist-user-message.js';
|
|
33
|
+
import { buildRenderPromptHandler } from './render-prompt.js';
|
|
34
|
+
import { buildRunRetrievalsHandler } from './run-retrievals.js';
|
|
35
|
+
import { buildSetupHandler } from './setup.js';
|
|
36
|
+
|
|
37
|
+
// `agent-loop` is a kernel loop node — the executor dispatches it
|
|
38
|
+
// internally; only body-node handlers are registered.
|
|
39
|
+
export function buildHandlers(ctx: TurnContext): HandlerRegistry {
|
|
40
|
+
const entries: [NodeId, NodeHandler][] = [
|
|
41
|
+
[SETUP_NODE as NodeId, buildSetupHandler(ctx)],
|
|
42
|
+
[RENDER_PROMPT_NODE as NodeId, buildRenderPromptHandler(ctx)],
|
|
43
|
+
[PERSIST_USER_MESSAGE_NODE as NodeId, buildPersistUserMessageHandler(ctx)],
|
|
44
|
+
[RUN_RETRIEVALS_NODE as NodeId, buildRunRetrievalsHandler(ctx)],
|
|
45
|
+
[BUILD_INITIAL_MESSAGES_NODE as NodeId, buildBuildInitialMessagesHandler(ctx)],
|
|
46
|
+
[MODEL_CALL_NODE as NodeId, buildModelCallHandler(ctx)],
|
|
47
|
+
[DISPATCH_TOOLS_NODE as NodeId, buildDispatchToolsHandler(ctx)],
|
|
48
|
+
[BUDGET_CHECK_NODE as NodeId, buildBudgetCheckHandler(ctx)],
|
|
49
|
+
[PERSIST_FINAL_MESSAGE_NODE as NodeId, buildPersistFinalMessageHandler(ctx)],
|
|
50
|
+
[EVALUATE_GUARDRAILS_NODE as NodeId, buildEvaluateGuardrailsHandler(ctx)],
|
|
51
|
+
[PERSIST_PROVENANCE_NODE as NodeId, buildPersistProvenanceHandler(ctx)],
|
|
52
|
+
[COMPOSE_RESULT_NODE as NodeId, buildComposeResultHandler(ctx)],
|
|
53
|
+
];
|
|
54
|
+
// Assert `agent-loop` isn't accidentally registered — the runtime's
|
|
55
|
+
// flow executor rejects handlers for internal-dispatch nodes at load
|
|
56
|
+
// time. Sanity check for regressions.
|
|
57
|
+
if (entries.some(([id]) => id === (AGENT_LOOP_NODE as NodeId))) {
|
|
58
|
+
throw new Error('agent-loop must not have a handler — it is dispatched by the kernel');
|
|
59
|
+
}
|
|
60
|
+
return new Map(entries);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type { TurnContext };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { ModelCallInput, ModelMessage } from '@kindgi/capabilities';
|
|
5
|
+
import type { NodeHandler } from '@kindgi/handler';
|
|
6
|
+
|
|
7
|
+
import { emitTurnEvent } from '../streaming.js';
|
|
8
|
+
|
|
9
|
+
import type { AgentTurnIterationOutput, TurnContext } from './context.js';
|
|
10
|
+
import { throwAgentTurnFailure } from './errors.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Loop-body node #1. Invoke the model with the current
|
|
14
|
+
* `nextMessages` accumulator. Emits `model.call.started` +
|
|
15
|
+
* `model.call.completed`, records provenance for the model-call node,
|
|
16
|
+
* and accumulates usage on `ctx.usage`.
|
|
17
|
+
*
|
|
18
|
+
* Output is a partial `AgentTurnIterationOutput` — `dispatch-tools`
|
|
19
|
+
* receives it and finishes composing the iteration output.
|
|
20
|
+
*/
|
|
21
|
+
export function buildModelCallHandler(ctx: TurnContext): NodeHandler {
|
|
22
|
+
return async (input: unknown, kctx) => {
|
|
23
|
+
if (ctx.provider === undefined || ctx.model === undefined || ctx.tools === undefined) {
|
|
24
|
+
throwAgentTurnFailure({
|
|
25
|
+
code: 'model-invocation-failed',
|
|
26
|
+
message: 'model-call invoked before setup completed',
|
|
27
|
+
cause: null,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const shaped = input as { readonly nextMessages?: readonly ModelMessage[] } | undefined;
|
|
31
|
+
const nextMessages: readonly ModelMessage[] = shaped?.nextMessages ?? [];
|
|
32
|
+
|
|
33
|
+
ctx.usage.steps += 1;
|
|
34
|
+
|
|
35
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
36
|
+
kind: 'model.call.started',
|
|
37
|
+
step: ctx.usage.steps,
|
|
38
|
+
providerId: ctx.provider.metadata.id,
|
|
39
|
+
model: ctx.model.name,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let callResult: Awaited<ReturnType<typeof ctx.provider.invoke>>;
|
|
43
|
+
if (kctx.dryRun) {
|
|
44
|
+
// Dry-run: skip the network call. Return a schema-conformant
|
|
45
|
+
// mock indicating the model would have terminated cleanly. The
|
|
46
|
+
// mock requests no tools, so a dry run does not preview tool
|
|
47
|
+
// planning.
|
|
48
|
+
callResult = {
|
|
49
|
+
finishReason: 'stop',
|
|
50
|
+
message: { role: 'assistant', content: '[dry-run: model call skipped]' },
|
|
51
|
+
usage: { promptTokens: 0, completionTokens: 0 },
|
|
52
|
+
costUsd: 0,
|
|
53
|
+
durationMs: 0,
|
|
54
|
+
provider: { id: ctx.provider.metadata.id, model: ctx.model.name },
|
|
55
|
+
};
|
|
56
|
+
} else {
|
|
57
|
+
const callInput: ModelCallInput = {
|
|
58
|
+
model: ctx.model.name,
|
|
59
|
+
messages: nextMessages,
|
|
60
|
+
...(ctx.tools.definitions.length > 0 && {
|
|
61
|
+
tools: ctx.tools.definitions,
|
|
62
|
+
}),
|
|
63
|
+
abortSignal: ctx.turnAbort.signal,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
callResult = await ctx.provider.invoke(callInput);
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
if (ctx.turnAbort.signal.aborted) {
|
|
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
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ctx.usage.promptTokens += callResult.usage.promptTokens;
|
|
88
|
+
ctx.usage.completionTokens += callResult.usage.completionTokens;
|
|
89
|
+
ctx.usage.totalCostUsd += callResult.costUsd;
|
|
90
|
+
ctx.lastProvider = callResult.provider;
|
|
91
|
+
|
|
92
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
93
|
+
kind: 'model.call.completed',
|
|
94
|
+
step: ctx.usage.steps,
|
|
95
|
+
finishReason: callResult.finishReason,
|
|
96
|
+
promptTokens: callResult.usage.promptTokens,
|
|
97
|
+
completionTokens: callResult.usage.completionTokens,
|
|
98
|
+
costUsd: callResult.costUsd,
|
|
99
|
+
durationMs: callResult.durationMs,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (ctx.provenance !== undefined && ctx.userMessage !== undefined) {
|
|
103
|
+
const modelCallNodeId = `model-call:${ctx.usage.steps}`;
|
|
104
|
+
ctx.provenance.addNode({
|
|
105
|
+
id: modelCallNodeId,
|
|
106
|
+
kind: 'model-call',
|
|
107
|
+
timestamp: new Date().toISOString() as never,
|
|
108
|
+
modelVersion: `${callResult.provider.id}/${callResult.provider.model}`,
|
|
109
|
+
attributes: {
|
|
110
|
+
step: ctx.usage.steps,
|
|
111
|
+
promptTokens: callResult.usage.promptTokens,
|
|
112
|
+
completionTokens: callResult.usage.completionTokens,
|
|
113
|
+
costUsd: callResult.costUsd,
|
|
114
|
+
finishReason: callResult.finishReason,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
ctx.provenance.addEdge({
|
|
118
|
+
from: modelCallNodeId,
|
|
119
|
+
to: `input:${ctx.userMessage.sequence}`,
|
|
120
|
+
kind: 'caused-by',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Partial iteration output — `dispatch-tools` finishes it.
|
|
125
|
+
const partial: Omit<AgentTurnIterationOutput, 'iterationAppended' | 'finishedTurn'> & {
|
|
126
|
+
readonly step: number;
|
|
127
|
+
} = {
|
|
128
|
+
step: ctx.usage.steps,
|
|
129
|
+
finishReason: callResult.finishReason,
|
|
130
|
+
message: callResult.message,
|
|
131
|
+
iterationUsage: {
|
|
132
|
+
promptTokens: callResult.usage.promptTokens,
|
|
133
|
+
completionTokens: callResult.usage.completionTokens,
|
|
134
|
+
costUsd: callResult.costUsd,
|
|
135
|
+
},
|
|
136
|
+
provider: callResult.provider,
|
|
137
|
+
nextMessages,
|
|
138
|
+
};
|
|
139
|
+
return partial;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Longest cause text a failure message carries. */
|
|
144
|
+
const MAX_CAUSE_CHARS = 500;
|
|
145
|
+
|
|
146
|
+
/** A thrown value's message on one line, at most `MAX_CAUSE_CHARS` long. */
|
|
147
|
+
function describeCause(cause: unknown): string {
|
|
148
|
+
const text = (cause instanceof Error ? cause.message : String(cause)).replace(/\s+/g, ' ').trim();
|
|
149
|
+
if (text.length === 0) return cause instanceof Error ? cause.name : 'no detail';
|
|
150
|
+
return text.length > MAX_CAUSE_CHARS ? `${text.slice(0, MAX_CAUSE_CHARS - 1)}…` : text;
|
|
151
|
+
}
|