@the-open-engine/zeroshot 6.9.1 → 6.10.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/inspect.js +1 -0
- package/cli/index.js +1 -1
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +15 -2
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +32 -2
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/common.js +1 -1
- package/lib/agent-cli-provider/adapters/common.js.map +1 -1
- package/lib/agent-cli-provider/adapters/index.d.ts +1 -0
- package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/index.js +8 -0
- package/lib/agent-cli-provider/adapters/index.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +1 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +1 -1
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +2 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +9 -0
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +3 -0
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +4 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-context-builder.js +90 -4
- package/src/agent/agent-context-sources.js +20 -2
- package/src/agent/agent-lifecycle.js +30 -2
- package/src/agent/agent-task-executor.js +31 -1
- package/src/agent/guidance-queue.js +29 -7
- package/src/agent/provider-session.js +277 -0
- package/src/agent-cli-provider/adapters/claude.ts +18 -3
- package/src/agent-cli-provider/adapters/codex.ts +47 -3
- package/src/agent-cli-provider/adapters/common.ts +1 -1
- package/src/agent-cli-provider/adapters/index.ts +10 -0
- package/src/agent-cli-provider/contract-options.ts +1 -0
- package/src/agent-cli-provider/index.ts +1 -0
- package/src/agent-cli-provider/provider-registry.ts +13 -1
- package/src/agent-cli-provider/types.ts +4 -0
- package/src/agent-wrapper.js +44 -8
- package/src/ledger-sequence.js +63 -0
- package/src/ledger.js +114 -29
- package/src/orchestrator.js +40 -2
- package/task-lib/attachable-watcher.js +27 -5
- package/task-lib/commands/resume.js +29 -6
- package/task-lib/commands/status.js +3 -0
- package/task-lib/provider-helper-runtime.js +1 -0
- package/task-lib/provider-session-capture.js +138 -0
- package/task-lib/runner.js +10 -2
- package/task-lib/store.js +58 -6
- package/task-lib/watcher.js +27 -5
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { normalizeProviderName, providerSupportsCapability } = require('../../lib/provider-names');
|
|
5
|
+
const { tryCanonicalMessageSequence } = require('../ledger-sequence');
|
|
6
|
+
|
|
7
|
+
const DURABLE_SESSION_BOUNDARY_EVENTS = new Set([
|
|
8
|
+
'TASK_STARTED',
|
|
9
|
+
'TASK_COMPLETED',
|
|
10
|
+
'TASK_FAILED',
|
|
11
|
+
'RETRY_SCHEDULED',
|
|
12
|
+
'AGENT_RESTART_ATTEMPT',
|
|
13
|
+
]);
|
|
14
|
+
const RESTORABLE_AGENT_STATES = new Set(['idle', 'stopped', 'completed']);
|
|
15
|
+
|
|
16
|
+
function normalizeNonEmptyString(value) {
|
|
17
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeAbsolutePath(value) {
|
|
21
|
+
const normalized = normalizeNonEmptyString(value);
|
|
22
|
+
return normalized ? path.resolve(normalized) : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeCursor(value) {
|
|
26
|
+
return tryCanonicalMessageSequence(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeNullableCursor(value) {
|
|
30
|
+
return value === null ? null : normalizeCursor(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizePromptIdentity(value) {
|
|
34
|
+
if (value === null) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value) ? value : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function promptIdentity(value) {
|
|
41
|
+
if (value === null || value === undefined) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return `sha256:${crypto.createHash('sha256').update(String(value)).digest('hex')}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function supportsSessionResume(providerName) {
|
|
48
|
+
try {
|
|
49
|
+
return providerSupportsCapability(providerName, 'sessionResume');
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeProviderSession(value) {
|
|
56
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const provider = normalizeProviderName(value.provider);
|
|
61
|
+
const sessionId = normalizeNonEmptyString(value.sessionId);
|
|
62
|
+
const agentId = normalizeNonEmptyString(value.agentId);
|
|
63
|
+
const taskId = normalizeNonEmptyString(value.taskId);
|
|
64
|
+
const generation = value.generation;
|
|
65
|
+
const cwd = normalizeAbsolutePath(value.cwd);
|
|
66
|
+
const worktreePath =
|
|
67
|
+
value.worktreePath === null ? null : normalizeAbsolutePath(value.worktreePath);
|
|
68
|
+
const contextSequence = normalizeCursor(value.contextSequence);
|
|
69
|
+
const guidanceSequence = normalizeNullableCursor(value.guidanceSequence);
|
|
70
|
+
const normalizedPromptIdentity = normalizePromptIdentity(value.promptIdentity);
|
|
71
|
+
|
|
72
|
+
if (
|
|
73
|
+
!provider ||
|
|
74
|
+
!sessionId ||
|
|
75
|
+
!agentId ||
|
|
76
|
+
!taskId ||
|
|
77
|
+
!Number.isInteger(generation) ||
|
|
78
|
+
generation < 1 ||
|
|
79
|
+
!cwd ||
|
|
80
|
+
!Object.hasOwn(value, 'worktreePath') ||
|
|
81
|
+
(value.worktreePath !== null && !worktreePath) ||
|
|
82
|
+
contextSequence === null ||
|
|
83
|
+
!Object.hasOwn(value, 'guidanceSequence') ||
|
|
84
|
+
(value.guidanceSequence !== null && guidanceSequence === null) ||
|
|
85
|
+
!Object.hasOwn(value, 'promptIdentity') ||
|
|
86
|
+
normalizedPromptIdentity === undefined ||
|
|
87
|
+
!supportsSessionResume(provider)
|
|
88
|
+
) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
provider,
|
|
94
|
+
sessionId,
|
|
95
|
+
agentId,
|
|
96
|
+
taskId,
|
|
97
|
+
generation,
|
|
98
|
+
cwd,
|
|
99
|
+
worktreePath,
|
|
100
|
+
contextSequence,
|
|
101
|
+
guidanceSequence,
|
|
102
|
+
promptIdentity: normalizedPromptIdentity,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function agentWorkspaceProvenance(agent) {
|
|
107
|
+
const worktreePath = agent?.worktree?.enabled ? normalizeAbsolutePath(agent.worktree.path) : null;
|
|
108
|
+
const cwd = normalizeAbsolutePath(agent?.config?.cwd || worktreePath || process.cwd());
|
|
109
|
+
return { cwd, worktreePath };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function agentCanReuseSession(agent, providerName) {
|
|
113
|
+
return !agent?.isolation?.enabled && supportsSessionResume(providerName);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sessionMatchesAgent(session, agent, providerName, expectedGeneration) {
|
|
117
|
+
const provider = normalizeProviderName(providerName);
|
|
118
|
+
const workspace = agentWorkspaceProvenance(agent);
|
|
119
|
+
return (
|
|
120
|
+
agentCanReuseSession(agent, provider) &&
|
|
121
|
+
session.provider === provider &&
|
|
122
|
+
session.agentId === agent?.id &&
|
|
123
|
+
session.generation === expectedGeneration &&
|
|
124
|
+
session.cwd === workspace.cwd &&
|
|
125
|
+
session.worktreePath === workspace.worktreePath
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveAgentProviderSession(agent, providerName) {
|
|
130
|
+
const stored = normalizeProviderSession(agent?.providerSession);
|
|
131
|
+
const expectedGeneration = Number.isInteger(agent?.iteration) ? agent.iteration - 1 : -1;
|
|
132
|
+
|
|
133
|
+
if (!stored || !sessionMatchesAgent(stored, agent, providerName, expectedGeneration)) {
|
|
134
|
+
if (agent) {
|
|
135
|
+
if (agent.providerSession) {
|
|
136
|
+
agent.lastGuidanceAppliedId = null;
|
|
137
|
+
}
|
|
138
|
+
agent.providerSession = null;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return stored;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveAgentResumeSessionId(agent, providerName) {
|
|
147
|
+
return resolveAgentProviderSession(agent, providerName)?.sessionId || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function validateCompletedResumeIdentity(taskInfo) {
|
|
151
|
+
const requestedSessionId = normalizeNonEmptyString(taskInfo?.requestedResumeSessionId);
|
|
152
|
+
if (!requestedSessionId) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (taskInfo?.resumeIdentityVerified !== true) {
|
|
156
|
+
return 'Provider continuation identity was not durably verified';
|
|
157
|
+
}
|
|
158
|
+
if (taskInfo?.sessionIdConflict === true) {
|
|
159
|
+
return 'Provider continuation emitted conflicting session identities';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const capturedSessionId = normalizeNonEmptyString(taskInfo?.sessionId);
|
|
163
|
+
if (capturedSessionId === requestedSessionId) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return capturedSessionId
|
|
168
|
+
? 'Provider continuation returned a different session identity'
|
|
169
|
+
: 'Provider continuation did not confirm the requested session identity';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function providerSessionFromCompletedTask({
|
|
173
|
+
agent,
|
|
174
|
+
providerName,
|
|
175
|
+
taskInfo,
|
|
176
|
+
logicalSuccess = true,
|
|
177
|
+
}) {
|
|
178
|
+
const provider = normalizeProviderName(providerName);
|
|
179
|
+
if (!logicalSuccess || !agentCanReuseSession(agent, provider)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (!taskInfo || taskInfo.status !== 'completed') {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (normalizeProviderName(taskInfo.provider) !== provider) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (taskInfo.sessionIdConflict === true) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (validateCompletedResumeIdentity(taskInfo)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const sessionId = normalizeNonEmptyString(taskInfo.sessionId);
|
|
196
|
+
const taskId = normalizeNonEmptyString(taskInfo.id);
|
|
197
|
+
const generation = agent?.iteration;
|
|
198
|
+
const agentId = normalizeNonEmptyString(agent?.id);
|
|
199
|
+
const workspace = agentWorkspaceProvenance(agent);
|
|
200
|
+
return normalizeProviderSession({
|
|
201
|
+
provider,
|
|
202
|
+
sessionId,
|
|
203
|
+
agentId,
|
|
204
|
+
taskId,
|
|
205
|
+
generation,
|
|
206
|
+
...workspace,
|
|
207
|
+
contextSequence: agent?.currentContextSequence,
|
|
208
|
+
guidanceSequence: agent?.currentGuidanceSequence ?? null,
|
|
209
|
+
promptIdentity: agent?.currentPromptIdentity ?? null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function updateAgentProviderSession(agent, value) {
|
|
214
|
+
if (agent.providerSession && value === null) {
|
|
215
|
+
agent.lastGuidanceAppliedId = null;
|
|
216
|
+
}
|
|
217
|
+
const session = normalizeProviderSession(value);
|
|
218
|
+
agent.providerSession = session;
|
|
219
|
+
return session;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readLastDurableSessionBoundary(messageBus, clusterId, agentId) {
|
|
223
|
+
const lifecycle = messageBus.query({
|
|
224
|
+
cluster_id: clusterId,
|
|
225
|
+
topic: 'AGENT_LIFECYCLE',
|
|
226
|
+
sender: agentId,
|
|
227
|
+
afterId: '0',
|
|
228
|
+
});
|
|
229
|
+
return lifecycle
|
|
230
|
+
.filter((message) => DURABLE_SESSION_BOUNDARY_EVENTS.has(message.content?.data?.event))
|
|
231
|
+
.at(-1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function restoreAgentProviderSession({ agent, savedState, messageBus, clusterId }) {
|
|
235
|
+
const session = normalizeProviderSession(savedState?.providerSession);
|
|
236
|
+
if (!session || !RESTORABLE_AGENT_STATES.has(savedState?.state)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
if (
|
|
240
|
+
!Object.hasOwn(savedState, 'lastGuidanceAppliedId') ||
|
|
241
|
+
normalizeNullableCursor(savedState.lastGuidanceAppliedId) !== session.guidanceSequence
|
|
242
|
+
) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
if (!sessionMatchesAgent(session, agent, session.provider, savedState.iteration)) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const boundary = readLastDurableSessionBoundary(messageBus, clusterId, agent.id);
|
|
250
|
+
const data = boundary?.content?.data;
|
|
251
|
+
if (
|
|
252
|
+
data?.event !== 'TASK_COMPLETED' ||
|
|
253
|
+
data.taskId !== session.taskId ||
|
|
254
|
+
data.iteration !== session.generation ||
|
|
255
|
+
normalizeProviderName(data.provider) !== session.provider ||
|
|
256
|
+
normalizeCursor(data.contextSequence) !== session.contextSequence ||
|
|
257
|
+
normalizeNullableCursor(data.guidanceSequence) !== session.guidanceSequence ||
|
|
258
|
+
data.promptIdentity !== session.promptIdentity
|
|
259
|
+
) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return session;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = {
|
|
267
|
+
agentWorkspaceProvenance,
|
|
268
|
+
normalizeProviderSession,
|
|
269
|
+
promptIdentity,
|
|
270
|
+
providerSessionFromCompletedTask,
|
|
271
|
+
resolveAgentProviderSession,
|
|
272
|
+
resolveAgentResumeSessionId,
|
|
273
|
+
restoreAgentProviderSession,
|
|
274
|
+
supportsSessionResume,
|
|
275
|
+
updateAgentProviderSession,
|
|
276
|
+
validateCompletedResumeIdentity,
|
|
277
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { stringifyJson } from '../json';
|
|
1
|
+
import { getString, isRecord, stringifyJson, tryParseJson } from '../json';
|
|
2
2
|
import {
|
|
3
3
|
type BuildProviderCommandOptions,
|
|
4
4
|
type ClaudeCliFeatures,
|
|
@@ -66,6 +66,7 @@ function detectCliFeatures(helpText?: string | null): ClaudeCliFeatures {
|
|
|
66
66
|
supportsVerbose: unknown ? true : /--verbose/.test(help),
|
|
67
67
|
supportsModel: unknown ? true : /--model/.test(help),
|
|
68
68
|
supportsEffort: unknown ? true : /--effort/.test(help),
|
|
69
|
+
supportsResume: unknown ? true : /--resume/.test(help),
|
|
69
70
|
unknown,
|
|
70
71
|
};
|
|
71
72
|
}
|
|
@@ -115,15 +116,28 @@ function addAutoApproveArgs(args: string[], options: BuildProviderCommandOptions
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
function addSessionArgs(args: string[], options: BuildProviderCommandOptions): void {
|
|
118
|
-
|
|
119
|
+
const features = optionFeatures(options);
|
|
120
|
+
if ((options.resumeSessionId || options.continueSession) && features.supportsResume === false) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Claude CLI cannot safely run continuation context because this installation lacks --resume.'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (options.resumeSessionId && features.supportsResume !== false) {
|
|
119
126
|
args.push('--resume', options.resumeSessionId);
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
|
-
if (options.continueSession) {
|
|
129
|
+
if (options.continueSession && features.supportsResume !== false) {
|
|
123
130
|
args.push('--continue');
|
|
124
131
|
}
|
|
125
132
|
}
|
|
126
133
|
|
|
134
|
+
function extractSessionId(line: string): string | null {
|
|
135
|
+
const event = tryParseJson(line.trim());
|
|
136
|
+
if (!isRecord(event)) return null;
|
|
137
|
+
const sessionId = getString(event, 'session_id');
|
|
138
|
+
return sessionId?.trim() || null;
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
|
|
128
142
|
const features = optionFeatures(options);
|
|
129
143
|
const warnings: WarningMetadata[] = [];
|
|
@@ -228,6 +242,7 @@ export const claudeAdapter: ProviderAdapter = {
|
|
|
228
242
|
defaultMinLevel: 'level1',
|
|
229
243
|
detectCliFeatures,
|
|
230
244
|
buildCommand,
|
|
245
|
+
extractSessionId,
|
|
231
246
|
parseEvent: parseClaudeEvent,
|
|
232
247
|
createParserState: () => createParserState('claude'),
|
|
233
248
|
resolveModelSpec,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getString, isRecord, tryParseJson } from '../json';
|
|
1
2
|
import { appendJsonSchemaPrompt, writeStrictOutputSchemaFile } from '../schema';
|
|
2
3
|
import {
|
|
3
4
|
type BuildProviderCommandOptions,
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
createParserState,
|
|
19
20
|
optionFeatures,
|
|
20
21
|
resolveModelSpecWithConfig,
|
|
21
|
-
unsupportedSessionControlWarnings,
|
|
22
22
|
validateModelIdFromCatalog,
|
|
23
23
|
warning,
|
|
24
24
|
} from './common';
|
|
@@ -58,10 +58,18 @@ function detectCliFeatures(helpText?: string | null): CodexCliFeatures {
|
|
|
58
58
|
supportsConfigOverride: supports(help, /--config\b/),
|
|
59
59
|
supportsModel: supports(help, /\s-m\b/) || supports(help, /--model\b/),
|
|
60
60
|
supportsSkipGitRepoCheck: supports(help, /--skip-git-repo-check\b/),
|
|
61
|
+
supportsResume: supports(help, /\bresume\b/),
|
|
61
62
|
unknown,
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
function extractSessionId(line: string): string | null {
|
|
67
|
+
const event = tryParseJson(line.trim());
|
|
68
|
+
if (!isRecord(event) || getString(event, 'type') !== 'thread.started') return null;
|
|
69
|
+
const sessionId = getString(event, 'thread_id');
|
|
70
|
+
return sessionId?.trim() || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
function addOutputArgs(args: string[], options: BuildProviderCommandOptions): void {
|
|
66
74
|
const features = optionFeatures(options);
|
|
67
75
|
if (
|
|
@@ -121,7 +129,25 @@ function applySchemaArgs(
|
|
|
121
129
|
|
|
122
130
|
function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
|
|
123
131
|
const features = optionFeatures(options);
|
|
124
|
-
const warnings: WarningMetadata[] =
|
|
132
|
+
const warnings: WarningMetadata[] = [];
|
|
133
|
+
if (options.continueSession) {
|
|
134
|
+
warnings.push(
|
|
135
|
+
warning(
|
|
136
|
+
'codex',
|
|
137
|
+
'unsupported-session-control',
|
|
138
|
+
'Codex requires an explicit session ID; ignoring continueSession.'
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (options.resumeSessionId && features.supportsResume === false) {
|
|
143
|
+
warnings.push(
|
|
144
|
+
warning(
|
|
145
|
+
'codex',
|
|
146
|
+
'codex-session-resume-unsupported',
|
|
147
|
+
'Codex CLI does not support exec resume; starting a fresh session.'
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
125
151
|
if (options.autoApprove && features.supportsAutoApprove === false) {
|
|
126
152
|
warnings.push(
|
|
127
153
|
warning(
|
|
@@ -153,16 +179,33 @@ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[
|
|
|
153
179
|
}
|
|
154
180
|
|
|
155
181
|
function buildCommand(context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
|
|
182
|
+
if (options.resumeSessionId && optionFeatures(options).supportsResume === false) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
'Codex CLI cannot safely run continuation context because this installation lacks exec resume.'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
156
187
|
const args: string[] = ['exec'];
|
|
157
188
|
const cleanup: string[] = [];
|
|
189
|
+
const resumeSessionId =
|
|
190
|
+
options.resumeSessionId && optionFeatures(options).supportsResume !== false
|
|
191
|
+
? options.resumeSessionId
|
|
192
|
+
: null;
|
|
193
|
+
if (resumeSessionId) {
|
|
194
|
+
args.push('resume');
|
|
195
|
+
}
|
|
158
196
|
|
|
159
197
|
addOutputArgs(args, options);
|
|
160
198
|
addModelArgs(args, options);
|
|
161
|
-
|
|
199
|
+
if (!resumeSessionId) {
|
|
200
|
+
addCwdArgs(args, options);
|
|
201
|
+
}
|
|
162
202
|
addAutoApproveArgs(args, options);
|
|
163
203
|
addSkipGitArgs(args, options);
|
|
164
204
|
const finalContext = applySchemaArgs(args, cleanup, context, options);
|
|
165
205
|
|
|
206
|
+
if (resumeSessionId) {
|
|
207
|
+
args.push(resumeSessionId);
|
|
208
|
+
}
|
|
166
209
|
args.push(finalContext);
|
|
167
210
|
|
|
168
211
|
return commandSpec({
|
|
@@ -216,6 +259,7 @@ export const codexAdapter: ProviderAdapter = {
|
|
|
216
259
|
defaultMinLevel: 'level1',
|
|
217
260
|
detectCliFeatures,
|
|
218
261
|
buildCommand,
|
|
262
|
+
extractSessionId,
|
|
219
263
|
parseEvent: parseCodexEvent,
|
|
220
264
|
createParserState: () => createParserState('codex'),
|
|
221
265
|
resolveModelSpec,
|
|
@@ -64,7 +64,7 @@ export function unsupportedSessionControlWarnings(
|
|
|
64
64
|
warning(
|
|
65
65
|
provider,
|
|
66
66
|
'unsupported-session-control',
|
|
67
|
-
|
|
67
|
+
`Provider ${provider} does not support resume/continue session control; ignoring.`
|
|
68
68
|
),
|
|
69
69
|
];
|
|
70
70
|
}
|
|
@@ -79,6 +79,16 @@ export function parseProviderChunk(
|
|
|
79
79
|
return events;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export function extractProviderSessionId(
|
|
83
|
+
providerName: KnownProviderName | string,
|
|
84
|
+
line: string
|
|
85
|
+
): string | null {
|
|
86
|
+
const adapter = getProviderAdapter(providerName || 'claude');
|
|
87
|
+
const content = stripTimestampPrefix(line);
|
|
88
|
+
if (!content || !adapter.extractSessionId) return null;
|
|
89
|
+
return adapter.extractSessionId(content);
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
export function resolveModelSpec(
|
|
83
93
|
providerName: KnownProviderName | string,
|
|
84
94
|
level: ModelLevel,
|
|
@@ -19,6 +19,7 @@ export interface ProviderCapabilities {
|
|
|
19
19
|
readonly streamJson: ProviderCapabilityState;
|
|
20
20
|
readonly thinkingMode: ProviderCapabilityState;
|
|
21
21
|
readonly reasoningEffort: ProviderCapabilityState;
|
|
22
|
+
readonly sessionResume: ProviderCapabilityState;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
interface FixedProviderCommandSpec {
|
|
@@ -93,13 +94,22 @@ export interface ProviderRegistryEntry {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
const STANDARD_CAPABILITIES: Readonly<
|
|
96
|
-
Pick<
|
|
97
|
+
Pick<
|
|
98
|
+
ProviderCapabilities,
|
|
99
|
+
| 'dockerIsolation'
|
|
100
|
+
| 'worktreeIsolation'
|
|
101
|
+
| 'mcpServers'
|
|
102
|
+
| 'streamJson'
|
|
103
|
+
| 'thinkingMode'
|
|
104
|
+
| 'sessionResume'
|
|
105
|
+
>
|
|
97
106
|
> = {
|
|
98
107
|
dockerIsolation: true,
|
|
99
108
|
worktreeIsolation: true,
|
|
100
109
|
mcpServers: true,
|
|
101
110
|
streamJson: true,
|
|
102
111
|
thinkingMode: true,
|
|
112
|
+
sessionResume: false,
|
|
103
113
|
};
|
|
104
114
|
|
|
105
115
|
const CLAUDE_DOCKER_ENV_PASSTHROUGH = [
|
|
@@ -161,6 +171,7 @@ export const providerRegistry = [
|
|
|
161
171
|
...STANDARD_CAPABILITIES,
|
|
162
172
|
jsonSchema: true,
|
|
163
173
|
reasoningEffort: true,
|
|
174
|
+
sessionResume: true,
|
|
164
175
|
},
|
|
165
176
|
docs: {
|
|
166
177
|
label: 'Claude',
|
|
@@ -197,6 +208,7 @@ export const providerRegistry = [
|
|
|
197
208
|
...STANDARD_CAPABILITIES,
|
|
198
209
|
jsonSchema: true,
|
|
199
210
|
reasoningEffort: true,
|
|
211
|
+
sessionResume: true,
|
|
200
212
|
},
|
|
201
213
|
docs: {
|
|
202
214
|
label: 'Codex',
|
|
@@ -101,6 +101,7 @@ export interface ClaudeCliFeatures extends BaseCliFeatures {
|
|
|
101
101
|
readonly supportsVerbose: boolean;
|
|
102
102
|
readonly supportsModel: boolean;
|
|
103
103
|
readonly supportsEffort: boolean;
|
|
104
|
+
readonly supportsResume: boolean;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
export interface CodexCliFeatures extends BaseCliFeatures {
|
|
@@ -112,6 +113,7 @@ export interface CodexCliFeatures extends BaseCliFeatures {
|
|
|
112
113
|
readonly supportsConfigOverride: boolean;
|
|
113
114
|
readonly supportsModel: boolean;
|
|
114
115
|
readonly supportsSkipGitRepoCheck: boolean;
|
|
116
|
+
readonly supportsResume: boolean;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
export interface GeminiCliFeatures extends BaseCliFeatures {
|
|
@@ -212,6 +214,7 @@ export interface CliFeatureOverrides {
|
|
|
212
214
|
readonly supportsNoAskUser?: boolean;
|
|
213
215
|
readonly supportsAddDir?: boolean;
|
|
214
216
|
readonly supportsMcpConfig?: boolean;
|
|
217
|
+
readonly supportsResume?: boolean;
|
|
215
218
|
readonly supportsBundledRunner?: boolean;
|
|
216
219
|
readonly supportsAcpStdio?: boolean;
|
|
217
220
|
readonly supportsPromptImages?: boolean;
|
|
@@ -368,6 +371,7 @@ export interface ProviderAdapter {
|
|
|
368
371
|
readonly defaultMinLevel: ModelLevel;
|
|
369
372
|
detectCliFeatures(helpText?: string | null): ProviderCliFeatures;
|
|
370
373
|
buildCommand(context: string, options?: BuildProviderCommandOptions): CommandSpec;
|
|
374
|
+
extractSessionId?(line: string): string | null;
|
|
371
375
|
parseEvent(line: string, state: ProviderParserState): ProviderParseResult;
|
|
372
376
|
createParserState(): ProviderParserState;
|
|
373
377
|
resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec;
|
package/src/agent-wrapper.js
CHANGED
|
@@ -17,6 +17,11 @@ const { normalizeProviderName } = require('../lib/provider-names');
|
|
|
17
17
|
const { getProvider } = require('./providers');
|
|
18
18
|
const { buildContext } = require('./agent/agent-context-builder');
|
|
19
19
|
const { collectQueuedGuidance } = require('./agent/guidance-queue');
|
|
20
|
+
const {
|
|
21
|
+
promptIdentity,
|
|
22
|
+
resolveAgentProviderSession,
|
|
23
|
+
updateAgentProviderSession,
|
|
24
|
+
} = require('./agent/provider-session');
|
|
20
25
|
const { findMatchingTrigger, evaluateTrigger } = require('./agent/agent-trigger-evaluator');
|
|
21
26
|
const { executeHook } = require('./agent/agent-hook-executor');
|
|
22
27
|
const { injectInput: injectAgentInput } = require('./agent/agent-input-injector');
|
|
@@ -66,6 +71,11 @@ class AgentWrapper {
|
|
|
66
71
|
this.currentTask = null;
|
|
67
72
|
/** @type {string | null} */
|
|
68
73
|
this.currentTaskId = null; // Track spawned task ID for resume capability
|
|
74
|
+
/** @type {{provider: string, sessionId: string, agentId: string, taskId: string, generation: number, cwd: string, worktreePath: string|null, contextSequence: string, guidanceSequence: string|null, promptIdentity: string|null} | null} */
|
|
75
|
+
this.providerSession = null; // Provider continuation state, owned by this logical agent only
|
|
76
|
+
this.currentContextSequence = '0';
|
|
77
|
+
this.currentGuidanceSequence = null;
|
|
78
|
+
this.currentPromptIdentity = null;
|
|
69
79
|
/** @type {number | null} */
|
|
70
80
|
this.processPid = null; // Track process PID for resource monitoring
|
|
71
81
|
this.running = false;
|
|
@@ -76,7 +86,7 @@ class AgentWrapper {
|
|
|
76
86
|
/** @type {number | null} */
|
|
77
87
|
this.lastAgentStartTime = null; // Track when agent last began executing (for context filtering)
|
|
78
88
|
/** @type {number | null} */
|
|
79
|
-
this.
|
|
89
|
+
this.lastGuidanceAppliedId = null; // Track last queued guidance sequence applied to prompt
|
|
80
90
|
|
|
81
91
|
// LIVENESS DETECTION - Track output freshness to detect stuck agents
|
|
82
92
|
/** @type {number | null} */
|
|
@@ -119,6 +129,7 @@ class AgentWrapper {
|
|
|
119
129
|
|
|
120
130
|
this.testMode = options.testMode || false;
|
|
121
131
|
this.quiet = options.quiet || false;
|
|
132
|
+
this.providerCliFeatures = options.providerCliFeatures || null;
|
|
122
133
|
|
|
123
134
|
// ISOLATION SUPPORT - Run tasks inside Docker container
|
|
124
135
|
this.isolation = options.isolation || null;
|
|
@@ -435,12 +446,29 @@ class AgentWrapper {
|
|
|
435
446
|
*/
|
|
436
447
|
_buildContext(triggeringMessage) {
|
|
437
448
|
const previousAgentStart = this.lastAgentStartTime;
|
|
449
|
+
const providerName = this._resolveProvider();
|
|
450
|
+
let providerSession = resolveAgentProviderSession(this, providerName);
|
|
451
|
+
if (providerSession && !this._providerSupportsSessionResume(providerName)) {
|
|
452
|
+
updateAgentProviderSession(this, null);
|
|
453
|
+
providerSession = null;
|
|
454
|
+
}
|
|
455
|
+
const contextMode = providerSession ? 'continuation' : 'full';
|
|
456
|
+
const selectedPrompt = this._selectPrompt();
|
|
457
|
+
const latestMessage = this.messageBus.findLast({
|
|
458
|
+
cluster_id: this.cluster.id,
|
|
459
|
+
orderBySequence: true,
|
|
460
|
+
});
|
|
461
|
+
this.currentContextSequence = latestMessage?.sequence || '0';
|
|
438
462
|
const queuedGuidance = collectQueuedGuidance({
|
|
439
463
|
messageBus: this.messageBus,
|
|
440
464
|
clusterId: this.cluster.id,
|
|
441
465
|
agentId: this.id,
|
|
442
|
-
|
|
466
|
+
afterId: this.lastGuidanceAppliedId,
|
|
467
|
+
throughId: this.currentContextSequence,
|
|
443
468
|
});
|
|
469
|
+
this.currentGuidanceSequence =
|
|
470
|
+
queuedGuidance.latestSequence ?? this.lastGuidanceAppliedId ?? null;
|
|
471
|
+
this.currentPromptIdentity = promptIdentity(selectedPrompt);
|
|
444
472
|
const context = buildContext({
|
|
445
473
|
id: this.id,
|
|
446
474
|
role: this.role,
|
|
@@ -451,27 +479,35 @@ class AgentWrapper {
|
|
|
451
479
|
lastTaskEndTime: this.lastTaskEndTime,
|
|
452
480
|
lastAgentStartTime: previousAgentStart,
|
|
453
481
|
triggeringMessage,
|
|
454
|
-
selectedPrompt
|
|
482
|
+
selectedPrompt,
|
|
455
483
|
queuedGuidance: queuedGuidance.guidanceBlock,
|
|
484
|
+
mode: contextMode,
|
|
485
|
+
continuationSequence: providerSession?.contextSequence,
|
|
486
|
+
contextThroughId: this.currentContextSequence,
|
|
487
|
+
previousPromptIdentity: providerSession?.promptIdentity,
|
|
488
|
+
currentPromptIdentity: this.currentPromptIdentity,
|
|
456
489
|
// Pass isolation state for conditional git restriction
|
|
457
490
|
worktree: this.worktree,
|
|
458
491
|
isolation: this.isolation,
|
|
459
492
|
});
|
|
460
493
|
|
|
461
494
|
// Record when this iteration started so future "since: last_agent_start" filters work.
|
|
462
|
-
const latestMessage = this.messageBus.findLast({ cluster_id: this.cluster.id });
|
|
463
495
|
const latestTimestamp = latestMessage?.timestamp;
|
|
464
496
|
const now = Date.now();
|
|
465
497
|
this.lastAgentStartTime =
|
|
466
498
|
typeof latestTimestamp === 'number' ? Math.max(now, latestTimestamp + 1) : now;
|
|
467
499
|
|
|
468
|
-
if (queuedGuidance.latestTimestamp !== null) {
|
|
469
|
-
this.lastGuidanceAppliedAt = queuedGuidance.latestTimestamp;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
500
|
return context;
|
|
473
501
|
}
|
|
474
502
|
|
|
503
|
+
_providerSupportsSessionResume(providerName) {
|
|
504
|
+
const override = this.providerCliFeatures?.[providerName];
|
|
505
|
+
if (override && Object.hasOwn(override, 'supportsResume')) {
|
|
506
|
+
return override.supportsResume === true;
|
|
507
|
+
}
|
|
508
|
+
return getProvider(providerName).getCliFeatures().supportsResume === true;
|
|
509
|
+
}
|
|
510
|
+
|
|
475
511
|
/**
|
|
476
512
|
* Spawn claude-zeroshots process and stream output via message bus
|
|
477
513
|
* @private
|