@the-open-engine/zeroshot 6.10.2 → 6.12.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/cli/commands/providers.js +13 -13
- package/cli/index.js +77 -35
- package/cli/lib/first-run.js +12 -11
- package/cli/lib/update-checker.js +517 -132
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +3 -0
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +1 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster/client.cjs +146 -0
- package/lib/cluster/client.d.ts +44 -0
- package/lib/cluster/client.mjs +141 -0
- package/lib/cluster/connection.cjs +382 -0
- package/lib/cluster/connection.d.ts +41 -0
- package/lib/cluster/connection.mjs +378 -0
- package/lib/cluster/errors.cjs +44 -0
- package/lib/cluster/errors.d.ts +23 -0
- package/lib/cluster/errors.mjs +32 -0
- package/lib/cluster/frames.cjs +49 -0
- package/lib/cluster/frames.d.ts +12 -0
- package/lib/cluster/frames.mjs +45 -0
- package/lib/cluster/generated/protocol-schema.cjs +5 -0
- package/lib/cluster/generated/protocol-schema.d.ts +1 -0
- package/lib/cluster/generated/protocol-schema.mjs +2 -0
- package/lib/cluster/generated/protocol.cjs +22 -0
- package/lib/cluster/generated/protocol.d.ts +781 -0
- package/lib/cluster/generated/protocol.mjs +19 -0
- package/lib/cluster/index.cjs +40 -0
- package/lib/cluster/index.d.ts +10 -0
- package/lib/cluster/index.mjs +6 -0
- package/lib/cluster/queue.cjs +71 -0
- package/lib/cluster/queue.d.ts +15 -0
- package/lib/cluster/queue.mjs +67 -0
- package/lib/cluster/socket.cjs +15 -0
- package/lib/cluster/socket.d.ts +11 -0
- package/lib/cluster/socket.mjs +12 -0
- package/lib/cluster/subscriptions.cjs +270 -0
- package/lib/cluster/subscriptions.d.ts +69 -0
- package/lib/cluster/subscriptions.mjs +264 -0
- package/lib/cluster/validators.cjs +46 -0
- package/lib/cluster/validators.d.ts +3 -0
- package/lib/cluster/validators.mjs +42 -0
- package/lib/settings.js +195 -23
- package/lib/setup-apply.js +45 -32
- package/lib/setup-undo.js +25 -16
- package/package.json +25 -3
- package/scripts/build-cluster.js +43 -0
- package/scripts/generate-cluster-types.js +203 -0
- package/scripts/release-dry-run.js +6 -6
- package/scripts/rust-distribution.js +933 -0
- package/src/agent/agent-hook-executor.js +14 -6
- package/src/agent/agent-lifecycle.js +25 -8
- package/src/agent/agent-task-executor.js +711 -139
- package/src/agent/output-reformatter.js +54 -41
- package/src/agent/pr-verification.js +11 -3
- package/src/agent/task-execution-handle.js +373 -0
- package/src/agent-cli-provider/adapters/opencode.ts +3 -0
- package/src/agent-cli-provider/types.ts +1 -0
- package/src/agent-wrapper.js +5 -2
- package/src/cluster/client.ts +199 -0
- package/src/cluster/connection.ts +300 -0
- package/src/cluster/errors.ts +32 -0
- package/src/cluster/frames.ts +44 -0
- package/src/cluster/generated/protocol-schema.ts +2 -0
- package/src/cluster/generated/protocol.ts +183 -0
- package/src/cluster/index.ts +38 -0
- package/src/cluster/queue.ts +84 -0
- package/src/cluster/socket.ts +31 -0
- package/src/cluster/subscriptions.ts +256 -0
- package/src/cluster/validators.ts +56 -0
- package/src/cluster/ws.d.ts +7 -0
- package/src/isolation-manager.js +16 -0
- package/src/issue-providers/jira-provider.js +1 -1
- package/src/issue-providers/linear-provider.js +4 -4
- package/task-lib/runner.js +3 -0
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
* When an LLM outputs markdown/text instead of JSON despite schema instructions,
|
|
5
5
|
* this module attempts to extract/reformat the content into valid JSON.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* To enable reformatting:
|
|
11
|
-
* 1. Implement SDK support in the provider (getSDKEnvVar, callSimple)
|
|
12
|
-
* 2. The reformatOutput() function will then work automatically
|
|
7
|
+
* Opencode output can be reformatted through its CLI when direct JSON extraction fails.
|
|
8
|
+
* Other providers remain on their own extraction paths and never depend on the opencode binary.
|
|
13
9
|
*/
|
|
14
10
|
|
|
15
11
|
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
16
12
|
|
|
13
|
+
|
|
14
|
+
function createCancellationError() {
|
|
15
|
+
const error = new Error('Output reformatting cancelled');
|
|
16
|
+
error.code = 'REFORMAT_CANCELLED';
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
17
21
|
/**
|
|
18
22
|
* Build the reformatting prompt
|
|
19
23
|
*
|
|
@@ -27,7 +31,9 @@ function buildReformatPrompt(rawOutput, schema, previousError = null) {
|
|
|
27
31
|
// Truncate long outputs to avoid context limits
|
|
28
32
|
const truncatedOutput = rawOutput.length > 4000 ? rawOutput.slice(-4000) : rawOutput;
|
|
29
33
|
|
|
30
|
-
let prompt = `
|
|
34
|
+
let prompt = `CRITICAL: Do NOT use any tools. Do NOT read, write, or edit any files. Do NOT explore the codebase. This is a pure text-to-JSON transformation — respond with JSON only.
|
|
35
|
+
|
|
36
|
+
Convert this text into a JSON object matching the schema.
|
|
31
37
|
|
|
32
38
|
## SCHEMA
|
|
33
39
|
\`\`\`json
|
|
@@ -58,44 +64,45 @@ Fix this issue in your response.`;
|
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* STATUS: SDK NOT IMPLEMENTED - This function always throws.
|
|
64
|
-
* When SDK support is added to providers, this will work automatically.
|
|
67
|
+
* This fallback is intentionally scoped to opencode agents. It participates in agent
|
|
68
|
+
* cancellation so retries cannot outlive cluster shutdown.
|
|
65
69
|
*
|
|
66
70
|
* @param {Object} options
|
|
67
71
|
* @param {string} options.rawOutput - The non-JSON output to reformat
|
|
68
72
|
* @param {Object} options.schema - Target JSON schema
|
|
69
|
-
* @param {string} options.providerName -
|
|
73
|
+
* @param {string} options.providerName - Active provider name
|
|
70
74
|
* @param {number} [options.maxAttempts=3] - Maximum reformatting attempts
|
|
71
75
|
* @param {Function} [options.onAttempt] - Callback for each attempt (attempt, error)
|
|
76
|
+
* @param {Function} [options.isCancelled] - Returns true after agent cancellation
|
|
77
|
+
* @param {Function} options.runReformat - Runs the prompt in the active agent execution context
|
|
72
78
|
* @returns {Promise<Object>} The reformatted JSON object
|
|
73
|
-
* @throws {Error}
|
|
79
|
+
* @throws {Error} If the provider is unsupported, cancellation occurs, or attempts fail
|
|
74
80
|
*/
|
|
75
|
-
function reformatOutput({
|
|
81
|
+
async function reformatOutput({
|
|
76
82
|
rawOutput,
|
|
77
|
-
schema
|
|
83
|
+
schema,
|
|
78
84
|
providerName,
|
|
79
|
-
maxAttempts
|
|
80
|
-
onAttempt
|
|
85
|
+
maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
86
|
+
onAttempt,
|
|
87
|
+
isCancelled = () => false,
|
|
88
|
+
runReformat,
|
|
81
89
|
}) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
new Error(
|
|
86
|
-
`Output reformatting not available: SDK not implemented for provider "${providerName}". ` +
|
|
90
|
+
if (providerName !== 'opencode') {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Output reformatting not available for provider "${providerName}". ` +
|
|
87
93
|
`Agent output must be valid JSON. Raw output (last 200 chars): ${(rawOutput || '').slice(-200)}`
|
|
88
|
-
)
|
|
89
|
-
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (typeof runReformat !== 'function') {
|
|
97
|
+
throw new Error('Output reformatting requires the active agent execution context');
|
|
98
|
+
}
|
|
90
99
|
|
|
91
|
-
// FUTURE: When SDK support is added to providers, uncomment this:
|
|
92
|
-
/*
|
|
93
|
-
const { getProvider } = require('../providers');
|
|
94
|
-
const provider = getProvider(providerName);
|
|
95
100
|
|
|
101
|
+
const { extractJsonFromOutput } = require('./output-extraction');
|
|
96
102
|
let lastError = null;
|
|
97
103
|
|
|
98
104
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
105
|
+
if (isCancelled()) throw createCancellationError();
|
|
99
106
|
if (onAttempt) {
|
|
100
107
|
onAttempt(attempt, lastError);
|
|
101
108
|
}
|
|
@@ -103,23 +110,19 @@ function reformatOutput({
|
|
|
103
110
|
const prompt = buildReformatPrompt(rawOutput, schema, lastError);
|
|
104
111
|
|
|
105
112
|
try {
|
|
106
|
-
const result = await
|
|
107
|
-
|
|
108
|
-
maxTokens: 2000,
|
|
109
|
-
});
|
|
110
|
-
|
|
113
|
+
const result = await runReformat(prompt);
|
|
114
|
+
if (isCancelled()) throw createCancellationError();
|
|
111
115
|
if (!result?.success) {
|
|
112
|
-
lastError = result?.error || '
|
|
116
|
+
lastError = result?.error || 'reformat task failed';
|
|
113
117
|
continue;
|
|
114
118
|
}
|
|
115
|
-
|
|
116
|
-
if (!
|
|
117
|
-
lastError = '
|
|
119
|
+
const output = result.output;
|
|
120
|
+
if (!output) {
|
|
121
|
+
lastError = 'reformat task returned no output';
|
|
118
122
|
continue;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
const parsed = extractJsonFromOutput(
|
|
122
|
-
|
|
125
|
+
const parsed = extractJsonFromOutput(output, 'opencode');
|
|
123
126
|
if (!parsed) {
|
|
124
127
|
lastError = 'Could not extract JSON from reformatted output';
|
|
125
128
|
continue;
|
|
@@ -133,14 +136,24 @@ function reformatOutput({
|
|
|
133
136
|
|
|
134
137
|
return parsed;
|
|
135
138
|
} catch (err) {
|
|
139
|
+
if (
|
|
140
|
+
err.code === 'REFORMAT_CANCELLED' ||
|
|
141
|
+
err.code === 'AGENT_TASK_TIMEOUT' ||
|
|
142
|
+
err.nestedExecutionLifecycle === true ||
|
|
143
|
+
err.retainTaskHandle === true ||
|
|
144
|
+
err.permanent === true ||
|
|
145
|
+
err.terminationExhausted === true
|
|
146
|
+
) {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
136
149
|
lastError = err.message;
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
throw new Error(
|
|
141
|
-
`Failed to reformat output after ${maxAttempts} attempts
|
|
154
|
+
`Failed to reformat output after ${maxAttempts} attempts (provider "${providerName}"). ` +
|
|
155
|
+
`Last error: ${lastError}. Raw output (last 200 chars): ${(rawOutput || '').slice(-200)}`
|
|
142
156
|
);
|
|
143
|
-
*/
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
/**
|
|
@@ -335,9 +335,12 @@ function resolveFieldValue(structuredOutput, fieldNames) {
|
|
|
335
335
|
return null;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
function resolvePrClaimsFromOutput({ output, providerName, adapter }) {
|
|
338
|
+
function resolvePrClaimsFromOutput({ output, parsedResult, providerName, adapter }) {
|
|
339
339
|
const { extractJsonFromOutput } = require('./output-extraction');
|
|
340
|
-
const structuredOutput =
|
|
340
|
+
const structuredOutput =
|
|
341
|
+
parsedResult && typeof parsedResult === 'object'
|
|
342
|
+
? parsedResult
|
|
343
|
+
: extractJsonFromOutput(output, providerName) || {};
|
|
341
344
|
|
|
342
345
|
const structuredUrl = resolveFieldValue(structuredOutput, getClaimUrlFieldCandidates(adapter));
|
|
343
346
|
const structuredNumber = resolveFieldValue(
|
|
@@ -586,7 +589,12 @@ async function verifyPullRequest({ result, agent, autoMerge }) {
|
|
|
586
589
|
const adapter = getVerificationAdapter(platform);
|
|
587
590
|
const providerName =
|
|
588
591
|
typeof agent?._resolveProvider === 'function' ? agent._resolveProvider() : 'claude';
|
|
589
|
-
const claims = resolvePrClaimsFromOutput({
|
|
592
|
+
const claims = resolvePrClaimsFromOutput({
|
|
593
|
+
output: result.output,
|
|
594
|
+
parsedResult: result.parsedResult,
|
|
595
|
+
providerName,
|
|
596
|
+
adapter,
|
|
597
|
+
});
|
|
590
598
|
|
|
591
599
|
if (handleBlockedPusherOutcome({ claims, platform, agent })) {
|
|
592
600
|
return;
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
function isTerminationConfirmed(termination) {
|
|
2
|
+
return termination?.forced !== false || termination?.alreadyTerminal === true;
|
|
3
|
+
}
|
|
4
|
+
const MAX_DEADLINE_CLEANUP_ATTEMPTS = 3;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* TaskExecutionHandle — first-class reentrant task-execution handle.
|
|
8
|
+
*
|
|
9
|
+
* Created BEFORE the provider process is spawned so cancellation is possible
|
|
10
|
+
* even before a task ID or PID is known. Owns the process tree, tracks late
|
|
11
|
+
* task-ID / PID assignment, and supports nested (child) executions that never
|
|
12
|
+
* overwrite the parent's identity.
|
|
13
|
+
*/
|
|
14
|
+
class TaskExecutionHandle {
|
|
15
|
+
constructor(agentId) {
|
|
16
|
+
/** @type {string} Owning agent id (immutable). */
|
|
17
|
+
this.agentId = agentId;
|
|
18
|
+
/** @type {string|null} Zeroshot task id, assigned once after registration. */
|
|
19
|
+
this._taskId = null;
|
|
20
|
+
/** @type {number|null} Real CLI process pid, assigned once after registration. */
|
|
21
|
+
this._pid = null;
|
|
22
|
+
/** @type {import('child_process').ChildProcess|null} Launch wrapper process. */
|
|
23
|
+
this._proc = null;
|
|
24
|
+
this._cancelled = false;
|
|
25
|
+
this._cancelReason = null;
|
|
26
|
+
this._cancelDetails = {};
|
|
27
|
+
this._cancelAction = null;
|
|
28
|
+
this._failClosedAction = null;
|
|
29
|
+
this._failClosedError = null;
|
|
30
|
+
this._invokedCancelActions = new Set();
|
|
31
|
+
this._cancelActionPromises = [];
|
|
32
|
+
this._lastCancellationResult = undefined;
|
|
33
|
+
this._executionFinished = false;
|
|
34
|
+
this._retainOwnership = false;
|
|
35
|
+
this.settled = false;
|
|
36
|
+
this._resolveSettle = null;
|
|
37
|
+
this._settlePromise = new Promise((resolve) => {
|
|
38
|
+
this._resolveSettle = resolve;
|
|
39
|
+
});
|
|
40
|
+
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
41
|
+
this._killTimer = null;
|
|
42
|
+
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
43
|
+
this._deadlineTimer = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get taskId() {
|
|
47
|
+
return this._taskId;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get pid() {
|
|
51
|
+
return this._pid;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get isCancelled() {
|
|
55
|
+
return this._cancelled;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get cancelReason() {
|
|
59
|
+
return this._cancelReason;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get cancelDetails() {
|
|
63
|
+
return this._cancelDetails;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
attachProcess(proc) {
|
|
67
|
+
this._proc = proc;
|
|
68
|
+
const clearWrapperKill = () => this._clearKillTimer();
|
|
69
|
+
proc.once('close', clearWrapperKill);
|
|
70
|
+
proc.once('error', clearWrapperKill);
|
|
71
|
+
if (this._cancelled) {
|
|
72
|
+
this._killProcess();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
assignTaskId(taskId) {
|
|
77
|
+
if (this._taskId && this._taskId !== taskId) {
|
|
78
|
+
throw new Error(`Nested execution task ID changed from ${this._taskId} to ${taskId}`);
|
|
79
|
+
}
|
|
80
|
+
this._taskId = taskId;
|
|
81
|
+
if (this._cancelled) {
|
|
82
|
+
this._killProcess();
|
|
83
|
+
this._invokeCancelAction();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
assignPid(pid) {
|
|
88
|
+
if (this._pid && this._pid !== pid) {
|
|
89
|
+
throw new Error(`Nested execution PID changed from ${this._pid} to ${pid}`);
|
|
90
|
+
}
|
|
91
|
+
this._pid = pid;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setCancelAction(action) {
|
|
95
|
+
const previousAction = this._cancelAction;
|
|
96
|
+
this._cancelAction = action;
|
|
97
|
+
if (this._cancelled) {
|
|
98
|
+
this._invokeCancelAction();
|
|
99
|
+
}
|
|
100
|
+
return previousAction;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setFailClosedAction(action) {
|
|
104
|
+
this._failClosedAction = action;
|
|
105
|
+
if (this._failClosedError) {
|
|
106
|
+
action(this._failClosedError);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
failClosed(error) {
|
|
111
|
+
if (this.settled || this._failClosedError) return false;
|
|
112
|
+
this._retainOwnership = true;
|
|
113
|
+
this._failClosedError = error;
|
|
114
|
+
if (!error.taskId && this._taskId) {
|
|
115
|
+
error.taskId = this._taskId;
|
|
116
|
+
}
|
|
117
|
+
if (this._failClosedAction) {
|
|
118
|
+
this._failClosedAction(error);
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cancel(reason = 'Task cancelled', details = {}) {
|
|
124
|
+
if (!this._cancelled) {
|
|
125
|
+
this._cancelled = true;
|
|
126
|
+
this._cancelReason = reason;
|
|
127
|
+
this._cancelDetails = details;
|
|
128
|
+
}
|
|
129
|
+
this._clearDeadlineTimer();
|
|
130
|
+
this._killProcess();
|
|
131
|
+
this._invokeCancelAction();
|
|
132
|
+
return this.waitForCancellation();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async waitForCancellation() {
|
|
136
|
+
let observedCount = -1;
|
|
137
|
+
let results = [];
|
|
138
|
+
try {
|
|
139
|
+
while (observedCount !== this._cancelActionPromises.length) {
|
|
140
|
+
observedCount = this._cancelActionPromises.length;
|
|
141
|
+
results = await Promise.all(this._cancelActionPromises);
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
this._retainOwnership = true;
|
|
145
|
+
this._invokedCancelActions.delete(this._cancelAction);
|
|
146
|
+
this._cancelActionPromises = [];
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
const observedTermination = results.at(-1);
|
|
150
|
+
if (observedTermination !== undefined && observedTermination !== null) {
|
|
151
|
+
this._lastCancellationResult = observedTermination;
|
|
152
|
+
}
|
|
153
|
+
const termination =
|
|
154
|
+
observedTermination !== undefined && observedTermination !== null
|
|
155
|
+
? observedTermination
|
|
156
|
+
: this._lastCancellationResult;
|
|
157
|
+
if (termination === undefined || termination === null) {
|
|
158
|
+
return termination;
|
|
159
|
+
}
|
|
160
|
+
if (!isTerminationConfirmed(termination)) {
|
|
161
|
+
this._retainOwnership = true;
|
|
162
|
+
this._invokedCancelActions.delete(this._cancelAction);
|
|
163
|
+
this._cancelActionPromises = [];
|
|
164
|
+
} else {
|
|
165
|
+
this._retainOwnership = false;
|
|
166
|
+
if (this._executionFinished) {
|
|
167
|
+
this.markSettled();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return termination;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
armDeadline(timeoutMs) {
|
|
174
|
+
if (timeoutMs <= 0 || this.settled) return;
|
|
175
|
+
this._clearDeadlineTimer();
|
|
176
|
+
this._deadlineTimer = setTimeout(() => {
|
|
177
|
+
this._deadlineTimer = null;
|
|
178
|
+
this._cancelAtDeadline(timeoutMs).catch(() => {
|
|
179
|
+
// failClosed records the permanent error before notifying the execution owner.
|
|
180
|
+
});
|
|
181
|
+
}, timeoutMs);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async _cancelAtDeadline(timeoutMs) {
|
|
185
|
+
const reason = `Nested task timed out after ${timeoutMs}ms`;
|
|
186
|
+
let lastError = null;
|
|
187
|
+
let termination = null;
|
|
188
|
+
let attempts = 0;
|
|
189
|
+
for (attempts = 1; attempts <= MAX_DEADLINE_CLEANUP_ATTEMPTS; attempts++) {
|
|
190
|
+
try {
|
|
191
|
+
termination = await this.cancel(reason, { code: 'AGENT_TASK_TIMEOUT' });
|
|
192
|
+
if (termination && isTerminationConfirmed(termination)) return;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
lastError = error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const detail =
|
|
199
|
+
lastError?.message || termination?.reason || 'task termination was not confirmed';
|
|
200
|
+
const error = new Error(
|
|
201
|
+
`Failed to terminate nested task ${this._taskId || 'before task ID assignment'} ` +
|
|
202
|
+
`after ${attempts - 1} deadline cleanup attempts: ${detail}`
|
|
203
|
+
);
|
|
204
|
+
error.code = 'NESTED_TASK_TERMINATION_EXHAUSTED';
|
|
205
|
+
error.taskId = this._taskId;
|
|
206
|
+
error.permanent = true;
|
|
207
|
+
error.restartExhausted = true;
|
|
208
|
+
error.terminationExhausted = true;
|
|
209
|
+
error.terminationAttempts = attempts - 1;
|
|
210
|
+
error.retainTaskHandle = true;
|
|
211
|
+
error.nestedExecutionLifecycle = true;
|
|
212
|
+
this.failClosed(error);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
retainOwnership() {
|
|
216
|
+
this._retainOwnership = true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
finishExecution() {
|
|
220
|
+
this._executionFinished = true;
|
|
221
|
+
if (!this._retainOwnership) {
|
|
222
|
+
this.markSettled();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
markSettled() {
|
|
227
|
+
if (this.settled) return;
|
|
228
|
+
this.settled = true;
|
|
229
|
+
this._clearKillTimer();
|
|
230
|
+
this._clearDeadlineTimer();
|
|
231
|
+
this._resolveSettle();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async settle() {
|
|
235
|
+
await this._settlePromise;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_invokeCancelAction() {
|
|
239
|
+
const action = this._cancelAction;
|
|
240
|
+
if (!action || this._invokedCancelActions.has(action)) return;
|
|
241
|
+
this._invokedCancelActions.add(action);
|
|
242
|
+
const cancellation = Promise.resolve().then(() =>
|
|
243
|
+
action(this._cancelReason, this._cancelDetails)
|
|
244
|
+
);
|
|
245
|
+
this._cancelActionPromises.push(cancellation);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_clearDeadlineTimer() {
|
|
249
|
+
if (this._deadlineTimer) {
|
|
250
|
+
clearTimeout(this._deadlineTimer);
|
|
251
|
+
this._deadlineTimer = null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_clearKillTimer() {
|
|
256
|
+
if (this._killTimer) {
|
|
257
|
+
clearTimeout(this._killTimer);
|
|
258
|
+
this._killTimer = null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_killProcess() {
|
|
263
|
+
if (!this._proc || this.settled) return;
|
|
264
|
+
try {
|
|
265
|
+
this._proc.kill('SIGTERM');
|
|
266
|
+
const proc = this._proc;
|
|
267
|
+
this._clearKillTimer();
|
|
268
|
+
this._killTimer = setTimeout(() => {
|
|
269
|
+
this._killTimer = null;
|
|
270
|
+
if (!this.settled) {
|
|
271
|
+
try {
|
|
272
|
+
proc.kill('SIGKILL');
|
|
273
|
+
} catch {
|
|
274
|
+
// Process already exited.
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}, 3000);
|
|
278
|
+
} catch {
|
|
279
|
+
// Process already exited.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
class NestedExecutionRegistry {
|
|
285
|
+
constructor(agentId) {
|
|
286
|
+
this.agentId = agentId;
|
|
287
|
+
this._handles = new Set();
|
|
288
|
+
this._cancellation = null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
get size() {
|
|
292
|
+
return this._handles.size;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
get hasActive() {
|
|
296
|
+
return this._handles.size > 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
get activeTaskIds() {
|
|
300
|
+
return [...this._handles].map((handle) => handle.taskId).filter(Boolean);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
register(handle) {
|
|
304
|
+
if (this._cancellation) {
|
|
305
|
+
const error = new Error(this._cancellation.reason);
|
|
306
|
+
error.code = this._cancellation.details.code || 'REFORMAT_CANCELLED';
|
|
307
|
+
error.nestedExecutionCancellation = true;
|
|
308
|
+
error.nestedExecutionLifecycle = true;
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
if (!(handle instanceof TaskExecutionHandle)) {
|
|
312
|
+
throw new TypeError('Nested execution registry accepts TaskExecutionHandle instances only');
|
|
313
|
+
}
|
|
314
|
+
if (handle.agentId !== this.agentId) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Nested execution owner mismatch: registry ${this.agentId}, handle ${handle.agentId}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
this._handles.add(handle);
|
|
320
|
+
return handle;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
unregister(handle) {
|
|
324
|
+
if (!handle.settled) {
|
|
325
|
+
throw new Error('Cannot unregister a nested execution before settlement');
|
|
326
|
+
}
|
|
327
|
+
this._handles.delete(handle);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
failClosed(error) {
|
|
331
|
+
let dispatched = false;
|
|
332
|
+
for (const handle of this._handles) {
|
|
333
|
+
dispatched = handle.failClosed(error) || dispatched;
|
|
334
|
+
}
|
|
335
|
+
return dispatched;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async cancelAll(reason = 'Task cancelled', details = {}) {
|
|
339
|
+
this._cancellation = { reason, details };
|
|
340
|
+
try {
|
|
341
|
+
const handles = [...this._handles];
|
|
342
|
+
const terminations = await Promise.all(
|
|
343
|
+
handles.map(async (handle) => {
|
|
344
|
+
const termination = await handle.cancel(reason, details);
|
|
345
|
+
if (isTerminationConfirmed(termination)) {
|
|
346
|
+
await handle.settle();
|
|
347
|
+
this.unregister(handle);
|
|
348
|
+
}
|
|
349
|
+
return termination;
|
|
350
|
+
})
|
|
351
|
+
);
|
|
352
|
+
const failed = terminations.find(
|
|
353
|
+
(termination) => !isTerminationConfirmed(termination)
|
|
354
|
+
);
|
|
355
|
+
return failed || { forced: true, nested: terminations };
|
|
356
|
+
} finally {
|
|
357
|
+
this._cancellation = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getNestedExecutionRegistry(agent) {
|
|
363
|
+
if (!agent.nestedExecutions) {
|
|
364
|
+
agent.nestedExecutions = new NestedExecutionRegistry(agent.id);
|
|
365
|
+
}
|
|
366
|
+
return agent.nestedExecutions;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
module.exports = {
|
|
370
|
+
getNestedExecutionRegistry,
|
|
371
|
+
NestedExecutionRegistry,
|
|
372
|
+
TaskExecutionHandle,
|
|
373
|
+
};
|
|
@@ -55,6 +55,9 @@ function addOpencodeOptionalArgs(args: string[], options: BuildProviderCommandOp
|
|
|
55
55
|
) {
|
|
56
56
|
args.push('--format', 'json');
|
|
57
57
|
}
|
|
58
|
+
if (options.agentName) {
|
|
59
|
+
args.push('--agent', options.agentName);
|
|
60
|
+
}
|
|
58
61
|
|
|
59
62
|
if (options.modelSpec?.model) {
|
|
60
63
|
args.push('--model', options.modelSpec.model);
|
|
@@ -268,6 +268,7 @@ export interface BuildProviderCommandOptions {
|
|
|
268
268
|
readonly outputFormat?: OutputFormat;
|
|
269
269
|
readonly jsonSchema?: unknown;
|
|
270
270
|
readonly cwd?: string;
|
|
271
|
+
readonly agentName?: string;
|
|
271
272
|
readonly autoApprove?: boolean;
|
|
272
273
|
readonly resumeSessionId?: string;
|
|
273
274
|
readonly continueSession?: boolean;
|
package/src/agent-wrapper.js
CHANGED
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
const { findMatchingTrigger, evaluateTrigger } = require('./agent/agent-trigger-evaluator');
|
|
26
26
|
const { executeHook } = require('./agent/agent-hook-executor');
|
|
27
27
|
const { injectInput: injectAgentInput } = require('./agent/agent-input-injector');
|
|
28
|
+
const { NestedExecutionRegistry } = require('./agent/task-execution-handle');
|
|
28
29
|
const {
|
|
29
30
|
spawnClaudeTask,
|
|
30
31
|
followClaudeTaskLogs,
|
|
@@ -78,6 +79,8 @@ class AgentWrapper {
|
|
|
78
79
|
this.currentPromptIdentity = null;
|
|
79
80
|
/** @type {number | null} */
|
|
80
81
|
this.processPid = null; // Track process PID for resource monitoring
|
|
82
|
+
/** Active child executions never overwrite the parent task identity fields. */
|
|
83
|
+
this.nestedExecutions = new NestedExecutionRegistry(this.id);
|
|
81
84
|
this.running = false;
|
|
82
85
|
/** @type {Function | null} */
|
|
83
86
|
this.unsubscribe = null;
|
|
@@ -512,8 +515,8 @@ class AgentWrapper {
|
|
|
512
515
|
* Spawn claude-zeroshots process and stream output via message bus
|
|
513
516
|
* @private
|
|
514
517
|
*/
|
|
515
|
-
_spawnClaudeTask(context) {
|
|
516
|
-
return spawnClaudeTask(this, context);
|
|
518
|
+
_spawnClaudeTask(context, options = {}) {
|
|
519
|
+
return spawnClaudeTask(this, context, options);
|
|
517
520
|
}
|
|
518
521
|
|
|
519
522
|
/**
|