@the-open-engine/zeroshot 6.34.2 → 6.35.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.
Files changed (29) hide show
  1. package/lib/agent-cli-provider/adapters/gateway.d.ts.map +1 -1
  2. package/lib/agent-cli-provider/adapters/gateway.js +1 -0
  3. package/lib/agent-cli-provider/adapters/gateway.js.map +1 -1
  4. package/lib/agent-cli-provider/gateway-tools.d.ts.map +1 -1
  5. package/lib/agent-cli-provider/gateway-tools.js +23 -1
  6. package/lib/agent-cli-provider/gateway-tools.js.map +1 -1
  7. package/lib/agent-cli-provider/provider-registry.d.ts +2 -2
  8. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/provider-registry.js +3 -1
  10. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  11. package/lib/agent-cli-provider/single-agent-runtime.js +27 -3
  12. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  13. package/lib/agent-cli-provider/types.d.ts +1 -0
  14. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  15. package/lib/agent-cli-provider/types.js.map +1 -1
  16. package/lib/delivery-contract.js +139 -0
  17. package/npm-shrinkwrap.json +2 -2
  18. package/package.json +1 -1
  19. package/src/agent/agent-lifecycle.js +24 -19
  20. package/src/agent/agent-task-executor.js +169 -68
  21. package/src/agent/output-extraction.js +111 -39
  22. package/src/agent/pr-verification.js +19 -13
  23. package/src/agent/provider-terminal-failure.js +186 -0
  24. package/src/agent-cli-provider/adapters/gateway.ts +1 -0
  25. package/src/agent-cli-provider/gateway-tools.ts +41 -3
  26. package/src/agent-cli-provider/provider-registry.ts +3 -1
  27. package/src/agent-cli-provider/single-agent-runtime.ts +30 -3
  28. package/src/agent-cli-provider/types.ts +1 -0
  29. package/src/orchestrator.js +33 -1
