@11agents/cli 0.1.14 → 0.1.15
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 +3 -2
- package/package.json +1 -1
- package/src/commands/logs.js +4 -0
- package/src/commands/runtime.js +179 -29
package/README.md
CHANGED
|
@@ -107,10 +107,11 @@ On startup, and every 30 minutes after that, the daemon syncs project metadata a
|
|
|
107
107
|
- Cloud database snapshot: `~/.11agents/<project>/database/snapshot.json`
|
|
108
108
|
- Task scratch directory: `~/.11agents/<project>/tmp/<taskId>/`
|
|
109
109
|
- Task execution logs: `~/.11agents/<project>/runs/<taskId>/`
|
|
110
|
+
- Current claimed task marker: `~/.11agents/claim_id`
|
|
110
111
|
|
|
111
|
-
Codex runs from `~/.11agents/<project>/` by default. Treat that directory as read-only project context. Task code may write temporary files only under `./tmp/<taskId>/`; the daemon removes that task scratch directory after the task finishes. Agent environment variables from the control plane are injected into the Codex child process and are not written to disk.
|
|
112
|
+
Codex runs from `~/.11agents/<project>/` by default. Treat that directory as read-only project context. Task code may write temporary files only under `./tmp/<taskId>/`; the daemon removes that task scratch directory after the task finishes. Agent environment variables from the control plane are injected into the Codex child process and are not written to disk. Each run directory stores `prompt.md`, raw Codex JSONL in `stdout.log`, readable Codex dialogue in `transcript.log`, `stderr.log`, `last_message.md`, `completion.json`, and `meta.json`.
|
|
112
113
|
|
|
113
|
-
The built-in Codex worker starts task executions with `codex --yolo exec` and prefixes the task prompt with `/goal ` by default so remote runtime tasks can run without approval prompts or sandbox restrictions. To opt a daemon back into a Codex sandbox, start it with `--codex-sandbox read-only`, `--codex-sandbox workspace-write`, or `--codex-sandbox danger-full-access`.
|
|
114
|
+
The built-in Codex worker starts task executions with `codex --yolo exec --json --output-last-message <run>/last_message.md` and prefixes the task prompt with `/goal ` by default so remote runtime tasks can run without approval prompts or sandbox restrictions. To opt a daemon back into a Codex sandbox, start it with `--codex-sandbox read-only`, `--codex-sandbox workspace-write`, or `--codex-sandbox danger-full-access`.
|
|
114
115
|
|
|
115
116
|
The built-in task runner currently supports Codex tasks. A custom handler may export:
|
|
116
117
|
|
package/package.json
CHANGED
package/src/commands/logs.js
CHANGED
|
@@ -58,6 +58,7 @@ export async function formatTaskLog({ taskId, project = '', homeDir = homedir(),
|
|
|
58
58
|
|
|
59
59
|
const stdout = tailText(await readText(path.join(runDir, 'stdout.log'), ''), tail)
|
|
60
60
|
const stderr = tailText(await readText(path.join(runDir, 'stderr.log'), ''), tail)
|
|
61
|
+
const transcript = tailText(await readText(path.join(runDir, 'transcript.log'), ''), tail)
|
|
61
62
|
const completion = await readText(path.join(runDir, 'completion.json'), '')
|
|
62
63
|
return [
|
|
63
64
|
`task: ${meta.task_id || taskId}`,
|
|
@@ -67,6 +68,9 @@ export async function formatTaskLog({ taskId, project = '', homeDir = homedir(),
|
|
|
67
68
|
`exit_code: ${meta.exit_code ?? ''}`,
|
|
68
69
|
`run_dir: ${runDir}`,
|
|
69
70
|
'',
|
|
71
|
+
'transcript:',
|
|
72
|
+
transcript,
|
|
73
|
+
'',
|
|
70
74
|
'stdout:',
|
|
71
75
|
stdout,
|
|
72
76
|
'',
|
package/src/commands/runtime.js
CHANGED
|
@@ -82,6 +82,29 @@ function runtimeDeps(overrides = {}) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function currentClaimPath(homeDir) {
|
|
86
|
+
return path.join(homeDir, '.11agents', 'claim_id')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function writeCurrentClaim(deps, task, machineKey) {
|
|
90
|
+
const payload = {
|
|
91
|
+
task_id: String(task.id || ''),
|
|
92
|
+
runtime_id: String(task.runtime_id || task.runtime?.id || ''),
|
|
93
|
+
machine_key: String(task.runtime?.machine_key || machineKey || ''),
|
|
94
|
+
}
|
|
95
|
+
await mkdir(path.dirname(currentClaimPath(deps.homeDir)), { recursive: true })
|
|
96
|
+
await writeFile(currentClaimPath(deps.homeDir), JSON.stringify(payload))
|
|
97
|
+
return payload
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function readCurrentClaim(deps) {
|
|
101
|
+
return readJsonFile(currentClaimPath(deps.homeDir), null)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function clearCurrentClaim(deps) {
|
|
105
|
+
await rm(currentClaimPath(deps.homeDir), { force: true })
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
function errorMessage(error) {
|
|
86
109
|
return error instanceof Error ? error.message : String(error)
|
|
87
110
|
}
|
|
@@ -263,6 +286,98 @@ function normalizeTaskCompletion(task, completion) {
|
|
|
263
286
|
return body
|
|
264
287
|
}
|
|
265
288
|
|
|
289
|
+
function failedClaimCompletionBody(claim, comment) {
|
|
290
|
+
return {
|
|
291
|
+
task_id: String(claim?.task_id || ''),
|
|
292
|
+
runtime_id: String(claim?.runtime_id || ''),
|
|
293
|
+
machine_key: String(claim?.machine_key || ''),
|
|
294
|
+
comment,
|
|
295
|
+
memory_delta: '',
|
|
296
|
+
status: 'failed',
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function codexTranscriptFromJsonl(text) {
|
|
301
|
+
const lines = []
|
|
302
|
+
for (const line of String(text || '').split('\n')) {
|
|
303
|
+
const trimmed = line.trim()
|
|
304
|
+
if (!trimmed) continue
|
|
305
|
+
try {
|
|
306
|
+
const event = JSON.parse(trimmed)
|
|
307
|
+
if (event?.type === 'item.completed') {
|
|
308
|
+
const item = event.item || {}
|
|
309
|
+
if (item.type === 'agent_message' && item.text) {
|
|
310
|
+
lines.push(`assistant: ${String(item.text).trim()}`)
|
|
311
|
+
} else if (item.type && item.text) {
|
|
312
|
+
lines.push(`${item.type}: ${String(item.text).trim()}`)
|
|
313
|
+
}
|
|
314
|
+
} else if (event?.type === 'turn.completed' && event.usage) {
|
|
315
|
+
lines.push(`usage: ${JSON.stringify(event.usage)}`)
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
lines.push(trimmed)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return lines.join('\n')
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function codexAssistantTextFromJsonl(text) {
|
|
325
|
+
const messages = []
|
|
326
|
+
for (const line of String(text || '').split('\n')) {
|
|
327
|
+
const trimmed = line.trim()
|
|
328
|
+
if (!trimmed) continue
|
|
329
|
+
try {
|
|
330
|
+
const event = JSON.parse(trimmed)
|
|
331
|
+
const item = event?.type === 'item.completed' ? event.item || {} : {}
|
|
332
|
+
if (item.type === 'agent_message' && item.text) messages.push(String(item.text).trim())
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
return messages.join('\n\n')
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function failPersistedCurrentClaim(flags, deps, comment) {
|
|
339
|
+
const claim = await readCurrentClaim(deps)
|
|
340
|
+
if (!claim?.task_id || !claim?.runtime_id) return false
|
|
341
|
+
await deps.requestJson('/api/runtime/tasks/complete', {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
body: failedClaimCompletionBody(claim, comment),
|
|
344
|
+
config: configFromFlags(flags),
|
|
345
|
+
})
|
|
346
|
+
await clearCurrentClaim(deps)
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function installCurrentClaimExitHandlers(flags, deps) {
|
|
351
|
+
let shuttingDown = false
|
|
352
|
+
const failAndExit = async (signal, exitCode) => {
|
|
353
|
+
if (shuttingDown) return
|
|
354
|
+
shuttingDown = true
|
|
355
|
+
try {
|
|
356
|
+
await failPersistedCurrentClaim(
|
|
357
|
+
flags,
|
|
358
|
+
deps,
|
|
359
|
+
`Runtime task failed locally: daemon received ${signal} before the claimed task completed.`
|
|
360
|
+
)
|
|
361
|
+
} catch (error) {
|
|
362
|
+
deps.log(JSON.stringify({
|
|
363
|
+
warning: 'failed to mark current claimed task failed during daemon shutdown',
|
|
364
|
+
signal,
|
|
365
|
+
error: errorMessage(error),
|
|
366
|
+
}, null, 2))
|
|
367
|
+
} finally {
|
|
368
|
+
process.exit(exitCode)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const onSigterm = () => { void failAndExit('SIGTERM', 143) }
|
|
372
|
+
const onSigint = () => { void failAndExit('SIGINT', 130) }
|
|
373
|
+
process.once('SIGTERM', onSigterm)
|
|
374
|
+
process.once('SIGINT', onSigint)
|
|
375
|
+
return () => {
|
|
376
|
+
process.off('SIGTERM', onSigterm)
|
|
377
|
+
process.off('SIGINT', onSigint)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
266
381
|
function extractJsonObject(text) {
|
|
267
382
|
const source = String(text || '').trim()
|
|
268
383
|
if (!source) return null
|
|
@@ -863,11 +978,15 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
863
978
|
const codexBin = flag(flags, 'codex-bin', 'codex')
|
|
864
979
|
const workdir = flag(flags, 'codex-workdir', task.execution_context?.workdir || process.cwd())
|
|
865
980
|
const sandbox = flag(flags, 'codex-sandbox')
|
|
981
|
+
const runDir = task.execution_context?.run_dir
|
|
982
|
+
const lastMessagePath = runDir ? path.join(runDir, 'last_message.md') : ''
|
|
983
|
+
const jsonLogArgs = lastMessagePath ? ['--json', '--output-last-message', lastMessagePath] : ['--json']
|
|
866
984
|
const args = sandbox
|
|
867
985
|
? [
|
|
868
986
|
'--ask-for-approval',
|
|
869
987
|
'never',
|
|
870
988
|
'exec',
|
|
989
|
+
...jsonLogArgs,
|
|
871
990
|
'--skip-git-repo-check',
|
|
872
991
|
'--sandbox',
|
|
873
992
|
sandbox,
|
|
@@ -878,6 +997,7 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
878
997
|
: [
|
|
879
998
|
'--yolo',
|
|
880
999
|
'exec',
|
|
1000
|
+
...jsonLogArgs,
|
|
881
1001
|
'--skip-git-repo-check',
|
|
882
1002
|
'-C',
|
|
883
1003
|
workdir,
|
|
@@ -890,7 +1010,6 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
890
1010
|
if (profile) args.splice(execIndex + 1, 0, '--profile', profile)
|
|
891
1011
|
|
|
892
1012
|
const commandLine = [codexBin, ...args].map(value => JSON.stringify(String(value))).join(' ')
|
|
893
|
-
const runDir = task.execution_context?.run_dir
|
|
894
1013
|
await writeRunFile(runDir, 'prompt.md', `/goal ${prompt}`)
|
|
895
1014
|
await updateRunMeta(runDir, {
|
|
896
1015
|
provider: 'codex',
|
|
@@ -901,9 +1020,14 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
901
1020
|
})
|
|
902
1021
|
deps.log(JSON.stringify({ running: 'codex exec', command: commandLine, workdir }, null, 2))
|
|
903
1022
|
const result = await deps.runProcess(codexBin, args, { input: `/goal ${prompt}`, cwd: workdir, env: task.execution_context?.env || process.env })
|
|
904
|
-
const
|
|
1023
|
+
const rawStdout = String(result.stdout || '')
|
|
1024
|
+
const transcript = codexTranscriptFromJsonl(rawStdout)
|
|
1025
|
+
const assistantText = codexAssistantTextFromJsonl(rawStdout)
|
|
1026
|
+
const lastMessage = lastMessagePath ? String(await readFile(lastMessagePath, 'utf8').catch(() => '')).trim() : ''
|
|
1027
|
+
const output = (lastMessage || assistantText || rawStdout || transcript).trim()
|
|
905
1028
|
const error = String(result.stderr || '').trim()
|
|
906
|
-
await writeRunFile(runDir, 'stdout.log',
|
|
1029
|
+
await writeRunFile(runDir, 'stdout.log', rawStdout)
|
|
1030
|
+
await writeRunFile(runDir, 'transcript.log', transcript)
|
|
907
1031
|
await writeRunFile(runDir, 'stderr.log', String(result.stderr || ''))
|
|
908
1032
|
await updateRunMeta(runDir, { exit_code: result.code })
|
|
909
1033
|
if (result.code !== 0) {
|
|
@@ -1003,6 +1127,7 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
1003
1127
|
}
|
|
1004
1128
|
|
|
1005
1129
|
deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
|
|
1130
|
+
await writeCurrentClaim(deps, runtimeTask, machineKey)
|
|
1006
1131
|
let completion = null
|
|
1007
1132
|
let executionContext = null
|
|
1008
1133
|
if (runtimeTask.workspace?.slug) {
|
|
@@ -1112,6 +1237,7 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
1112
1237
|
config,
|
|
1113
1238
|
})
|
|
1114
1239
|
), deps, retryState)
|
|
1240
|
+
await clearCurrentClaim(deps)
|
|
1115
1241
|
deps.log(JSON.stringify(result, null, 2))
|
|
1116
1242
|
handled += 1
|
|
1117
1243
|
}
|
|
@@ -1143,33 +1269,57 @@ export async function startRuntimeDaemon(flags = {}, deps = {}) {
|
|
|
1143
1269
|
|
|
1144
1270
|
const handlerModule = await loadTaskHandler(handlerPath, resolvedDeps) || defaultTaskHandler(flags, resolvedDeps)
|
|
1145
1271
|
const retryState = createRetryState()
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
await
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1272
|
+
const uninstallExitHandlers = installCurrentClaimExitHandlers(flags, resolvedDeps)
|
|
1273
|
+
try {
|
|
1274
|
+
let registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
1275
|
+
await runWithDaemonRetry('recover current claimed task', () => failPersistedCurrentClaim(
|
|
1276
|
+
flags,
|
|
1277
|
+
resolvedDeps,
|
|
1278
|
+
'Runtime task failed locally: daemon restarted with a persisted claimed task that had not completed.'
|
|
1279
|
+
), resolvedDeps, retryState)
|
|
1280
|
+
await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
|
|
1281
|
+
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
|
|
1282
|
+
if (once) return
|
|
1283
|
+
|
|
1284
|
+
let lastScan = Date.now()
|
|
1285
|
+
let lastHeartbeat = Date.now()
|
|
1286
|
+
let lastTaskPoll = Date.now()
|
|
1287
|
+
let lastProjectRefresh = Date.now()
|
|
1288
|
+
while (true) {
|
|
1289
|
+
await resolvedDeps.sleep(Math.min(heartbeatIntervalMs, taskIntervalMs, projectRefreshIntervalMs))
|
|
1290
|
+
const now = Date.now()
|
|
1291
|
+
if (now - lastScan >= scanIntervalMs) {
|
|
1292
|
+
registration = await runWithDaemonRetry('register runtime', () => registerRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
1293
|
+
lastScan = now
|
|
1294
|
+
lastHeartbeat = now
|
|
1295
|
+
} else if (now - lastHeartbeat >= heartbeatIntervalMs) {
|
|
1296
|
+
await runWithDaemonRetry('heartbeat runtime', () => heartbeatRuntime(flags, resolvedDeps), resolvedDeps, retryState)
|
|
1297
|
+
lastHeartbeat = now
|
|
1298
|
+
}
|
|
1299
|
+
if (now - lastTaskPoll >= taskIntervalMs) {
|
|
1300
|
+
await claimAndRunRuntimeTasks(registration, flags, resolvedDeps, handlerModule, retryState, heartbeatIntervalMs)
|
|
1301
|
+
lastTaskPoll = now
|
|
1302
|
+
}
|
|
1303
|
+
if (now - lastProjectRefresh >= projectRefreshIntervalMs) {
|
|
1304
|
+
await syncRuntimeProjectMetadataBestEffort(flags, resolvedDeps)
|
|
1305
|
+
lastProjectRefresh = now
|
|
1306
|
+
}
|
|
1169
1307
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
try {
|
|
1310
|
+
await failPersistedCurrentClaim(
|
|
1311
|
+
flags,
|
|
1312
|
+
resolvedDeps,
|
|
1313
|
+
`Runtime task failed locally: daemon exited with error before the claimed task completed: ${errorMessage(error)}`
|
|
1314
|
+
)
|
|
1315
|
+
} catch (cleanupError) {
|
|
1316
|
+
resolvedDeps.log(JSON.stringify({
|
|
1317
|
+
warning: 'failed to mark current claimed task failed during daemon error exit',
|
|
1318
|
+
error: errorMessage(cleanupError),
|
|
1319
|
+
}, null, 2))
|
|
1173
1320
|
}
|
|
1321
|
+
throw error
|
|
1322
|
+
} finally {
|
|
1323
|
+
if (once) uninstallExitHandlers()
|
|
1174
1324
|
}
|
|
1175
1325
|
}
|