@navels/neal 0.2.0 → 0.3.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/README.md +206 -209
- package/SECURITY.md +16 -19
- package/dist/neal/adjudicator/{blocked-adjudicator.js → consultant.js} +21 -21
- package/dist/neal/adjudicator/execute.js +33 -33
- package/dist/neal/adjudicator/final-completion.js +2 -31
- package/dist/neal/adjudicator/planning.js +230 -38
- package/dist/neal/agents/prompts.js +6 -17
- package/dist/neal/agents/rounds.js +43 -73
- package/dist/neal/agents/schemas.js +47 -13
- package/dist/neal/blocked-guidance.js +16 -9
- package/dist/neal/cli.js +2 -2
- package/dist/neal/commands/check.js +2 -2
- package/dist/neal/commands/compat.js +100 -36
- package/dist/neal/commands/interactive-activity.js +2 -2
- package/dist/neal/commands/recovery-guidance.js +20 -8
- package/dist/neal/commands/runtime.js +4 -2
- package/dist/neal/config.js +16 -7
- package/dist/neal/context/inline-review-context.js +13 -28
- package/dist/neal/context/reviewer-context.js +19 -14
- package/dist/neal/eval/reviewer-eval-manifest.js +86 -0
- package/dist/neal/eval/reviewer-eval.js +155 -0
- package/dist/neal/interactive-controls.js +5 -0
- package/dist/neal/orchestrator/completion.js +49 -2
- package/dist/neal/orchestrator/failures.js +7 -0
- package/dist/neal/orchestrator/notifications.js +9 -9
- package/dist/neal/orchestrator/phases/planning.js +117 -80
- package/dist/neal/orchestrator/phases/recovery.js +151 -115
- package/dist/neal/orchestrator/phases/review.js +28 -1
- package/dist/neal/orchestrator/transitions.js +7 -1
- package/dist/neal/orchestrator.js +3 -0
- package/dist/neal/plan-queue.js +29 -1
- package/dist/neal/prompts/execute.js +7 -19
- package/dist/neal/prompts/planning.js +64 -55
- package/dist/neal/prompts/review-doctrine.js +13 -50
- package/dist/neal/prompts/specialized.js +7 -17
- package/dist/neal/prompts/specs.js +180 -32
- package/dist/neal/providers/agent-settings-isolation.js +35 -0
- package/dist/neal/providers/anthropic-claude.js +18 -1
- package/dist/neal/providers/detection.js +3 -7
- package/dist/neal/providers/git-config-isolation.js +83 -0
- package/dist/neal/providers/openai-codex.js +25 -3
- package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
- package/dist/neal/providers/openai-compatible.js +1078 -430
- package/dist/neal/providers/openrouter-routing.js +68 -0
- package/dist/neal/providers/pricing.js +1 -1
- package/dist/neal/providers/rate-card.js +6 -32
- package/dist/neal/providers/registry.js +6 -17
- package/dist/neal/providers/types.js +12 -0
- package/dist/neal/resume-decision.js +7 -6
- package/dist/neal/resume-planner.js +9 -0
- package/dist/neal/retrospective.js +3 -3
- package/dist/neal/review-debt.js +30 -0
- package/dist/neal/review-findings/provider.js +17 -29
- package/dist/neal/review.js +17 -1
- package/dist/neal/state-invariants.js +11 -3
- package/dist/neal/state-views.js +27 -5
- package/dist/neal/state.js +59 -15
- package/dist/neal/status.js +28 -1
- package/dist/neal/support.js +3 -3
- package/docs/README.md +22 -18
- package/docs/{ADJUDICATOR_INVENTORY.md → adjudicator-inventory.md} +58 -57
- package/docs/architecture.md +79 -83
- package/docs/automation.md +11 -11
- package/docs/compat.md +91 -61
- package/docs/compatible-models.md +145 -121
- package/docs/demo.md +15 -5
- package/docs/maintenance.md +65 -34
- package/docs/plan-format.md +61 -33
- package/docs/prompt-evals.md +96 -0
- package/docs/{PROMPT_SPECS.md → prompt-specs.md} +52 -48
- package/docs/providers.md +279 -349
- package/docs/release.md +63 -81
- package/docs/review-convergence.md +246 -0
- package/docs/state-machine.md +61 -59
- package/docs/storage.md +40 -31
- package/docs/troubleshooting.md +32 -29
- package/examples/compat/add-edit-verify/PLAN.md +2 -0
- package/examples/compat/add-edit-verify/broken.diff +2 -2
- package/examples/compat/add-edit-verify/good.diff +2 -2
- package/examples/compat/add-edit-verify/src/add.js +1 -1
- package/examples/compat/manifest.json +25 -29
- package/examples/compat/sum-grep-edit/PLAN.md +33 -0
- package/examples/compat/sum-grep-edit/broken.diff +12 -0
- package/examples/compat/sum-grep-edit/good.diff +12 -0
- package/examples/compat/sum-grep-edit/package.json +5 -0
- package/examples/compat/sum-grep-edit/src/numbers.js +9 -0
- package/examples/compat/sum-grep-edit/test/numbers.test.js +15 -0
- package/examples/issue-triage-js/README.md +0 -15
- package/examples/reviewer-eval/clean-doc/base/src/clamp.ts +3 -0
- package/examples/reviewer-eval/clean-doc/change.diff +9 -0
- package/examples/reviewer-eval/clean-extract/base/src/greet.ts +3 -0
- package/examples/reviewer-eval/clean-extract/change.diff +13 -0
- package/examples/reviewer-eval/dropped-error/base/src/load.ts +4 -0
- package/examples/reviewer-eval/dropped-error/change.diff +15 -0
- package/examples/reviewer-eval/manifest.json +45 -0
- package/examples/reviewer-eval/off-by-one/base/src/paginate.ts +5 -0
- package/examples/reviewer-eval/off-by-one/change.diff +11 -0
- package/{config.yml → neal.yml} +1 -1
- package/package.json +15 -14
- package/dist/neal/providers/generic-agentic.js +0 -1256
- package/docs/comparison.md +0 -105
- package/docs/issue-pipeline.md +0 -124
- package/examples/compat/is-even-add-test/PLAN.md +0 -30
- package/examples/compat/is-even-add-test/broken.diff +0 -11
- package/examples/compat/is-even-add-test/good.diff +0 -11
- package/examples/compat/is-even-add-test/package.json +0 -5
- package/examples/compat/is-even-add-test/src/is-even.js +0 -3
- package/examples/compat/is-even-add-test/test/is-even.test.js +0 -9
- package/examples/compat/reverse-grep-edit/PLAN.md +0 -32
- package/examples/compat/reverse-grep-edit/broken.diff +0 -12
- package/examples/compat/reverse-grep-edit/good.diff +0 -12
- package/examples/compat/reverse-grep-edit/package.json +0 -5
- package/examples/compat/reverse-grep-edit/src/strings.js +0 -10
- package/examples/compat/reverse-grep-edit/test/strings.test.js +0 -15
|
@@ -1,1256 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generic-agentic provider: neal's CoderAdapter contract implemented on the
|
|
3
|
-
* Vercel AI SDK, so any OpenAI-compatible endpoint with a strong tool-calling
|
|
4
|
-
* model can serve the coder role through ordinary config. Ported from the
|
|
5
|
-
* Phase 0 spike (`spike/generic-agentic/src/loop.ts`; copied in semantics,
|
|
6
|
-
* never imported) with the Phase 1 hardening from the integration plan:
|
|
7
|
-
*
|
|
8
|
-
* - one `generateText` call per turn (`stopWhen: stepCountIs(1)`,
|
|
9
|
-
* `maxRetries: 0`) with a neal-owned outer loop over an explicit message
|
|
10
|
-
* history; the AI SDK is never used as a multi-step black box;
|
|
11
|
-
* - per-turn bounded transient retry (`args.apiRetryLimit ?? 0`) with the
|
|
12
|
-
* standard `api_retry` `tool_progress` event shape and the bespoke
|
|
13
|
-
* adapter's backoff schedule (500 ms base, doubling, 5 s cap);
|
|
14
|
-
* - abort/inactivity wiring: every primary turn receives
|
|
15
|
-
* `AbortSignal.any([args.signal, AbortSignal.timeout(inactivityTimeoutMs)])`
|
|
16
|
-
* (the caller signal is omitted when absent). A caller-signal abort is a
|
|
17
|
-
* non-retryable `timeout`; an inactivity expiry is a retryable `timeout`;
|
|
18
|
-
* the two are disambiguated by checking `args.signal?.aborted` after the
|
|
19
|
-
* abort (`AbortSignal.timeout` aborts with a `TimeoutError` DOMException);
|
|
20
|
-
* - structured output (coder and structured-advisor alike) through the AI
|
|
21
|
-
* SDK's native structured-output channel: the tool loop runs to its normal
|
|
22
|
-
* zero-tool-call completion turn, then exactly one dedicated finalization
|
|
23
|
-
* turn carries `output: Output.object(...)` (see
|
|
24
|
-
* `runStructuredFinalizationTurn`, shared by both paths); there are no
|
|
25
|
-
* fence instructions and no repair turns on this provider;
|
|
26
|
-
* - events-only synthetic session handles: `supportsSessionResume: false`,
|
|
27
|
-
* so `args.onSessionStarted` is never invoked, returned session handles
|
|
28
|
-
* are always null, and a non-null `args.resumeHandle` is corrupted state
|
|
29
|
-
* (`session_unavailable`);
|
|
30
|
-
* - a structured-advisor adapter running the same outer loop over the
|
|
31
|
-
* read-only toolset (`read_file`, `list_dir`, `grep`, `git_diff`) with its own smaller
|
|
32
|
-
* step cap (`GENERIC_AGENTIC_ADVISOR_MAX_STEPS`), the same settings
|
|
33
|
-
* resolution, retry semantics, and cancellation contract, so the coder
|
|
34
|
-
* role passes the final-completion structured-advisor gate and a generic
|
|
35
|
-
* reviewer inspects the repository directly with read tools instead of
|
|
36
|
-
* relying on Neal-inlined context (Phase 2).
|
|
37
|
-
*
|
|
38
|
-
* The transient classification table, backoff schedule, and `api_retry`
|
|
39
|
-
* event shape mirror the module-private helpers in `openai-compatible.ts`
|
|
40
|
-
* by local reimplementation — that file is intentionally untouched and its
|
|
41
|
-
* helpers stay private; behavioral parity is enforced by the test batteries.
|
|
42
|
-
*/
|
|
43
|
-
import { randomBytes } from 'node:crypto';
|
|
44
|
-
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
45
|
-
import { generateText, jsonSchema, NoObjectGeneratedError, Output, stepCountIs, } from 'ai';
|
|
46
|
-
import { getOpenAICompatibleSettings } from '../config.js';
|
|
47
|
-
import { resolveRateCost } from './pricing.js';
|
|
48
|
-
import { createCoderToolset, createPlanAuthorToolset, createReadOnlyToolset, } from './generic-agentic-tools.js';
|
|
49
|
-
import { NealProviderError } from './types.js';
|
|
50
|
-
const GENERIC_AGENTIC_PROVIDER_ID = 'generic-agentic';
|
|
51
|
-
/**
|
|
52
|
-
* Step cap for the coder loop: the maximum number of successful model turns
|
|
53
|
-
* per prompt before the adapter fails the attempt with `provider_failed`.
|
|
54
|
-
* This is a constant, not a config knob, by design. It originally shipped at
|
|
55
|
-
* 24, and live runs on `examples/issue-triage-js` hit that cap repeatedly on
|
|
56
|
-
* ordinary scopes (one tool call per turn means read/inspect/edit/test cycles
|
|
57
|
-
* consume turns quickly), so it was raised to 48. Raise it again only on live
|
|
58
|
-
* evidence that the cap binds on real projects, not speculatively.
|
|
59
|
-
*/
|
|
60
|
-
export const GENERIC_AGENTIC_MAX_STEPS = 48;
|
|
61
|
-
/**
|
|
62
|
-
* Step cap for the structured-advisor read-only tool loop: the maximum number
|
|
63
|
-
* of successful model turns per round before the adapter fails the round with
|
|
64
|
-
* a non-retryable `provider_failed`. A constant, not a config knob, by design
|
|
65
|
-
* (mirroring `GENERIC_AGENTIC_MAX_STEPS`). It ships at half the coder cap
|
|
66
|
-
* because reviews are bounded inspections, not implementations: the advisor
|
|
67
|
-
* only reads, lists, and greps before judging, while the coder's
|
|
68
|
-
* read/inspect/edit/test cycles consume turns far faster. Provider telemetry
|
|
69
|
-
* records tool turns per review round; raise this cap only on live evidence
|
|
70
|
-
* that reviews hit it, the same way the coder cap moved 24 -> 48.
|
|
71
|
-
*/
|
|
72
|
-
export const GENERIC_AGENTIC_ADVISOR_MAX_STEPS = 24;
|
|
73
|
-
const API_RETRY_BASE_DELAY_MS = 500;
|
|
74
|
-
const API_RETRY_MAX_DELAY_MS = 5_000;
|
|
75
|
-
// Network-level error codes treated as transient, mirroring the spike's
|
|
76
|
-
// isTransientError classification (statusCode 408/429/5xx, explicit
|
|
77
|
-
// isRetryable, or one of these codes on the error or its cause).
|
|
78
|
-
const NETWORK_ERROR_CODES = new Set([
|
|
79
|
-
'ECONNRESET',
|
|
80
|
-
'ECONNREFUSED',
|
|
81
|
-
'ETIMEDOUT',
|
|
82
|
-
'EPIPE',
|
|
83
|
-
'ENOTFOUND',
|
|
84
|
-
'EAI_AGAIN',
|
|
85
|
-
'UND_ERR_SOCKET',
|
|
86
|
-
'UND_ERR_CONNECT_TIMEOUT',
|
|
87
|
-
]);
|
|
88
|
-
function defaultSleep(ms) {
|
|
89
|
-
return new Promise((resolve) => {
|
|
90
|
-
setTimeout(resolve, ms);
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
function createDefaultGenericAgenticModel(args) {
|
|
94
|
-
// Exactly the spike's model construction: an OpenAI-compatible chat model.
|
|
95
|
-
// maxRetries: 0 on each generateText call keeps neal's own apiRetryLimit
|
|
96
|
-
// loop the only retry layer for this provider.
|
|
97
|
-
//
|
|
98
|
-
// supportsStructuredOutputs: true tells the SDK to send the structured
|
|
99
|
-
// finalization turn's request with `response_format.type: 'json_schema'`
|
|
100
|
-
// carrying neal's schema (the `Output.object`/`jsonSchema` constraint in
|
|
101
|
-
// runAgentModelTurn). Without it the SDK silently drops the schema,
|
|
102
|
-
// downgrades to loose `json_object`, and emits the request-build warning
|
|
103
|
-
// "JSON response format schema is only supported with structuredOutputs" —
|
|
104
|
-
// a silent schema-drop that makes neal ask for enforced JSON but receive
|
|
105
|
-
// unenforced JSON. With the flag set, a gateway that cannot honor the
|
|
106
|
-
// schema fails attributably instead.
|
|
107
|
-
return createOpenAICompatible({
|
|
108
|
-
name: GENERIC_AGENTIC_PROVIDER_ID,
|
|
109
|
-
baseURL: args.baseUrl,
|
|
110
|
-
apiKey: args.apiKey,
|
|
111
|
-
headers: args.headers,
|
|
112
|
-
supportsStructuredOutputs: true,
|
|
113
|
-
}).chatModel(args.model);
|
|
114
|
-
}
|
|
115
|
-
function createGenericAgenticProviderError(args) {
|
|
116
|
-
return new NealProviderError({
|
|
117
|
-
message: args.message,
|
|
118
|
-
provider: GENERIC_AGENTIC_PROVIDER_ID,
|
|
119
|
-
role: args.role,
|
|
120
|
-
sessionHandle: args.sessionHandle,
|
|
121
|
-
kind: args.kind,
|
|
122
|
-
retryable: args.retryable,
|
|
123
|
-
cause: args.cause,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
function buildSyntheticSessionHandle() {
|
|
127
|
-
// Synthetic events-only handle. This provider has no session resume;
|
|
128
|
-
// adapter methods always return sessionHandle: null and never invoke
|
|
129
|
-
// onSessionStarted, so no resumable handle is ever persisted for it.
|
|
130
|
-
return `${GENERIC_AGENTIC_PROVIDER_ID}:${new Date().toISOString()}:${randomBytes(4).toString('hex')}`;
|
|
131
|
-
}
|
|
132
|
-
function withEventsOnlySessionHandle(error) {
|
|
133
|
-
// The synthetic session handle is events-only telemetry and must never ride
|
|
134
|
-
// on thrown errors: the orchestrator persists `error.sessionHandle` into
|
|
135
|
-
// `state.coderSessionHandle` on coder-phase and final-completion failures
|
|
136
|
-
// (src/neal/orchestrator/phases/coder.ts, src/neal/orchestrator/completion.ts),
|
|
137
|
-
// and any persisted handle makes `assertAgentConfigSupportsResume` demand
|
|
138
|
-
// session_resume support — which this provider declares false — rejecting
|
|
139
|
-
// `neal resume` for the whole run.
|
|
140
|
-
if (error.sessionHandle === null) {
|
|
141
|
-
return error;
|
|
142
|
-
}
|
|
143
|
-
const stripped = new NealProviderError({
|
|
144
|
-
message: error.message,
|
|
145
|
-
provider: GENERIC_AGENTIC_PROVIDER_ID,
|
|
146
|
-
role: error.role,
|
|
147
|
-
sessionHandle: null,
|
|
148
|
-
kind: error.kind,
|
|
149
|
-
retryable: error.retryable,
|
|
150
|
-
cause: error.cause,
|
|
151
|
-
});
|
|
152
|
-
if (error.stack !== undefined) {
|
|
153
|
-
stripped.stack = error.stack;
|
|
154
|
-
}
|
|
155
|
-
return stripped;
|
|
156
|
-
}
|
|
157
|
-
function getApiRetryDelayMs(retryCount) {
|
|
158
|
-
return Math.min(API_RETRY_BASE_DELAY_MS * 2 ** (retryCount - 1), API_RETRY_MAX_DELAY_MS);
|
|
159
|
-
}
|
|
160
|
-
function readStatusCode(error) {
|
|
161
|
-
const status = error.statusCode;
|
|
162
|
-
return typeof status === 'number' && Number.isFinite(status) ? status : null;
|
|
163
|
-
}
|
|
164
|
-
function isAbortLikeError(error) {
|
|
165
|
-
// AbortSignal.timeout aborts with a DOMException named TimeoutError on
|
|
166
|
-
// Node >= 22; caller AbortController.abort() defaults to AbortError. The
|
|
167
|
-
// AI SDK propagates the abort reason when maxRetries is 0.
|
|
168
|
-
const name = error.name;
|
|
169
|
-
return name === 'TimeoutError' || name === 'AbortError';
|
|
170
|
-
}
|
|
171
|
-
function hasTransientShape(error) {
|
|
172
|
-
if (error.isRetryable === true) {
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
const status = readStatusCode(error);
|
|
176
|
-
if (status !== null && (status === 408 || status === 429 || status >= 500)) {
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
const cause = error.cause;
|
|
180
|
-
const codes = [
|
|
181
|
-
error.code,
|
|
182
|
-
typeof cause === 'object' && cause !== null ? cause.code : undefined,
|
|
183
|
-
];
|
|
184
|
-
return codes.some((code) => typeof code === 'string' && NETWORK_ERROR_CODES.has(code));
|
|
185
|
-
}
|
|
186
|
-
function describeError(error) {
|
|
187
|
-
if (error instanceof Error) {
|
|
188
|
-
return error.message;
|
|
189
|
-
}
|
|
190
|
-
if (typeof error === 'object' && error !== null) {
|
|
191
|
-
const candidate = error;
|
|
192
|
-
const message = typeof candidate.message === 'string' ? candidate.message : String(error);
|
|
193
|
-
return typeof candidate.statusCode === 'number'
|
|
194
|
-
? `HTTP ${candidate.statusCode}: ${message}`
|
|
195
|
-
: message;
|
|
196
|
-
}
|
|
197
|
-
return String(error);
|
|
198
|
-
}
|
|
199
|
-
const MAX_ERROR_DIAGNOSTIC_TEXT_LENGTH = 2_000;
|
|
200
|
-
const MAX_ERROR_CAUSE_DEPTH = 3;
|
|
201
|
-
function truncateDiagnosticText(text) {
|
|
202
|
-
if (text.length <= MAX_ERROR_DIAGNOSTIC_TEXT_LENGTH) {
|
|
203
|
-
return text;
|
|
204
|
-
}
|
|
205
|
-
return `${text.slice(0, MAX_ERROR_DIAGNOSTIC_TEXT_LENGTH)}...[truncated ${text.length - MAX_ERROR_DIAGNOSTIC_TEXT_LENGTH} chars]`;
|
|
206
|
-
}
|
|
207
|
-
function readErrorProperty(error, key) {
|
|
208
|
-
if (typeof error !== 'object' || error === null) {
|
|
209
|
-
return undefined;
|
|
210
|
-
}
|
|
211
|
-
return error[key];
|
|
212
|
-
}
|
|
213
|
-
function buildErrorDiagnostic(error, depth = 0) {
|
|
214
|
-
const diagnostic = {
|
|
215
|
-
message: describeError(error),
|
|
216
|
-
};
|
|
217
|
-
if (error instanceof Error && error.name) {
|
|
218
|
-
diagnostic.name = error.name;
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
const name = readErrorProperty(error, 'name');
|
|
222
|
-
if (typeof name === 'string' && name.trim() !== '') {
|
|
223
|
-
diagnostic.name = name;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const statusCode = readErrorProperty(error, 'statusCode');
|
|
227
|
-
if (typeof statusCode === 'number') {
|
|
228
|
-
diagnostic.statusCode = statusCode;
|
|
229
|
-
}
|
|
230
|
-
const url = readErrorProperty(error, 'url');
|
|
231
|
-
if (typeof url === 'string' && url.trim() !== '') {
|
|
232
|
-
diagnostic.url = url;
|
|
233
|
-
}
|
|
234
|
-
const responseBody = readErrorProperty(error, 'responseBody');
|
|
235
|
-
if (typeof responseBody === 'string' && responseBody.trim() !== '') {
|
|
236
|
-
diagnostic.responseBody = truncateDiagnosticText(responseBody);
|
|
237
|
-
diagnostic.responseBodyTruncated = responseBody.length > MAX_ERROR_DIAGNOSTIC_TEXT_LENGTH;
|
|
238
|
-
}
|
|
239
|
-
// Rejected structured-output text: NoObjectGeneratedError carries the
|
|
240
|
-
// assistant text it could not parse as `text`, and the adapter constructs
|
|
241
|
-
// validator-failure causes with the generated JSON as `text`, so failed
|
|
242
|
-
// structured runs stay classifiable ("emitted XML garbage" vs "wrote a real
|
|
243
|
-
// patch and fumbled the payload") from the provider_error event alone.
|
|
244
|
-
const text = readErrorProperty(error, 'text');
|
|
245
|
-
if (typeof text === 'string' && text.trim() !== '') {
|
|
246
|
-
diagnostic.text = truncateDiagnosticText(text);
|
|
247
|
-
diagnostic.textTruncated = text.length > MAX_ERROR_DIAGNOSTIC_TEXT_LENGTH;
|
|
248
|
-
}
|
|
249
|
-
const cause = readErrorProperty(error, 'cause');
|
|
250
|
-
if (cause !== undefined && depth < MAX_ERROR_CAUSE_DEPTH) {
|
|
251
|
-
diagnostic.cause = buildErrorDiagnostic(cause, depth + 1);
|
|
252
|
-
}
|
|
253
|
-
return diagnostic;
|
|
254
|
-
}
|
|
255
|
-
function providerErrorData(error) {
|
|
256
|
-
return {
|
|
257
|
-
retryable: error.retryable,
|
|
258
|
-
diagnostic: buildErrorDiagnostic(error.cause ?? error),
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Normalizes any thrown value into a NealProviderError with the same
|
|
263
|
-
* classification table as the bespoke openai-compatible adapter plus the
|
|
264
|
-
* abort-source disambiguation contract:
|
|
265
|
-
*
|
|
266
|
-
* - caller signal aborted -> `timeout`, retryable: false (the caller owns
|
|
267
|
-
* the cancellation; never retried in-round);
|
|
268
|
-
* - abort without a caller abort -> inactivity expiry -> `timeout`,
|
|
269
|
-
* retryable: true;
|
|
270
|
-
* - HTTP 401/403 -> `permission_denied`, non-retryable;
|
|
271
|
-
* - HTTP 408/429/5xx, explicit isRetryable, network codes -> `api_error`,
|
|
272
|
-
* retryable;
|
|
273
|
-
* - everything else -> `provider_failed`, non-retryable.
|
|
274
|
-
*/
|
|
275
|
-
function normalizeGenericAgenticError(error, ctx) {
|
|
276
|
-
if (error instanceof NealProviderError) {
|
|
277
|
-
return error;
|
|
278
|
-
}
|
|
279
|
-
if (ctx.callerSignal?.aborted) {
|
|
280
|
-
return createGenericAgenticProviderError({
|
|
281
|
-
message: `generic-agentic ${ctx.label} turn was aborted by the caller before completion.`,
|
|
282
|
-
role: ctx.role,
|
|
283
|
-
sessionHandle: ctx.sessionHandle,
|
|
284
|
-
kind: 'timeout',
|
|
285
|
-
retryable: false,
|
|
286
|
-
cause: error,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
if (typeof error === 'object' && error !== null) {
|
|
290
|
-
if (isAbortLikeError(error)) {
|
|
291
|
-
return createGenericAgenticProviderError({
|
|
292
|
-
message: `generic-agentic ${ctx.label} turn exceeded the inactivity timeout and was aborted.`,
|
|
293
|
-
role: ctx.role,
|
|
294
|
-
sessionHandle: ctx.sessionHandle,
|
|
295
|
-
kind: 'timeout',
|
|
296
|
-
retryable: true,
|
|
297
|
-
cause: error,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
const status = readStatusCode(error);
|
|
301
|
-
if (status === 401 || status === 403) {
|
|
302
|
-
return createGenericAgenticProviderError({
|
|
303
|
-
message: `generic-agentic ${ctx.label} request was rejected with HTTP ${status}: ${describeError(error)}`,
|
|
304
|
-
role: ctx.role,
|
|
305
|
-
sessionHandle: ctx.sessionHandle,
|
|
306
|
-
kind: 'permission_denied',
|
|
307
|
-
retryable: false,
|
|
308
|
-
cause: error,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
if (hasTransientShape(error)) {
|
|
312
|
-
return createGenericAgenticProviderError({
|
|
313
|
-
message: `generic-agentic ${ctx.label} request failed transiently: ${describeError(error)}`,
|
|
314
|
-
role: ctx.role,
|
|
315
|
-
sessionHandle: ctx.sessionHandle,
|
|
316
|
-
kind: 'api_error',
|
|
317
|
-
retryable: true,
|
|
318
|
-
cause: error,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return createGenericAgenticProviderError({
|
|
323
|
-
message: `generic-agentic ${ctx.label} request failed: ${describeError(error)}`,
|
|
324
|
-
role: ctx.role,
|
|
325
|
-
sessionHandle: ctx.sessionHandle,
|
|
326
|
-
kind: 'provider_failed',
|
|
327
|
-
retryable: false,
|
|
328
|
-
cause: error,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
function createTurnAbortController(callerSignal, inactivityTimeoutMs) {
|
|
332
|
-
const controller = new AbortController();
|
|
333
|
-
const abortFromCaller = () => {
|
|
334
|
-
controller.abort(callerSignal?.reason);
|
|
335
|
-
};
|
|
336
|
-
if (callerSignal?.aborted) {
|
|
337
|
-
abortFromCaller();
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
callerSignal?.addEventListener('abort', abortFromCaller, { once: true });
|
|
341
|
-
}
|
|
342
|
-
// Keep this as an ordinary ref'ed timer. In Node 22, AbortSignal.timeout()
|
|
343
|
-
// can let a mocked or idle SDK call be cancelled by node:test before the
|
|
344
|
-
// timeout fires because the process has no remaining ref'ed handles.
|
|
345
|
-
const timeout = setTimeout(() => {
|
|
346
|
-
controller.abort(new DOMException('The operation timed out.', 'TimeoutError'));
|
|
347
|
-
}, inactivityTimeoutMs);
|
|
348
|
-
return {
|
|
349
|
-
signal: controller.signal,
|
|
350
|
-
dispose: () => {
|
|
351
|
-
clearTimeout(timeout);
|
|
352
|
-
callerSignal?.removeEventListener('abort', abortFromCaller);
|
|
353
|
-
},
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
async function emitProviderEvent(events, event) {
|
|
357
|
-
await events?.(event);
|
|
358
|
-
}
|
|
359
|
-
function resolveGenericAgenticSettings(args) {
|
|
360
|
-
const { context } = args;
|
|
361
|
-
let settings;
|
|
362
|
-
try {
|
|
363
|
-
settings = args.resolveSettings(args.cwd);
|
|
364
|
-
}
|
|
365
|
-
catch (error) {
|
|
366
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
367
|
-
throw createGenericAgenticProviderError({
|
|
368
|
-
message: `generic-agentic ${context.noun} settings could not be resolved: ${message}`,
|
|
369
|
-
role: context.role,
|
|
370
|
-
kind: 'provider_failed',
|
|
371
|
-
retryable: false,
|
|
372
|
-
cause: error,
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
if (!settings.baseUrl) {
|
|
376
|
-
throw createGenericAgenticProviderError({
|
|
377
|
-
message: `generic-agentic ${context.subject} has no base URL. ` +
|
|
378
|
-
'Set providers.openai_compatible.base_url in config.yml or the OPENAI_COMPATIBLE_BASE_URL environment variable.',
|
|
379
|
-
role: context.role,
|
|
380
|
-
kind: 'provider_failed',
|
|
381
|
-
retryable: false,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
const model = args.roundModel ?? args.roleModel ?? settings.defaultModel;
|
|
385
|
-
if (!model) {
|
|
386
|
-
throw createGenericAgenticProviderError({
|
|
387
|
-
message: `generic-agentic ${context.subject} has no model. ` +
|
|
388
|
-
`Set a role model override (for example ${context.modelOverrideExample}), providers.openai_compatible.default_model in config.yml, or the OPENAI_COMPATIBLE_MODEL environment variable.`,
|
|
389
|
-
role: context.role,
|
|
390
|
-
kind: 'provider_failed',
|
|
391
|
-
retryable: false,
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
if (!settings.apiKey) {
|
|
395
|
-
throw createGenericAgenticProviderError({
|
|
396
|
-
message: `generic-agentic ${context.subject} has no API key. ` +
|
|
397
|
-
`Set the ${settings.apiKeyEnv} environment variable.`,
|
|
398
|
-
role: context.role,
|
|
399
|
-
kind: 'permission_denied',
|
|
400
|
-
retryable: false,
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
return {
|
|
404
|
-
baseUrl: settings.baseUrl,
|
|
405
|
-
apiKey: settings.apiKey,
|
|
406
|
-
model,
|
|
407
|
-
headers: settings.headers,
|
|
408
|
-
pricing: settings.pricing,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
const CODER_STEP_CAP = {
|
|
412
|
-
limit: GENERIC_AGENTIC_MAX_STEPS,
|
|
413
|
-
constantName: 'GENERIC_AGENTIC_MAX_STEPS',
|
|
414
|
-
loopDescription: 'coder loop',
|
|
415
|
-
};
|
|
416
|
-
function advisorStepCap(label) {
|
|
417
|
-
return {
|
|
418
|
-
limit: GENERIC_AGENTIC_ADVISOR_MAX_STEPS,
|
|
419
|
-
constantName: 'GENERIC_AGENTIC_ADVISOR_MAX_STEPS',
|
|
420
|
-
loopDescription: `${label} advisor loop`,
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Maps toolset events onto the provider runtime event grammar (adding the
|
|
425
|
-
* base fields for the owning role) and maintains the cumulative per-tool
|
|
426
|
-
* telemetry maps. The toolset hook is synchronous, so async sink results are
|
|
427
|
-
* fire-and-forget; sink failures must never turn a tool result into a thrown
|
|
428
|
-
* error.
|
|
429
|
-
*/
|
|
430
|
-
function forwardAgentToolEvent(event, ctx) {
|
|
431
|
-
const base = {
|
|
432
|
-
provider: GENERIC_AGENTIC_PROVIDER_ID,
|
|
433
|
-
role: ctx.role,
|
|
434
|
-
...(ctx.label !== undefined ? { label: ctx.label } : {}),
|
|
435
|
-
sessionHandle: ctx.sessionHandle,
|
|
436
|
-
};
|
|
437
|
-
const forward = (runtimeEvent) => {
|
|
438
|
-
if (!ctx.events) {
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
try {
|
|
442
|
-
void Promise.resolve(ctx.events(runtimeEvent)).catch(() => { });
|
|
443
|
-
}
|
|
444
|
-
catch {
|
|
445
|
-
// Telemetry must never break tool execution.
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
switch (event.type) {
|
|
449
|
-
case 'tool_started':
|
|
450
|
-
ctx.state.toolCalls[event.toolName] = (ctx.state.toolCalls[event.toolName] ?? 0) + 1;
|
|
451
|
-
forward({ type: 'tool_started', ...base, toolName: event.toolName });
|
|
452
|
-
return;
|
|
453
|
-
case 'tool_progress':
|
|
454
|
-
if (event.isError) {
|
|
455
|
-
ctx.state.toolErrors[event.toolName] = (ctx.state.toolErrors[event.toolName] ?? 0) + 1;
|
|
456
|
-
}
|
|
457
|
-
forward({
|
|
458
|
-
type: 'tool_progress',
|
|
459
|
-
...base,
|
|
460
|
-
toolName: event.toolName,
|
|
461
|
-
message: event.message,
|
|
462
|
-
isError: event.isError,
|
|
463
|
-
});
|
|
464
|
-
return;
|
|
465
|
-
case 'command_completed':
|
|
466
|
-
forward({
|
|
467
|
-
type: 'command_completed',
|
|
468
|
-
...base,
|
|
469
|
-
command: event.command,
|
|
470
|
-
exitCode: event.exitCode,
|
|
471
|
-
output: event.output,
|
|
472
|
-
cwd: event.cwd,
|
|
473
|
-
});
|
|
474
|
-
return;
|
|
475
|
-
case 'file_changed':
|
|
476
|
-
forward({ type: 'file_changed', ...base, files: event.files });
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* The outer tool loop: pushes the prompt onto the shared history and runs
|
|
482
|
-
* model turns (each with its own bounded transient retry) until a turn
|
|
483
|
-
* makes zero tool calls — its assistant text is the loop's completion text
|
|
484
|
-
* — or the step cap is exhausted (`provider_failed`).
|
|
485
|
-
*
|
|
486
|
-
* Discovery (recorded, ai@6.0.202): a tool call whose input fails the tool's
|
|
487
|
-
* inputSchema does NOT throw out of `generateText`. The SDK's parseToolCall
|
|
488
|
-
* catches `InvalidToolInputError`, marks the call `invalid: true,
|
|
489
|
-
* dynamic: true` (without executing the tool), and the generate loop feeds an
|
|
490
|
-
* `error-text` tool-result ("Invalid input for tool <name>: ...") back to the
|
|
491
|
-
* model. The invalid call still appears in `result.toolCalls`, so this loop
|
|
492
|
-
* counts it as a tool-calling turn and continues — the model sees the error
|
|
493
|
-
* and self-corrects or runs into the step cap. That native feedback behavior
|
|
494
|
-
* is the whole strict-input contract; no coercion or repair hook wraps it.
|
|
495
|
-
*/
|
|
496
|
-
async function runAgentToolLoop(ctx, prompt) {
|
|
497
|
-
ctx.state.messages.push({ role: 'user', content: prompt });
|
|
498
|
-
while (true) {
|
|
499
|
-
if (ctx.state.steps >= ctx.stepCap.limit) {
|
|
500
|
-
throw createGenericAgenticProviderError({
|
|
501
|
-
message: `generic-agentic ${ctx.stepCap.loopDescription} reached the ${ctx.stepCap.constantName} cap of ` +
|
|
502
|
-
`${ctx.stepCap.limit} model turns without a completion turn (a turn with zero tool calls).`,
|
|
503
|
-
role: ctx.role,
|
|
504
|
-
sessionHandle: ctx.sessionHandle,
|
|
505
|
-
kind: 'provider_failed',
|
|
506
|
-
retryable: false,
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
const turn = await runAgentModelTurn(ctx, { useTools: true });
|
|
510
|
-
ctx.state.messages.push(...turn.responseMessages);
|
|
511
|
-
if (turn.toolCallCount > 0) {
|
|
512
|
-
// Completion is structural only: a model that narrates completion
|
|
513
|
-
// while still calling tools keeps looping until it makes a turn with
|
|
514
|
-
// zero tool calls or exhausts the step cap (channel rule — no
|
|
515
|
-
// substring classification on content channels).
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
return turn.assistantText;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
/**
|
|
522
|
-
* One model turn: emits `turn_started`, makes a single `generateText` call
|
|
523
|
-
* (`stopWhen: stepCountIs(1)`, `maxRetries: 0`, abort/inactivity signal),
|
|
524
|
-
* applies the missing-content rule, emits `assistant_text` (when text is
|
|
525
|
-
* non-empty) then `usage_reported` and `turn_completed` with the
|
|
526
|
-
* cumulative per-tool telemetry providerData. Transient failures retry up
|
|
527
|
-
* to `apiRetryLimit` times with backoff and the standard `api_retry`
|
|
528
|
-
* event; a successful call consumes one step.
|
|
529
|
-
*/
|
|
530
|
-
async function runAgentModelTurn(ctx, turnOptions) {
|
|
531
|
-
const base = {
|
|
532
|
-
provider: GENERIC_AGENTIC_PROVIDER_ID,
|
|
533
|
-
role: ctx.role,
|
|
534
|
-
...(ctx.label !== undefined ? { label: ctx.label } : {}),
|
|
535
|
-
sessionHandle: ctx.sessionHandle,
|
|
536
|
-
};
|
|
537
|
-
let apiRetryCount = 0;
|
|
538
|
-
while (true) {
|
|
539
|
-
try {
|
|
540
|
-
// turn_started before each model call so startup silence is
|
|
541
|
-
// observable by the liveness watchdog.
|
|
542
|
-
await emitProviderEvent(ctx.events, { type: 'turn_started', ...base });
|
|
543
|
-
const turnAbort = createTurnAbortController(ctx.signal, ctx.inactivityTimeoutMs);
|
|
544
|
-
const result = await generateText({
|
|
545
|
-
model: ctx.state.model,
|
|
546
|
-
messages: ctx.state.messages,
|
|
547
|
-
...(turnOptions.useTools ? { tools: ctx.state.tools } : {}),
|
|
548
|
-
...(turnOptions.structuredOutput
|
|
549
|
-
? { output: Output.object({ schema: jsonSchema(turnOptions.structuredOutput.schema) }) }
|
|
550
|
-
: {}),
|
|
551
|
-
maxRetries: 0,
|
|
552
|
-
stopWhen: stepCountIs(1),
|
|
553
|
-
abortSignal: turnAbort.signal,
|
|
554
|
-
}).finally(() => {
|
|
555
|
-
turnAbort.dispose();
|
|
556
|
-
});
|
|
557
|
-
// Reasoning-model fallback: when content text is empty the final
|
|
558
|
-
// text can land in reasoningText (mirrors extractAssistantText in
|
|
559
|
-
// the bespoke adapter).
|
|
560
|
-
const assistantText = result.text.trim().length > 0 ? result.text : (result.reasoningText ?? '');
|
|
561
|
-
if (result.toolCalls.length === 0 && assistantText.trim().length === 0) {
|
|
562
|
-
// Missing-content rule: covers embedded HTTP-200 gateway errors
|
|
563
|
-
// and reasoning-only responses on the AI SDK path — transient, so
|
|
564
|
-
// it flows into the bounded retry loop.
|
|
565
|
-
throw createGenericAgenticProviderError({
|
|
566
|
-
message: `generic-agentic ${ctx.label ?? ctx.role} turn produced neither tool calls nor ` +
|
|
567
|
-
'non-empty assistant text (after the reasoning-text fallback).',
|
|
568
|
-
role: ctx.role,
|
|
569
|
-
sessionHandle: ctx.sessionHandle,
|
|
570
|
-
kind: 'api_error',
|
|
571
|
-
retryable: true,
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
// Discovery (recorded, ai@6.0.202): the SDK awaits structured-output
|
|
575
|
-
// parsing inside `generateText` itself, so a non-JSON or
|
|
576
|
-
// schema-mismatched final text throws `NoObjectGeneratedError`
|
|
577
|
-
// (carrying the rejected assistant text as `.text`) from the call
|
|
578
|
-
// above — classified in the catch below. The `result.output` getter
|
|
579
|
-
// throws the *distinct* `NoOutputGeneratedError` only when no output
|
|
580
|
-
// was resolved because the turn's finish reason was not 'stop' (for
|
|
581
|
-
// example a length-truncated response); that case is mapped here.
|
|
582
|
-
let structuredOutputValue;
|
|
583
|
-
if (turnOptions.structuredOutput) {
|
|
584
|
-
const { schemaLabel } = turnOptions.structuredOutput;
|
|
585
|
-
try {
|
|
586
|
-
structuredOutputValue = result.output;
|
|
587
|
-
}
|
|
588
|
-
catch (outputError) {
|
|
589
|
-
throw createGenericAgenticProviderError({
|
|
590
|
-
message: `generic-agentic ${ctx.label ?? ctx.role} finalization turn completed without a ` +
|
|
591
|
-
`"${schemaLabel}" structured output (the response did not finish cleanly).`,
|
|
592
|
-
role: ctx.role,
|
|
593
|
-
sessionHandle: ctx.sessionHandle,
|
|
594
|
-
kind: 'structured_output_missing',
|
|
595
|
-
retryable: false,
|
|
596
|
-
// Carry the turn's assistant text as `text` so the rejected
|
|
597
|
-
// excerpt rides into provider_error providerData.diagnostic.
|
|
598
|
-
cause: {
|
|
599
|
-
message: describeError(outputError),
|
|
600
|
-
text: assistantText,
|
|
601
|
-
cause: outputError,
|
|
602
|
-
},
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
ctx.state.steps += 1;
|
|
607
|
-
if (assistantText.trim().length > 0) {
|
|
608
|
-
await emitProviderEvent(ctx.events, {
|
|
609
|
-
type: 'assistant_text',
|
|
610
|
-
...base,
|
|
611
|
-
text: assistantText,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
// Cumulative per-tool telemetry so run records are self-sufficient for
|
|
615
|
-
// per-tool error attribution. Advisor rounds add the cumulative step
|
|
616
|
-
// count (see includeStepsTelemetry).
|
|
617
|
-
const providerData = {
|
|
618
|
-
toolCalls: { ...ctx.state.toolCalls },
|
|
619
|
-
toolErrors: { ...ctx.state.toolErrors },
|
|
620
|
-
...(ctx.includeStepsTelemetry ? { steps: ctx.state.steps } : {}),
|
|
621
|
-
};
|
|
622
|
-
// Rate-computed cost: config pricing (when present) wins; otherwise a
|
|
623
|
-
// card-listed model is priced by its resolved slug with no configuration.
|
|
624
|
-
// When neither yields pricing the cost fields are omitted so the
|
|
625
|
-
// tokens-only event shape is preserved (resolveRateCost returns null,
|
|
626
|
-
// matching the prior `computeRateCostUsd`-returns-0-for-empty behavior).
|
|
627
|
-
const cost = resolveRateCost({
|
|
628
|
-
usage: result.usage,
|
|
629
|
-
model: ctx.state.modelSlug,
|
|
630
|
-
configPricing: ctx.state.pricing,
|
|
631
|
-
}) ?? {};
|
|
632
|
-
await emitProviderEvent(ctx.events, {
|
|
633
|
-
type: 'usage_reported',
|
|
634
|
-
...base,
|
|
635
|
-
usage: result.usage,
|
|
636
|
-
...cost,
|
|
637
|
-
providerData,
|
|
638
|
-
});
|
|
639
|
-
await emitProviderEvent(ctx.events, {
|
|
640
|
-
type: 'turn_completed',
|
|
641
|
-
...base,
|
|
642
|
-
usage: result.usage,
|
|
643
|
-
...cost,
|
|
644
|
-
providerData,
|
|
645
|
-
});
|
|
646
|
-
return {
|
|
647
|
-
assistantText,
|
|
648
|
-
toolCallCount: result.toolCalls.length,
|
|
649
|
-
responseMessages: result.response.messages,
|
|
650
|
-
...(turnOptions.structuredOutput ? { structuredOutput: structuredOutputValue } : {}),
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
catch (error) {
|
|
654
|
-
// Structured-output failure semantics (no repair): NoObjectGeneratedError
|
|
655
|
-
// is classified before generic normalization. An empty rejected text is
|
|
656
|
-
// the missing-content rule (a transport fault — embedded HTTP-200
|
|
657
|
-
// gateway errors, reasoning-only responses — so it stays retryable);
|
|
658
|
-
// any other rejected text is the model failing the structured-output
|
|
659
|
-
// contract: non-retryable `structured_output_missing` with the SDK
|
|
660
|
-
// error (and its `.text` excerpt) as the cause.
|
|
661
|
-
let classified = error;
|
|
662
|
-
if (turnOptions.structuredOutput && NoObjectGeneratedError.isInstance(error)) {
|
|
663
|
-
const { schemaLabel } = turnOptions.structuredOutput;
|
|
664
|
-
classified =
|
|
665
|
-
(error.text ?? '').trim() === ''
|
|
666
|
-
? createGenericAgenticProviderError({
|
|
667
|
-
message: `generic-agentic ${ctx.label ?? ctx.role} finalization turn produced neither tool calls nor ` +
|
|
668
|
-
'non-empty assistant text (after the reasoning-text fallback).',
|
|
669
|
-
role: ctx.role,
|
|
670
|
-
sessionHandle: ctx.sessionHandle,
|
|
671
|
-
kind: 'api_error',
|
|
672
|
-
retryable: true,
|
|
673
|
-
cause: error,
|
|
674
|
-
})
|
|
675
|
-
: createGenericAgenticProviderError({
|
|
676
|
-
message: `generic-agentic ${ctx.label ?? ctx.role} finalization turn did not produce a parsable ` +
|
|
677
|
-
`"${schemaLabel}" JSON payload: ${describeError(error)}`,
|
|
678
|
-
role: ctx.role,
|
|
679
|
-
sessionHandle: ctx.sessionHandle,
|
|
680
|
-
kind: 'structured_output_missing',
|
|
681
|
-
retryable: false,
|
|
682
|
-
cause: error,
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
else if (turnOptions.structuredOutput &&
|
|
686
|
-
typeof error === 'object' &&
|
|
687
|
-
error !== null &&
|
|
688
|
-
readStatusCode(error) === 400) {
|
|
689
|
-
// Request-time schema rejection: now that the SDK sends
|
|
690
|
-
// `response_format.type: 'json_schema'`, a gateway/model that cannot
|
|
691
|
-
// honor schema-enforced JSON rejects the request with HTTP 400. That
|
|
692
|
-
// is the model failing the structured-output contract, not a generic
|
|
693
|
-
// transport fault, so it is an attributable, non-retryable
|
|
694
|
-
// structured_output_invalid (the same compat `structured_output`
|
|
695
|
-
// mode as the NoObjectGeneratedError/validator paths) rather than the
|
|
696
|
-
// `provider_failed` bucket normalizeGenericAgenticError would assign.
|
|
697
|
-
// The narrowing to a structured-output turn keeps a 400 on a normal
|
|
698
|
-
// tool turn on its existing `provider_failed` mapping. The raw SDK
|
|
699
|
-
// error rides as the cause so its message/responseBody reach the
|
|
700
|
-
// provider_error diagnostic, exactly like the NoObjectGeneratedError
|
|
701
|
-
// branch above.
|
|
702
|
-
const { schemaLabel } = turnOptions.structuredOutput;
|
|
703
|
-
classified = createGenericAgenticProviderError({
|
|
704
|
-
message: `generic-agentic ${ctx.label ?? ctx.role} finalization turn was rejected with HTTP 400 ` +
|
|
705
|
-
`while requesting schema-enforced "${schemaLabel}" structured output ` +
|
|
706
|
-
`(the model or gateway does not honor json_schema response_format): ${describeError(error)}`,
|
|
707
|
-
role: ctx.role,
|
|
708
|
-
sessionHandle: ctx.sessionHandle,
|
|
709
|
-
kind: 'structured_output_invalid',
|
|
710
|
-
retryable: false,
|
|
711
|
-
cause: error,
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
const providerError = normalizeGenericAgenticError(classified, {
|
|
715
|
-
role: ctx.role,
|
|
716
|
-
label: ctx.label ?? ctx.role,
|
|
717
|
-
sessionHandle: ctx.sessionHandle,
|
|
718
|
-
callerSignal: ctx.signal,
|
|
719
|
-
});
|
|
720
|
-
// Caller cancellation never burns retry budget: a caller-signal
|
|
721
|
-
// abort is non-retryable and the aborted check is defensive depth.
|
|
722
|
-
if (providerError.retryable && apiRetryCount < ctx.apiRetryLimit && !ctx.signal?.aborted) {
|
|
723
|
-
apiRetryCount += 1;
|
|
724
|
-
await emitProviderEvent(ctx.events, {
|
|
725
|
-
type: 'tool_progress',
|
|
726
|
-
...base,
|
|
727
|
-
toolName: 'api_retry',
|
|
728
|
-
message: `transient API failure; retrying (${apiRetryCount}/${ctx.apiRetryLimit})`,
|
|
729
|
-
isError: true,
|
|
730
|
-
providerData: {
|
|
731
|
-
retryCount: apiRetryCount,
|
|
732
|
-
retryLimit: ctx.apiRetryLimit,
|
|
733
|
-
message: providerError.message,
|
|
734
|
-
},
|
|
735
|
-
});
|
|
736
|
-
await ctx.sleep(getApiRetryDelayMs(apiRetryCount));
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
throw providerError;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
/**
|
|
744
|
-
* The dedicated SDK-native structured-output finalization turn, shared by the
|
|
745
|
-
* coder and structured-advisor paths: appends one user message requesting the
|
|
746
|
-
* final control payload, runs exactly one no-tools `runAgentModelTurn` with
|
|
747
|
-
* `output: Output.object(...)`, validates the SDK-parsed object with the
|
|
748
|
-
* protocol spec's validator (the single source of truth — the SDK schema is
|
|
749
|
-
* transport-level enforcement of the same JSON schema object, not a parallel
|
|
750
|
-
* contract), and emits `structured_output_received` on success.
|
|
751
|
-
*
|
|
752
|
-
* Why a dedicated turn instead of constraining the tool loop itself:
|
|
753
|
-
* - it preserves per-turn liveness granularity (each turn keeps its own
|
|
754
|
-
* `turn_started`/inactivity-abort window);
|
|
755
|
-
* - it avoids applying `response_format` to tool-calling turns, a known
|
|
756
|
-
* cross-provider compatibility gap on OpenAI-compatible gateways;
|
|
757
|
-
* - it sidesteps the SDK's documented +1-step interaction between
|
|
758
|
-
* structured-output generation and `stopWhen`.
|
|
759
|
-
*
|
|
760
|
-
* The turn goes through the existing `runAgentModelTurn` machinery, so the
|
|
761
|
-
* inactivity abort, caller-signal composition, the missing-content rule, and
|
|
762
|
-
* the bounded `api_retry` transient loop are the same code path; transport
|
|
763
|
-
* retries are not model repair. The turn increments the `steps` telemetry
|
|
764
|
-
* counter like any successful turn but is never subject to the step cap
|
|
765
|
-
* (the cap binds only inside `runAgentToolLoop`). There are no repair turns,
|
|
766
|
-
* no fence extraction, and no raw-JSON tolerance: a model that cannot answer
|
|
767
|
-
* this turn with the payload fails non-retryably.
|
|
768
|
-
*/
|
|
769
|
-
async function runStructuredFinalizationTurn(args) {
|
|
770
|
-
const { ctx, protocol, schema } = args;
|
|
771
|
-
// The schema (and example payload, when the spec provides one) rides in the
|
|
772
|
-
// message because some OpenAI-compatible endpoints silently ignore
|
|
773
|
-
// `response_format` (a known gateway gap); without it those models would
|
|
774
|
-
// have no shape information at all. This is prompt content, not a parsing
|
|
775
|
-
// protocol — nothing reads the text back.
|
|
776
|
-
const promptLines = [
|
|
777
|
-
`Return the final ${protocol.schemaLabel} control payload now as a single JSON object. Do not call tools.`,
|
|
778
|
-
'',
|
|
779
|
-
'The payload must satisfy this JSON schema:',
|
|
780
|
-
JSON.stringify(schema, null, 2),
|
|
781
|
-
];
|
|
782
|
-
if (protocol.examplePayload !== undefined) {
|
|
783
|
-
promptLines.push('', 'Example payload:', JSON.stringify(protocol.examplePayload, null, 2));
|
|
784
|
-
}
|
|
785
|
-
ctx.state.messages.push({ role: 'user', content: promptLines.join('\n') });
|
|
786
|
-
const turn = await runAgentModelTurn(ctx, {
|
|
787
|
-
useTools: false,
|
|
788
|
-
structuredOutput: { schema, schemaLabel: protocol.schemaLabel },
|
|
789
|
-
});
|
|
790
|
-
ctx.state.messages.push(...turn.responseMessages);
|
|
791
|
-
let structured;
|
|
792
|
-
try {
|
|
793
|
-
structured = protocol.validator(turn.structuredOutput);
|
|
794
|
-
}
|
|
795
|
-
catch (validationError) {
|
|
796
|
-
throw createGenericAgenticProviderError({
|
|
797
|
-
message: `generic-agentic ${ctx.label ?? ctx.role} finalization payload failed "${protocol.schemaLabel}" ` +
|
|
798
|
-
`validation: ${describeError(validationError)}`,
|
|
799
|
-
role: ctx.role,
|
|
800
|
-
sessionHandle: ctx.sessionHandle,
|
|
801
|
-
kind: 'structured_output_invalid',
|
|
802
|
-
retryable: false,
|
|
803
|
-
// The generated JSON rides as `text` on the cause so the excerpt
|
|
804
|
-
// reaches provider_error providerData.diagnostic (same capture as
|
|
805
|
-
// NoObjectGeneratedError.text).
|
|
806
|
-
cause: {
|
|
807
|
-
message: describeError(validationError),
|
|
808
|
-
text: JSON.stringify(turn.structuredOutput, null, 2) ?? String(turn.structuredOutput),
|
|
809
|
-
cause: validationError,
|
|
810
|
-
},
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
await emitProviderEvent(ctx.events, {
|
|
814
|
-
type: 'structured_output_received',
|
|
815
|
-
provider: GENERIC_AGENTIC_PROVIDER_ID,
|
|
816
|
-
role: ctx.role,
|
|
817
|
-
...(ctx.label !== undefined ? { label: ctx.label } : {}),
|
|
818
|
-
sessionHandle: ctx.sessionHandle,
|
|
819
|
-
providerData: { schemaLabel: protocol.schemaLabel },
|
|
820
|
-
});
|
|
821
|
-
return structured;
|
|
822
|
-
}
|
|
823
|
-
/**
|
|
824
|
-
* Appends the protocol spec's `responseShapeHint` (decision-content guidance,
|
|
825
|
-
* not transport instructions) to the base prompt. The base prompt is sent
|
|
826
|
-
* fence-free: no neal-json instructions and no schema dump — the schema is
|
|
827
|
-
* delivered by the finalization turn.
|
|
828
|
-
*/
|
|
829
|
-
function appendResponseShapeHint(prompt, protocol) {
|
|
830
|
-
const hint = protocol.responseShapeHint?.trim();
|
|
831
|
-
return hint ? `${prompt}\n\nResponse shape guidance:\n${hint}` : prompt;
|
|
832
|
-
}
|
|
833
|
-
class GenericAgenticCoderAdapter {
|
|
834
|
-
options;
|
|
835
|
-
constructor(options) {
|
|
836
|
-
this.options = options;
|
|
837
|
-
}
|
|
838
|
-
async runPrompt(args) {
|
|
839
|
-
const sessionHandle = buildSyntheticSessionHandle();
|
|
840
|
-
try {
|
|
841
|
-
this.assertNoResumeHandle(args.resumeHandle, sessionHandle);
|
|
842
|
-
const state = this.prepareLoopState({
|
|
843
|
-
cwd: args.cwd,
|
|
844
|
-
sessionHandle,
|
|
845
|
-
events: args.events,
|
|
846
|
-
toolPolicy: args.toolPolicy,
|
|
847
|
-
});
|
|
848
|
-
await this.emitSessionStarted({ sessionHandle, events: args.events });
|
|
849
|
-
// CoderRunPromptArgs carries no apiRetryLimit: runPrompt performs no
|
|
850
|
-
// in-round transient retries; the liveness supervisor and orchestrator
|
|
851
|
-
// retries own recovery there.
|
|
852
|
-
const finalResponse = await runAgentToolLoop({
|
|
853
|
-
role: 'coder',
|
|
854
|
-
state,
|
|
855
|
-
sessionHandle,
|
|
856
|
-
inactivityTimeoutMs: args.inactivityTimeoutMs,
|
|
857
|
-
apiRetryLimit: 0,
|
|
858
|
-
stepCap: CODER_STEP_CAP,
|
|
859
|
-
includeStepsTelemetry: false,
|
|
860
|
-
sleep: this.options.sleep ?? defaultSleep,
|
|
861
|
-
signal: args.signal,
|
|
862
|
-
events: args.events,
|
|
863
|
-
}, args.prompt);
|
|
864
|
-
// Never persist a resumable handle for this provider, and never invoke
|
|
865
|
-
// args.onSessionStarted: the orchestrator persists that callback's
|
|
866
|
-
// handle and would then demand session_resume support.
|
|
867
|
-
return { sessionHandle: null, finalResponse };
|
|
868
|
-
}
|
|
869
|
-
catch (error) {
|
|
870
|
-
throw await this.surfaceError(error, {
|
|
871
|
-
sessionHandle,
|
|
872
|
-
events: args.events,
|
|
873
|
-
callerSignal: args.signal,
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
async runStructuredPrompt(args) {
|
|
878
|
-
const sessionHandle = buildSyntheticSessionHandle();
|
|
879
|
-
try {
|
|
880
|
-
this.assertNoResumeHandle(args.resumeHandle, sessionHandle);
|
|
881
|
-
const protocol = args.structuredJsonProtocol;
|
|
882
|
-
if (!protocol || protocol.protocol !== 'neal-json-block-v1') {
|
|
883
|
-
// The spec is still required: it carries the schema, validator, and
|
|
884
|
-
// labels that drive the SDK-native structured-output finalization
|
|
885
|
-
// turn (the fence protocol itself is never rendered here).
|
|
886
|
-
throw createGenericAgenticProviderError({
|
|
887
|
-
message: `generic-agentic ${args.label} prompts require the neal-json-block-v1 structured JSON ` +
|
|
888
|
-
'protocol spec; its schema, validator, and labels drive the SDK-native structured-output ' +
|
|
889
|
-
'finalization turn.',
|
|
890
|
-
role: 'coder',
|
|
891
|
-
sessionHandle,
|
|
892
|
-
kind: 'provider_failed',
|
|
893
|
-
retryable: false,
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
if (typeof protocol.schema === 'string') {
|
|
897
|
-
// Configuration error: the SDK's jsonSchema(...) needs the object
|
|
898
|
-
// form. No runtime caller passes the string arm of the spec's
|
|
899
|
-
// schema union today.
|
|
900
|
-
throw createGenericAgenticProviderError({
|
|
901
|
-
message: `generic-agentic ${args.label} prompts require an object-form JSON schema for ` +
|
|
902
|
-
`"${protocol.schemaLabel}"; a string schema cannot drive the SDK-native structured-output ` +
|
|
903
|
-
'finalization turn.',
|
|
904
|
-
role: 'coder',
|
|
905
|
-
sessionHandle,
|
|
906
|
-
kind: 'provider_failed',
|
|
907
|
-
retryable: false,
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
const state = this.prepareLoopState({
|
|
911
|
-
cwd: args.cwd,
|
|
912
|
-
sessionHandle,
|
|
913
|
-
label: args.label,
|
|
914
|
-
events: args.events,
|
|
915
|
-
toolPolicy: args.toolPolicy,
|
|
916
|
-
});
|
|
917
|
-
await this.emitSessionStarted({ sessionHandle, label: args.label, events: args.events });
|
|
918
|
-
const turnContext = {
|
|
919
|
-
role: 'coder',
|
|
920
|
-
state,
|
|
921
|
-
sessionHandle,
|
|
922
|
-
label: args.label,
|
|
923
|
-
inactivityTimeoutMs: args.inactivityTimeoutMs,
|
|
924
|
-
apiRetryLimit: args.apiRetryLimit ?? 0,
|
|
925
|
-
stepCap: CODER_STEP_CAP,
|
|
926
|
-
includeStepsTelemetry: false,
|
|
927
|
-
sleep: this.options.sleep ?? defaultSleep,
|
|
928
|
-
signal: args.signal,
|
|
929
|
-
events: args.events,
|
|
930
|
-
};
|
|
931
|
-
// The tool loop runs to its normal zero-tool-call completion turn on a
|
|
932
|
-
// fence-free prompt; its assistant text stays in the shared history.
|
|
933
|
-
// Then exactly one finalization turn produces the structured payload.
|
|
934
|
-
await runAgentToolLoop(turnContext, appendResponseShapeHint(args.prompt, protocol));
|
|
935
|
-
const structured = await runStructuredFinalizationTurn({
|
|
936
|
-
ctx: turnContext,
|
|
937
|
-
protocol,
|
|
938
|
-
schema: protocol.schema,
|
|
939
|
-
});
|
|
940
|
-
// Never persist a resumable handle; args.onSessionStarted is never
|
|
941
|
-
// invoked for this provider (see runPrompt).
|
|
942
|
-
return { sessionHandle: null, structured };
|
|
943
|
-
}
|
|
944
|
-
catch (error) {
|
|
945
|
-
throw await this.surfaceError(error, {
|
|
946
|
-
sessionHandle,
|
|
947
|
-
label: args.label,
|
|
948
|
-
events: args.events,
|
|
949
|
-
callerSignal: args.signal,
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
assertNoResumeHandle(resumeHandle, sessionHandle) {
|
|
954
|
-
if (resumeHandle === undefined || resumeHandle === null) {
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
// Defensive: this provider never persists a session handle, so a
|
|
958
|
-
// non-null resume handle can only come from corrupted run state.
|
|
959
|
-
throw createGenericAgenticProviderError({
|
|
960
|
-
message: 'generic-agentic does not support session resume and never persists session handles, ' +
|
|
961
|
-
`but a resume handle was provided (${JSON.stringify(resumeHandle)}); this indicates corrupted run state.`,
|
|
962
|
-
role: 'coder',
|
|
963
|
-
sessionHandle,
|
|
964
|
-
kind: 'session_unavailable',
|
|
965
|
-
retryable: false,
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
prepareLoopState(args) {
|
|
969
|
-
const resolveSettings = this.options.resolveSettings ?? getOpenAICompatibleSettings;
|
|
970
|
-
const settings = resolveGenericAgenticSettings({
|
|
971
|
-
cwd: args.cwd,
|
|
972
|
-
roundModel: null,
|
|
973
|
-
roleModel: this.options.model ?? null,
|
|
974
|
-
context: {
|
|
975
|
-
role: 'coder',
|
|
976
|
-
noun: 'coder',
|
|
977
|
-
subject: 'coder run',
|
|
978
|
-
modelOverrideExample: 'agent.coder.model',
|
|
979
|
-
},
|
|
980
|
-
resolveSettings,
|
|
981
|
-
});
|
|
982
|
-
const createModel = this.options.createModel ?? createDefaultGenericAgenticModel;
|
|
983
|
-
const model = createModel({
|
|
984
|
-
baseUrl: settings.baseUrl,
|
|
985
|
-
apiKey: settings.apiKey,
|
|
986
|
-
headers: settings.headers,
|
|
987
|
-
model: settings.model,
|
|
988
|
-
});
|
|
989
|
-
const state = {
|
|
990
|
-
model,
|
|
991
|
-
modelSlug: settings.model,
|
|
992
|
-
// Assigned immediately below; the toolset's event hook needs the state
|
|
993
|
-
// object to update the cumulative per-tool telemetry maps.
|
|
994
|
-
tools: undefined,
|
|
995
|
-
messages: [],
|
|
996
|
-
toolCalls: {},
|
|
997
|
-
toolErrors: {},
|
|
998
|
-
steps: 0,
|
|
999
|
-
pricing: settings.pricing,
|
|
1000
|
-
};
|
|
1001
|
-
const createToolset = args.toolPolicy?.allowRun === false ? createPlanAuthorToolset : createCoderToolset;
|
|
1002
|
-
state.tools = createToolset(args.cwd, {
|
|
1003
|
-
allowedWritePaths: args.toolPolicy?.allowedWritePaths,
|
|
1004
|
-
emitToolEvent: (event) => forwardAgentToolEvent(event, {
|
|
1005
|
-
role: 'coder',
|
|
1006
|
-
state,
|
|
1007
|
-
sessionHandle: args.sessionHandle,
|
|
1008
|
-
label: args.label,
|
|
1009
|
-
events: args.events,
|
|
1010
|
-
}),
|
|
1011
|
-
});
|
|
1012
|
-
return state;
|
|
1013
|
-
}
|
|
1014
|
-
async emitSessionStarted(args) {
|
|
1015
|
-
await emitProviderEvent(args.events, {
|
|
1016
|
-
type: 'session_started',
|
|
1017
|
-
provider: GENERIC_AGENTIC_PROVIDER_ID,
|
|
1018
|
-
role: 'coder',
|
|
1019
|
-
...(args.label !== undefined ? { label: args.label } : {}),
|
|
1020
|
-
sessionHandle: args.sessionHandle,
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
async surfaceError(error, ctx) {
|
|
1024
|
-
const providerError = normalizeGenericAgenticError(error, {
|
|
1025
|
-
role: 'coder',
|
|
1026
|
-
label: ctx.label ?? 'coder',
|
|
1027
|
-
sessionHandle: ctx.sessionHandle,
|
|
1028
|
-
callerSignal: ctx.callerSignal,
|
|
1029
|
-
});
|
|
1030
|
-
await emitProviderEvent(ctx.events, {
|
|
1031
|
-
type: 'provider_error',
|
|
1032
|
-
provider: GENERIC_AGENTIC_PROVIDER_ID,
|
|
1033
|
-
role: 'coder',
|
|
1034
|
-
...(ctx.label !== undefined ? { label: ctx.label } : {}),
|
|
1035
|
-
sessionHandle: providerError.sessionHandle ?? ctx.sessionHandle,
|
|
1036
|
-
message: providerError.message,
|
|
1037
|
-
errorKind: providerError.kind,
|
|
1038
|
-
providerData: providerErrorData(providerError),
|
|
1039
|
-
});
|
|
1040
|
-
// Thrown errors must not carry the events-only synthetic handle (see
|
|
1041
|
-
// withEventsOnlySessionHandle): the orchestrator persists it from the
|
|
1042
|
-
// error and resume would then demand session_resume support.
|
|
1043
|
-
return withEventsOnlySessionHandle(providerError);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
/**
|
|
1047
|
-
* Structured-advisor adapter with a bounded read-only tool loop: rounds run
|
|
1048
|
-
* the shared outer loop over `createReadOnlyToolset` (`read_file`,
|
|
1049
|
-
* `list_dir`, `grep`, `git_diff`; never write or shell) until a zero-tool-call turn,
|
|
1050
|
-
* then exactly one SDK-native structured-output finalization turn produces
|
|
1051
|
-
* the round payload (`runStructuredFinalizationTurn`, shared with the coder
|
|
1052
|
-
* structured path). Settings resolution, transient retry,
|
|
1053
|
-
* abort/inactivity composition, and the missing-content rule are the shared
|
|
1054
|
-
* per-turn machinery. This is the adapter
|
|
1055
|
-
* `runCoderFinalCompletionSummaryRound` uses when the coder is
|
|
1056
|
-
* `generic-agentic`, and it is what lets a generic reviewer inspect the
|
|
1057
|
-
* repository directly instead of relying on Neal-inlined context. Advisor
|
|
1058
|
-
* rounds are stateless: no advisor handle is ever persisted, so
|
|
1059
|
-
* `args.resumeHandle` is silently ignored (unlike the coder path's
|
|
1060
|
-
* `session_unavailable` guard).
|
|
1061
|
-
*/
|
|
1062
|
-
class GenericAgenticStructuredAdvisorAdapter {
|
|
1063
|
-
options;
|
|
1064
|
-
constructor(options) {
|
|
1065
|
-
this.options = options;
|
|
1066
|
-
}
|
|
1067
|
-
async runStructuredRound(args) {
|
|
1068
|
-
const sessionHandle = buildSyntheticSessionHandle();
|
|
1069
|
-
try {
|
|
1070
|
-
const protocol = args.structuredJsonProtocol;
|
|
1071
|
-
if (!protocol || protocol.protocol !== 'neal-json-block-v1') {
|
|
1072
|
-
// The spec is still required: it carries the schema, validator, and
|
|
1073
|
-
// labels that drive the SDK-native structured-output finalization
|
|
1074
|
-
// turn (the fence protocol itself is never rendered here).
|
|
1075
|
-
throw createGenericAgenticProviderError({
|
|
1076
|
-
message: `generic-agentic ${args.label} rounds require the neal-json-block-v1 structured JSON ` +
|
|
1077
|
-
'protocol spec; its schema, validator, and labels drive the SDK-native structured-output ' +
|
|
1078
|
-
'finalization turn.',
|
|
1079
|
-
role: 'structured-advisor',
|
|
1080
|
-
sessionHandle,
|
|
1081
|
-
kind: 'provider_failed',
|
|
1082
|
-
retryable: false,
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
if (typeof protocol.schema === 'string') {
|
|
1086
|
-
// Configuration error: the SDK's jsonSchema(...) needs the object
|
|
1087
|
-
// form. No runtime caller passes the string arm of the spec's
|
|
1088
|
-
// schema union today.
|
|
1089
|
-
throw createGenericAgenticProviderError({
|
|
1090
|
-
message: `generic-agentic ${args.label} rounds require an object-form JSON schema for ` +
|
|
1091
|
-
`"${protocol.schemaLabel}"; a string schema cannot drive the SDK-native structured-output ` +
|
|
1092
|
-
'finalization turn.',
|
|
1093
|
-
role: 'structured-advisor',
|
|
1094
|
-
sessionHandle,
|
|
1095
|
-
kind: 'provider_failed',
|
|
1096
|
-
retryable: false,
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
const resolveSettings = this.options.resolveSettings ?? getOpenAICompatibleSettings;
|
|
1100
|
-
const settings = resolveGenericAgenticSettings({
|
|
1101
|
-
cwd: args.cwd,
|
|
1102
|
-
// Round-level args.model wins over the adapter/role model and
|
|
1103
|
-
// default_model (matching resolveRoundSettings in the bespoke
|
|
1104
|
-
// adapter).
|
|
1105
|
-
roundModel: args.model ?? null,
|
|
1106
|
-
roleModel: this.options.model ?? null,
|
|
1107
|
-
context: {
|
|
1108
|
-
role: 'structured-advisor',
|
|
1109
|
-
noun: args.label,
|
|
1110
|
-
subject: `${args.label} round`,
|
|
1111
|
-
modelOverrideExample: 'agent.reviewer.model',
|
|
1112
|
-
},
|
|
1113
|
-
resolveSettings,
|
|
1114
|
-
});
|
|
1115
|
-
const createModel = this.options.createModel ?? createDefaultGenericAgenticModel;
|
|
1116
|
-
const model = createModel({
|
|
1117
|
-
baseUrl: settings.baseUrl,
|
|
1118
|
-
apiKey: settings.apiKey,
|
|
1119
|
-
headers: settings.headers,
|
|
1120
|
-
model: settings.model,
|
|
1121
|
-
});
|
|
1122
|
-
const state = {
|
|
1123
|
-
model,
|
|
1124
|
-
modelSlug: settings.model,
|
|
1125
|
-
// Assigned immediately below; the toolset's event hook needs the
|
|
1126
|
-
// state object to update the cumulative per-tool telemetry maps.
|
|
1127
|
-
tools: undefined,
|
|
1128
|
-
messages: [],
|
|
1129
|
-
toolCalls: {},
|
|
1130
|
-
toolErrors: {},
|
|
1131
|
-
steps: 0,
|
|
1132
|
-
pricing: settings.pricing,
|
|
1133
|
-
};
|
|
1134
|
-
// Read-only inspection toolset only: advisor rounds must never gain
|
|
1135
|
-
// write or shell access. Tool events forward with the advisor role and
|
|
1136
|
-
// the round label.
|
|
1137
|
-
state.tools = createReadOnlyToolset(args.cwd, {
|
|
1138
|
-
emitToolEvent: (event) => forwardAgentToolEvent(event, {
|
|
1139
|
-
role: 'structured-advisor',
|
|
1140
|
-
state,
|
|
1141
|
-
sessionHandle,
|
|
1142
|
-
label: args.label,
|
|
1143
|
-
events: args.events,
|
|
1144
|
-
}),
|
|
1145
|
-
});
|
|
1146
|
-
const turnContext = {
|
|
1147
|
-
role: 'structured-advisor',
|
|
1148
|
-
state,
|
|
1149
|
-
sessionHandle,
|
|
1150
|
-
label: args.label,
|
|
1151
|
-
inactivityTimeoutMs: args.inactivityTimeoutMs,
|
|
1152
|
-
apiRetryLimit: args.apiRetryLimit,
|
|
1153
|
-
stepCap: advisorStepCap(args.label),
|
|
1154
|
-
includeStepsTelemetry: true,
|
|
1155
|
-
sleep: this.options.sleep ?? defaultSleep,
|
|
1156
|
-
// Caller cancellation is wired into every turn — the read-only tool
|
|
1157
|
-
// loop and the finalization turn alike (matching the coder
|
|
1158
|
-
// structured path).
|
|
1159
|
-
signal: args.signal,
|
|
1160
|
-
events: args.events,
|
|
1161
|
-
};
|
|
1162
|
-
await emitProviderEvent(args.events, {
|
|
1163
|
-
type: 'session_started',
|
|
1164
|
-
provider: GENERIC_AGENTIC_PROVIDER_ID,
|
|
1165
|
-
role: 'structured-advisor',
|
|
1166
|
-
label: args.label,
|
|
1167
|
-
sessionHandle,
|
|
1168
|
-
});
|
|
1169
|
-
// The read-only tool loop runs to its normal zero-tool-call completion
|
|
1170
|
-
// turn on a fence-free prompt; its assistant text stays in the shared
|
|
1171
|
-
// history. Then exactly one finalization turn produces the structured
|
|
1172
|
-
// payload (it increments the `steps` telemetry like any successful
|
|
1173
|
-
// turn but is never subject to the advisor step cap, which binds only
|
|
1174
|
-
// inside the tool loop).
|
|
1175
|
-
await runAgentToolLoop(turnContext, appendResponseShapeHint(args.prompt, protocol));
|
|
1176
|
-
const structured = await runStructuredFinalizationTurn({
|
|
1177
|
-
ctx: turnContext,
|
|
1178
|
-
protocol,
|
|
1179
|
-
schema: protocol.schema,
|
|
1180
|
-
});
|
|
1181
|
-
// Never persist a resumable handle for this provider.
|
|
1182
|
-
return { sessionHandle: null, structured };
|
|
1183
|
-
}
|
|
1184
|
-
catch (error) {
|
|
1185
|
-
const providerError = normalizeGenericAgenticError(error, {
|
|
1186
|
-
role: 'structured-advisor',
|
|
1187
|
-
label: args.label,
|
|
1188
|
-
sessionHandle,
|
|
1189
|
-
callerSignal: args.signal,
|
|
1190
|
-
});
|
|
1191
|
-
await emitProviderEvent(args.events, {
|
|
1192
|
-
type: 'provider_error',
|
|
1193
|
-
provider: GENERIC_AGENTIC_PROVIDER_ID,
|
|
1194
|
-
role: 'structured-advisor',
|
|
1195
|
-
label: args.label,
|
|
1196
|
-
sessionHandle: providerError.sessionHandle ?? sessionHandle,
|
|
1197
|
-
message: providerError.message,
|
|
1198
|
-
errorKind: providerError.kind,
|
|
1199
|
-
providerData: providerErrorData(providerError),
|
|
1200
|
-
});
|
|
1201
|
-
// Strip the events-only handle before throwing: the final-completion
|
|
1202
|
-
// summary path persists `error.sessionHandle` into
|
|
1203
|
-
// `state.coderSessionHandle` (src/neal/orchestrator/completion.ts), and
|
|
1204
|
-
// a persisted handle rejects `neal resume` for this provider.
|
|
1205
|
-
throw withEventsOnlySessionHandle(providerError);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
export function createGenericAgenticCoderAdapter(options = {}) {
|
|
1210
|
-
return new GenericAgenticCoderAdapter(options);
|
|
1211
|
-
}
|
|
1212
|
-
export function createGenericAgenticStructuredAdvisorAdapter(options = {}) {
|
|
1213
|
-
return new GenericAgenticStructuredAdvisorAdapter(options);
|
|
1214
|
-
}
|
|
1215
|
-
export const genericAgenticProviderDefinition = {
|
|
1216
|
-
id: GENERIC_AGENTIC_PROVIDER_ID,
|
|
1217
|
-
displayName: 'Generic Agentic (AI SDK)',
|
|
1218
|
-
capabilities: {
|
|
1219
|
-
coder: {
|
|
1220
|
-
supported: true,
|
|
1221
|
-
toolAccess: { read: true, write: true, shell: true },
|
|
1222
|
-
supportsSessionResume: false,
|
|
1223
|
-
supportsModelOverride: true,
|
|
1224
|
-
supportsStructuredOutput: true,
|
|
1225
|
-
usageReporting: 'opportunistic',
|
|
1226
|
-
},
|
|
1227
|
-
// Required so the coder role passes the final-completion
|
|
1228
|
-
// structured-advisor gate (assertAgentConfigSupportsWriterRun asserts
|
|
1229
|
-
// the coder provider's structured-advisor path for
|
|
1230
|
-
// runCoderFinalCompletionSummaryRound). Advisor rounds run the bounded
|
|
1231
|
-
// read-only tool loop (read_file, list_dir, grep), so read is true:
|
|
1232
|
-
// reviewers inspect the repository directly (the 'read-only' review
|
|
1233
|
-
// doctrine mode) instead of receiving Neal-inlined context. Write and
|
|
1234
|
-
// shell stay false — advisor rounds never mutate or execute.
|
|
1235
|
-
'structured-advisor': {
|
|
1236
|
-
supported: true,
|
|
1237
|
-
toolAccess: { read: true, write: false, shell: false },
|
|
1238
|
-
// The read-only advisor tool loop exposes a real commit-range diff tool
|
|
1239
|
-
// (git_diff in createReadOnlyToolset), so the read-only review doctrine
|
|
1240
|
-
// instructs that tool directly instead of having Neal inline the diff.
|
|
1241
|
-
providesRangeDiffTool: true,
|
|
1242
|
-
supportsSessionResume: false,
|
|
1243
|
-
supportsModelOverride: true,
|
|
1244
|
-
supportsStructuredOutput: true,
|
|
1245
|
-
usageReporting: 'opportunistic',
|
|
1246
|
-
},
|
|
1247
|
-
},
|
|
1248
|
-
createCoderAdapter: createGenericAgenticCoderAdapter,
|
|
1249
|
-
createStructuredAdvisorAdapter: createGenericAgenticStructuredAdvisorAdapter,
|
|
1250
|
-
};
|
|
1251
|
-
export const genericAgenticProviderTestHooks = {
|
|
1252
|
-
createDefaultGenericAgenticModel,
|
|
1253
|
-
normalizeGenericAgenticError,
|
|
1254
|
-
createCoderAdapterWithInjection: (injection, options) => new GenericAgenticCoderAdapter({ ...options, ...injection }),
|
|
1255
|
-
createStructuredAdvisorAdapterWithInjection: (injection, options) => new GenericAgenticStructuredAdvisorAdapter({ ...options, ...injection }),
|
|
1256
|
-
};
|