@@ -0,0 +1,186 @@
1
+ // @ts-nocheck
2
+
3
+ const { getProvider } = require('../providers');
4
+ const { extractCliFailure } = require('./output-extraction');
5
+
6
+ function categoryForProviderFailure(error, classification) {
7
+ const isPermanent = classification.retryable === false;
8
+ const authenticationPattern =
9
+ /(?:invalid[_ -]?api[_ -]?key|api[_ -]?key.*invalid|unauthori[sz]ed|forbidden|authentication|permission denied)/i;
10
+ if (isPermanent && authenticationPattern.test(error)) return 'authentication';
11
+
12
+ const quotaPattern = /(?:insufficient[_ -]?quota|quota exceeded|resource_exhausted)/i;
13
+ if (isPermanent && quotaPattern.test(error)) return 'quota';
14
+ if (isPermanent) return 'permanent';
15
+ return classification.kind === 'unknown-retryable' ? 'unknown' : 'transient';
16
+ }
17
+
18
+ function classifyProviderFailure(providerName, error) {
19
+ let rawClassification = { retryable: true, kind: 'unknown-retryable' };
20
+ try {
21
+ rawClassification = getProvider(providerName).adapter.classifyError(new Error(error));
22
+ } catch {
23
+ // Extraction remains available if a provider adapter cannot be loaded.
24
+ }
25
+ return {
26
+ retryable: rawClassification?.retryable !== false,
27
+ kind:
28
+ typeof rawClassification?.kind === 'string' ? rawClassification.kind : 'unknown-retryable',
29
+ };
30
+ }
31
+
32
+ function extractProviderFailure(output, providerName) {
33
+ const cliError = extractCliFailure(output, providerName);
34
+ if (!cliError) return null;
35
+
36
+ const classification = classifyProviderFailure(providerName, cliError.error);
37
+ const category = categoryForProviderFailure(cliError.error, classification);
38
+ return {
39
+ error: `Provider ${cliError.provider} failed (${category}; ${classification.kind})`,
40
+ provider: cliError.provider,
41
+ event: cliError.provider === 'codex' ? 'turn.failed' : 'terminal_error',
42
+ category,
43
+ classification,
44
+ diagnostic: cliError.diagnostic,
45
+ };
46
+ }
47
+
48
+ function redactTerminalFailureForControlPlane(state, providerName, content) {
49
+ const failure = extractProviderFailure(content, providerName);
50
+ if (!failure) return content;
51
+
52
+ state.providerFailure = failure;
53
+ let eventType = 'provider.failure';
54
+ try {
55
+ const parsed = JSON.parse(content);
56
+ if (typeof parsed?.type === 'string') eventType = parsed.type;
57
+ } catch {
58
+ // extractProviderFailure already proved a supported terminal envelope.
59
+ }
60
+ return JSON.stringify({
61
+ type: eventType,
62
+ ...(providerName === 'claude' ? { is_error: true } : {}),
63
+ ...(providerName === 'gemini' ? { status: 'error', severity: 'error' } : {}),
64
+ error: { message: failure.error },
65
+ zeroshot_failure: {
66
+ provider: failure.provider,
67
+ event: failure.event,
68
+ category: failure.category,
69
+ kind: failure.classification.kind,
70
+ retryable: failure.classification.retryable,
71
+ diagnostic: failure.diagnostic,
72
+ },
73
+ });
74
+ }
75
+
76
+ function decorateError(error, failure) {
77
+ if (!failure) return error;
78
+ error.provider = failure.provider || null;
79
+ error.providerEvent = failure.event || null;
80
+ error.providerCategory = failure.category || null;
81
+ error.classification = failure.classification || null;
82
+ error.providerDiagnostic = failure.diagnostic || null;
83
+ if (failure.classification?.retryable === false) error.permanent = true;
84
+ return error;
85
+ }
86
+
87
+ function receiptFields(error) {
88
+ if (!error?.provider) return {};
89
+ return {
90
+ provider: error.provider,
91
+ event: error.providerEvent,
92
+ category: error.providerCategory,
93
+ kind: error.classification?.kind,
94
+ retryable: error.classification?.retryable,
95
+ diagnostic: error.providerDiagnostic,
96
+ };
97
+ }
98
+
99
+ function workerFailure(error) {
100
+ const authenticationFailure =
101
+ error?.provider &&
102
+ error?.classification?.retryable === false &&
103
+ error?.providerCategory === 'authentication';
104
+ return authenticationFailure
105
+ ? { code: 'refusal', reason: 'authentication_required' }
106
+ : { code: 'crash', reason: 'declared_failure' };
107
+ }
108
+
109
+ function publishCriticalFailure({
110
+ agent,
111
+ error,
112
+ attempts,
113
+ worker,
114
+ unsupportedCapability,
115
+ structuredOutputInvalid,
116
+ }) {
117
+ const specific =
118
+ error?.hookFailure ||
119
+ structuredOutputInvalid ||
120
+ unsupportedCapability ||
121
+ error?.vertexModelError ||
122
+ error?.terminationExhausted;
123
+ const critical =
124
+ agent.role === 'implementation' ||
125
+ agent.role === 'coordinator' ||
126
+ agent.id === 'consensus-coordinator';
127
+ if (!critical || specific) return worker;
128
+
129
+ agent._publish({
130
+ topic: 'CLUSTER_FAILED',
131
+ receiver: 'broadcast',
132
+ content: {
133
+ text: `Critical agent ${agent.id} exhausted its retry budget`,
134
+ data: {
135
+ reason: error?.provider ? 'provider_execution_failed' : 'critical_agent_exhausted',
136
+ agentId: agent.id,
137
+ role: agent.role,
138
+ attempts,
139
+ code: worker.code,
140
+ workerReason: worker.reason,
141
+ ...receiptFields(error),
142
+ },
143
+ },
144
+ });
145
+ return worker;
146
+ }
147
+
148
+ function buildFinalFailureInfo({
149
+ agent,
150
+ error,
151
+ attempts,
152
+ worker,
153
+ unsupportedCapability,
154
+ structuredOutputInvalid,
155
+ }) {
156
+ return {
157
+ ...(error?.terminationExhausted ? agent.cluster.failureInfo : {}),
158
+ agentId: agent.id,
159
+ taskId: error?.taskId || agent.currentTaskId,
160
+ iteration: agent.iteration,
161
+ error: error.message,
162
+ attempts,
163
+ ...receiptFields(error),
164
+ ...(error?.provider ? { code: worker.code, workerReason: worker.reason } : {}),
165
+ ...(unsupportedCapability
166
+ ? {
167
+ code: error.code,
168
+ permanent: true,
169
+ provider: error.provider,
170
+ capability: error.capability,
171
+ }
172
+ : {}),
173
+ ...(structuredOutputInvalid ? { code: error.code, details: error.details ?? null } : {}),
174
+ timestamp: Date.now(),
175
+ };
176
+ }
177
+
178
+ module.exports = {
179
+ buildFinalFailureInfo,
180
+ decorateError,
181
+ extractProviderFailure,
182
+ publishCriticalFailure,
183
+ receiptFields,
184
+ redactTerminalFailureForControlPlane,
185
+ workerFailure,
186
+ };
@@ -33,6 +33,7 @@ export const gatewaySettingsDefaults: Readonly<Record<string, unknown>> = Object
33
33
  protocol: 'openai',
34
34
  baseUrl: null,
35
35
  apiKey: null,
36
+ apiKeyEnv: null,
36
37
  headers: null,
37
38
  model: null,
38
39
  maxTokens: null,
@@ -52,6 +52,7 @@ export function normalizeGatewayBuildOptions(
52
52
  const protocol = optionalGatewayProtocol(value.protocol, `${field}.protocol`);
53
53
  const baseUrl = optionalString(value.baseUrl, `${field}.baseUrl`);
54
54
  const apiKey = optionalString(value.apiKey, `${field}.apiKey`);
55
+ const apiKeyEnv = optionalEnvironmentName(value.apiKeyEnv, `${field}.apiKeyEnv`);
55
56
  const model = optionalNullableString(value.model, `${field}.model`);
56
57
  const maxTokens = optionalNullablePositiveInteger(value.maxTokens, `${field}.maxTokens`);
57
58
  const headers = optionalStringRecord(value.headers, `${field}.headers`);
@@ -60,6 +61,13 @@ export function normalizeGatewayBuildOptions(
60
61
  if (protocol !== undefined) result.protocol = protocol;
61
62
  if (baseUrl !== undefined) result.baseUrl = baseUrl;
62
63
  if (typeof apiKey === 'string') result.apiKey = apiKey;
64
+ if (apiKeyEnv !== undefined) result.apiKeyEnv = apiKeyEnv;
65
+ if (apiKey !== undefined && apiKeyEnv !== undefined) {
66
+ invalidField(
67
+ `${field}.apiKeyEnv`,
68
+ `${field}.apiKey and ${field}.apiKeyEnv are mutually exclusive.`
69
+ );
70
+ }
63
71
  if (model !== undefined) result.model = model;
64
72
  if (maxTokens !== undefined) result.maxTokens = maxTokens;
65
73
  if (headers !== undefined) result.headers = headers;
@@ -115,6 +123,18 @@ export function validateGatewaySettings(settings: Record<string, unknown>): stri
115
123
  );
116
124
  optionalString(settings.baseUrl, 'providerSettings.gateway.baseUrl');
117
125
  optionalString(settings.apiKey, 'providerSettings.gateway.apiKey');
126
+ optionalEnvironmentName(settings.apiKeyEnv, 'providerSettings.gateway.apiKeyEnv');
127
+ if (
128
+ settings.apiKey !== null &&
129
+ settings.apiKey !== undefined &&
130
+ settings.apiKeyEnv !== null &&
131
+ settings.apiKeyEnv !== undefined
132
+ ) {
133
+ invalidField(
134
+ 'providerSettings.gateway.apiKeyEnv',
135
+ 'providerSettings.gateway.apiKey and providerSettings.gateway.apiKeyEnv are mutually exclusive.'
136
+ );
137
+ }
118
138
  optionalNullableString(settings.model, 'providerSettings.gateway.model');
119
139
  const maxTokens = optionalNullablePositiveInteger(
120
140
  settings.maxTokens,
@@ -138,6 +158,14 @@ export function validateGatewaySettings(settings: Record<string, unknown>): stri
138
158
  }
139
159
  }
140
160
 
161
+ function optionalEnvironmentName(value: unknown, field: string): string | undefined {
162
+ const name = optionalString(value, field);
163
+ if (name !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
164
+ invalidField(field, `${field} must name an environment variable.`);
165
+ }
166
+ return name;
167
+ }
168
+
141
169
  export function normalizeGatewayToolPolicy(
142
170
  value: GatewayToolPolicy,
143
171
  field: string,
@@ -212,7 +240,11 @@ function requiredGatewayToolPolicy(
212
240
  return normalizeGatewayToolPolicy(value, field, cwd);
213
241
  }
214
242
 
215
- function normalizeGatewayRoots(value: readonly string[], field: string, cwd: string): readonly string[] {
243
+ function normalizeGatewayRoots(
244
+ value: readonly string[],
245
+ field: string,
246
+ cwd: string
247
+ ): readonly string[] {
216
248
  if (value.length === 0) {
217
249
  invalidField(field, `${field} must contain at least one root path.`);
218
250
  }
@@ -368,7 +400,11 @@ async function readGatewayFile(
368
400
  input: unknown,
369
401
  policy: GatewayToolPolicy
370
402
  ): Promise<Record<string, unknown>> {
371
- const targetPath = await assertWithinRoots(readGatewayFileInput(input), policy.roots, 'read_file.path');
403
+ const targetPath = await assertWithinRoots(
404
+ readGatewayFileInput(input),
405
+ policy.roots,
406
+ 'read_file.path'
407
+ );
372
408
  const content = await readFile(targetPath, 'utf8');
373
409
  return { path: targetPath, content };
374
410
  }
@@ -420,7 +456,9 @@ async function applyGatewayPatch(
420
456
  if (!current.includes(search)) {
421
457
  throw new Error('apply_patch.search did not match the target file.');
422
458
  }
423
- const next = request.replaceAll ? current.split(search).join(replace) : current.replace(search, replace);
459
+ const next = request.replaceAll
460
+ ? current.split(search).join(replace)
461
+ : current.replace(search, replace);
424
462
  await writeFile(targetPath, next, 'utf8');
425
463
  return {
426
464
  path: targetPath,
@@ -316,13 +316,15 @@ export const providerRegistry = [
316
316
  invoke: SPAWN_INVOKE,
317
317
  installInstructions: 'Bundled with Zeroshot; no external provider CLI install is required.',
318
318
  authInstructions:
319
- 'Configure providerSettings.gateway.protocol, baseUrl, apiKey, model, maxTokens when required, and toolPolicy in Zeroshot settings.',
319
+ 'Configure providerSettings.gateway protocol, base URL, apiKey or apiKeyEnv, model, ' +
320
+ 'maxTokens, and toolPolicy in Zeroshot settings.',
320
321
  credentialPaths: [],
321
322
  credentialEnvKeys: gatewayAdapter.credentialEnvKeys,
322
323
  settingsFields: [
323
324
  'protocol',
324
325
  'baseUrl',
325
326
  'apiKey',
327
+ 'apiKeyEnv',
326
328
  'headers',
327
329
  'model',
328
330
  'maxTokens',
@@ -766,6 +766,7 @@ function resolveRuntimeGatewayOptions(
766
766
  const cwd = baseOptions.cwd ?? process.cwd();
767
767
  const settingsGateway = providerSettings.gateway ?? {};
768
768
  const requestGateway = baseOptions.gateway ?? {};
769
+ const apiKey = resolveGatewayApiKey(requestGateway, settingsGateway);
769
770
  const mergedHeaders =
770
771
  requestGateway.headers === undefined
771
772
  ? settingsGateway.headers
@@ -777,9 +778,7 @@ function resolveRuntimeGatewayOptions(
777
778
  ...((requestGateway.baseUrl ?? settingsGateway.baseUrl)
778
779
  ? { baseUrl: requestGateway.baseUrl ?? settingsGateway.baseUrl }
779
780
  : {}),
780
- ...((requestGateway.apiKey ?? settingsGateway.apiKey)
781
- ? { apiKey: requestGateway.apiKey ?? settingsGateway.apiKey }
782
- : {}),
781
+ ...(apiKey === undefined ? {} : { apiKey }),
783
782
  ...(mergedHeaders === undefined ? {} : { headers: mergedHeaders }),
784
783
  model: requestGateway.model ?? modelSpec.model ?? settingsGateway.model ?? null,
785
784
  ...((requestGateway.maxTokens ?? settingsGateway.maxTokens)
@@ -792,6 +791,34 @@ function resolveRuntimeGatewayOptions(
792
791
  return resolveGatewayConfiguration(mergedGateway, 'options.gateway', cwd);
793
792
  }
794
793
 
794
+ function resolveGatewayApiKey(
795
+ request: GatewayBuildOptions,
796
+ settings: GatewayBuildOptions
797
+ ): string | undefined {
798
+ if (request.apiKeyEnv !== undefined) {
799
+ throw new Error('options.gateway.apiKeyEnv requires trusted provider settings');
800
+ }
801
+ const requestOwnsTransport =
802
+ request.protocol !== undefined ||
803
+ request.baseUrl !== undefined ||
804
+ request.headers !== undefined;
805
+ if ((settings.apiKeyEnv !== undefined || settings.apiKey !== undefined) && requestOwnsTransport) {
806
+ throw new Error('options.gateway transport cannot override credential settings');
807
+ }
808
+ const direct = request.apiKey ?? settings.apiKey;
809
+ const environmentName = settings.apiKeyEnv;
810
+ if (direct !== undefined && environmentName !== undefined) {
811
+ throw new Error('gateway apiKey and apiKeyEnv are mutually exclusive');
812
+ }
813
+ if (direct !== undefined) return direct;
814
+ if (environmentName === undefined) return undefined;
815
+ const value = process.env[environmentName];
816
+ if (typeof value !== 'string' || !value.trim()) {
817
+ throw new Error(`gateway apiKeyEnv requires environment variable ${environmentName}`);
818
+ }
819
+ return value;
820
+ }
821
+
795
822
  function shouldIncludeAuthEnv(
796
823
  baseOptions: BuildProviderCommandOptions,
797
824
  authEnv: Readonly<Record<string, string>>
@@ -74,6 +74,7 @@ export interface GatewayBuildOptions {
74
74
  readonly protocol?: GatewayProtocol;
75
75
  readonly baseUrl?: string;
76
76
  readonly apiKey?: string;
77
+ readonly apiKeyEnv?: string;
77
78
  readonly headers?: Readonly<Record<string, string>>;
78
79
  readonly model?: string | null;
79
80
  readonly maxTokens?: number;
@@ -275,6 +275,7 @@ class Orchestrator {
275
275
  // Track if orchestrator is closed (prevents _saveClusters race conditions during cleanup)
276
276
  this.closed = false;
277
277
  this._conductorWatchdogs = new Set();
278
+ this._clusterRunBoundaries = new Map();
278
279
 
279
280
  // Track if clusters are loaded (for lazy loading pattern)
280
281
  this._clustersLoaded = options.skipLoad === true;
@@ -1575,11 +1576,17 @@ class Orchestrator {
1575
1576
  }
1576
1577
 
1577
1578
  _registerAgentErrorHandler(messageBus, clusterId) {
1579
+ this._recordClusterRunBoundary(messageBus, clusterId);
1578
1580
  this._subscribeToClusterTopic(messageBus, clusterId, 'AGENT_ERROR', async (message) => {
1579
1581
  const agentRole = message.content?.data?.role;
1580
1582
  const attempts = message.content?.data?.attempts || 1;
1581
1583
  const hookFailure = message.content?.data?.hookFailure === true;
1582
1584
  const restartExhausted = message.content?.data?.restartExhausted === true;
1585
+ const durableClusterFailure = this._findCurrentRunClusterFailure(
1586
+ messageBus,
1587
+ clusterId,
1588
+ message.sequence
1589
+ );
1583
1590
 
1584
1591
  await this._saveClusters();
1585
1592
 
@@ -1587,7 +1594,10 @@ class Orchestrator {
1587
1594
  agentRole === 'implementation' ||
1588
1595
  agentRole === 'coordinator' ||
1589
1596
  message.sender === 'consensus-coordinator';
1590
- const shouldStop = shouldStopForRole && (hookFailure || restartExhausted || attempts >= 3);
1597
+ const shouldStop =
1598
+ !durableClusterFailure &&
1599
+ shouldStopForRole &&
1600
+ (hookFailure || restartExhausted || attempts >= 3);
1591
1601
 
1592
1602
  if (shouldStop) {
1593
1603
  this._log(`\n${'='.repeat(80)}`);
@@ -1610,6 +1620,25 @@ class Orchestrator {
1610
1620
  });
1611
1621
  }
1612
1622
 
1623
+ _recordClusterRunBoundary(messageBus, clusterId) {
1624
+ const latest = messageBus.findLast({ cluster_id: clusterId, orderBySequence: true });
1625
+ this._clusterRunBoundaries.set(clusterId, latest?.sequence ?? null);
1626
+ }
1627
+
1628
+ _findCurrentRunClusterFailure(messageBus, clusterId, throughId) {
1629
+ const boundary = this._clusterRunBoundaries.get(clusterId);
1630
+ return (
1631
+ messageBus.query({
1632
+ cluster_id: clusterId,
1633
+ topic: 'CLUSTER_FAILED',
1634
+ order: 'desc',
1635
+ limit: 1,
1636
+ ...(boundary === null ? {} : { afterId: boundary }),
1637
+ ...(throughId === undefined ? {} : { throughId }),
1638
+ })[0] || null
1639
+ );
1640
+ }
1641
+
1613
1642
  _registerPushBlockedHandler(messageBus, clusterId) {
1614
1643
  this._subscribeToClusterTopic(messageBus, clusterId, 'PUSH_BLOCKED', async (message) => {
1615
1644
  const reason = message.content?.data?.blocked_reason || 'unknown';
@@ -2425,6 +2454,7 @@ class Orchestrator {
2425
2454
 
2426
2455
  // Now remove from memory after persisting
2427
2456
  this.clusters.delete(clusterId);
2457
+ this._clusterRunBoundaries.delete(clusterId);
2428
2458
  }
2429
2459
 
2430
2460
  /**
@@ -2462,6 +2492,7 @@ class Orchestrator {
2462
2492
  watchdog.dispose();
2463
2493
  }
2464
2494
  this._conductorWatchdogs.clear();
2495
+ this._clusterRunBoundaries.clear();
2465
2496
 
2466
2497
  for (const cluster of this.clusters.values()) {
2467
2498
  if (typeof cluster.snapshotter?.stop === 'function') {
@@ -2902,6 +2933,7 @@ class Orchestrator {
2902
2933
  }
2903
2934
 
2904
2935
  async _restartClusterAgents(cluster) {
2936
+ this._recordClusterRunBoundary(cluster.messageBus, cluster.id);
2905
2937
  cluster.state = 'running';
2906
2938
  cluster.pid = process.pid;
2907
2939
  for (const agent of cluster.agents) {