@turns/dev 0.0.15 → 0.0.17
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 +17 -0
- package/dist/agent-anchor.js +38 -3
- package/dist/background-task-state.js +471 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,23 @@ process to stay resident. A run narrowed to a directory — watching or `--once`
|
|
|
38
38
|
never prunes saved upload cursors, so it cannot make the sessions it filtered
|
|
39
39
|
out replay from their first byte the next time the daemon starts.
|
|
40
40
|
|
|
41
|
+
Native background tasks retain their detached agent anchor after the foreground
|
|
42
|
+
prompt finishes, including while the daemon client is disconnected. Codex child
|
|
43
|
+
agents, Claude/Letta tasks, ACP task tools, Prime/OMP jobs, and DeepSeek jobs
|
|
44
|
+
suspend the normal five-minute idle cleanup. Follow-up prompts remain available;
|
|
45
|
+
the anchor starts a fresh idle countdown after the last known task finishes or
|
|
46
|
+
its transport exits. Explicit shutdown, interruption, transport replacement, and
|
|
47
|
+
turn timeouts retain their existing semantics.
|
|
48
|
+
|
|
49
|
+
Retention uses native lifecycle events and task-tool results. Assistant prose,
|
|
50
|
+
a completed launch tool, or a failed status query does not prove that a task has
|
|
51
|
+
finished. Copilot, Hermes, and OpenCode captures lack reliable terminal evidence
|
|
52
|
+
in their live streams, so their anchors can stay resident until a later native
|
|
53
|
+
status result, transport exit, or explicit shutdown. No time-based fallback kills
|
|
54
|
+
quiet tasks. One-shot harnesses such as Muse and Antigravity already remain
|
|
55
|
+
foreground-running until their native process exits; there is no warm child
|
|
56
|
+
process for idle cleanup to kill afterward.
|
|
57
|
+
|
|
41
58
|
The server is selected with `--server URL` or `TURNS_ORIGIN`. Its normalized
|
|
42
59
|
origin is saved and reused by later invocations and daemon starts. On Linux,
|
|
43
60
|
Turns uses the XDG config/state directories and installs this user service:
|
package/dist/agent-anchor.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ACP_PROFILES, lettaHeadlessEnvironment, lettaSessionIdentity, PROCESS_H
|
|
|
12
12
|
import { asyncQuestionInteractions, asyncQuestionReplyText, isAsyncQuestionInteraction, parseAsyncQuestionReplies, } from "./codex-async-questions.js";
|
|
13
13
|
import { recordLettaSessionContext } from "./letta-session-context.js";
|
|
14
14
|
import { recoverAsyncQuestion } from "./codex-async-question-recovery.js";
|
|
15
|
+
import { BackgroundTaskState } from "./background-task-state.js";
|
|
15
16
|
const socketIndex = process.argv.indexOf('--socket');
|
|
16
17
|
const socketPath = socketIndex >= 0 ? path.resolve(process.argv[socketIndex + 1] || '') : '';
|
|
17
18
|
if (!socketPath)
|
|
@@ -98,6 +99,7 @@ function childResult(child, timeoutMs) {
|
|
|
98
99
|
return result.promise;
|
|
99
100
|
}
|
|
100
101
|
class LineTransport {
|
|
102
|
+
backgroundTasks;
|
|
101
103
|
anchor;
|
|
102
104
|
harness;
|
|
103
105
|
nextId;
|
|
@@ -107,6 +109,7 @@ class LineTransport {
|
|
|
107
109
|
constructor(anchor, harness, command, args, cwd, env = {}) {
|
|
108
110
|
this.anchor = anchor;
|
|
109
111
|
this.harness = harness;
|
|
112
|
+
this.backgroundTasks = new BackgroundTaskState(anchor.source);
|
|
110
113
|
this.nextId = 1;
|
|
111
114
|
this.pending = new Map();
|
|
112
115
|
this.lastEventAt = Date.now();
|
|
@@ -135,6 +138,9 @@ class LineTransport {
|
|
|
135
138
|
for (const waiter of this.pending.values())
|
|
136
139
|
waiter.reject(error);
|
|
137
140
|
this.pending.clear();
|
|
141
|
+
this.backgroundTasks.clear();
|
|
142
|
+
if (this.anchor.transport === this && this.anchor.status === 'idle')
|
|
143
|
+
this.anchor.scheduleExit();
|
|
138
144
|
this.onClose?.(error);
|
|
139
145
|
});
|
|
140
146
|
}
|
|
@@ -157,6 +163,11 @@ class LineTransport {
|
|
|
157
163
|
json = JSON.parse(line);
|
|
158
164
|
}
|
|
159
165
|
catch { }
|
|
166
|
+
const wasActive = this.backgroundTasks.active;
|
|
167
|
+
this.backgroundTasks.observe({ json }, this.anchor.agentSessionId);
|
|
168
|
+
if (wasActive !== this.backgroundTasks.active && this.anchor.transport === this && this.anchor.status === 'idle') {
|
|
169
|
+
this.anchor.scheduleExit();
|
|
170
|
+
}
|
|
160
171
|
this.anchor.emitLive({
|
|
161
172
|
direction: 'in',
|
|
162
173
|
source: `${this.harness}.stdout`,
|
|
@@ -1168,6 +1179,7 @@ class OmpRpcTransport extends RpcTransport {
|
|
|
1168
1179
|
}
|
|
1169
1180
|
}
|
|
1170
1181
|
class DeepseekTransport {
|
|
1182
|
+
backgroundTasks;
|
|
1171
1183
|
anchor;
|
|
1172
1184
|
sessionId;
|
|
1173
1185
|
waiter;
|
|
@@ -1175,6 +1187,7 @@ class DeepseekTransport {
|
|
|
1175
1187
|
base;
|
|
1176
1188
|
constructor(anchor) {
|
|
1177
1189
|
this.anchor = anchor;
|
|
1190
|
+
this.backgroundTasks = new BackgroundTaskState('deepseek');
|
|
1178
1191
|
this.sessionId = anchor.agentSessionId;
|
|
1179
1192
|
this.waiter = null;
|
|
1180
1193
|
this.socket = null;
|
|
@@ -1184,6 +1197,13 @@ class DeepseekTransport {
|
|
|
1184
1197
|
this.base = base;
|
|
1185
1198
|
this.socket = new WebSocket(`${base.replace(/^http/, 'ws')}/api/events.mux`);
|
|
1186
1199
|
this.socket.on('message', (bytes) => this.receive(String(bytes)));
|
|
1200
|
+
this.socket.once('close', () => {
|
|
1201
|
+
this.backgroundTasks.clear();
|
|
1202
|
+
this.waiter?.reject(new Error('DeepSeek transport closed.'));
|
|
1203
|
+
this.waiter = null;
|
|
1204
|
+
if (this.anchor.transport === this && this.anchor.status === 'idle')
|
|
1205
|
+
this.anchor.scheduleExit();
|
|
1206
|
+
});
|
|
1187
1207
|
await new Promise((resolve, reject) => {
|
|
1188
1208
|
this.socket.once('open', resolve);
|
|
1189
1209
|
this.socket.once('error', reject);
|
|
@@ -1207,6 +1227,11 @@ class DeepseekTransport {
|
|
|
1207
1227
|
}
|
|
1208
1228
|
if (String(frame?.payload?.sessionId || '') !== this.sessionId)
|
|
1209
1229
|
return;
|
|
1230
|
+
const wasActive = this.backgroundTasks.active;
|
|
1231
|
+
this.backgroundTasks.observe({ json: frame });
|
|
1232
|
+
if (wasActive !== this.backgroundTasks.active && this.anchor.transport === this && this.anchor.status === 'idle') {
|
|
1233
|
+
this.anchor.scheduleExit();
|
|
1234
|
+
}
|
|
1210
1235
|
this.anchor.emitLive({
|
|
1211
1236
|
direction: 'in',
|
|
1212
1237
|
source: 'deepseek.websocket',
|
|
@@ -1275,10 +1300,20 @@ class DeepseekTransport {
|
|
|
1275
1300
|
async prompt(text, timeoutMs) {
|
|
1276
1301
|
const waiter = deferred();
|
|
1277
1302
|
this.waiter = waiter;
|
|
1278
|
-
await this.rpc('session.prompt', { sessionId: this.sessionId, mode: 'queue', content: [{ type: 'text', text }] });
|
|
1279
1303
|
const timer = setTimeout(() => waiter.reject(new Error('DeepSeek turn timed out.')), timeoutMs);
|
|
1280
1304
|
timer.unref?.();
|
|
1281
|
-
|
|
1305
|
+
// Observe both promises immediately: the event socket can close before the
|
|
1306
|
+
// HTTP acknowledgement arrives, and must not produce an unhandled rejection.
|
|
1307
|
+
return Promise.all([
|
|
1308
|
+
this.rpc('session.prompt', { sessionId: this.sessionId, mode: 'queue', content: [{ type: 'text', text }] }),
|
|
1309
|
+
waiter.promise,
|
|
1310
|
+
])
|
|
1311
|
+
.then(([, result]) => result)
|
|
1312
|
+
.finally(() => {
|
|
1313
|
+
clearTimeout(timer);
|
|
1314
|
+
if (this.waiter === waiter)
|
|
1315
|
+
this.waiter = null;
|
|
1316
|
+
});
|
|
1282
1317
|
}
|
|
1283
1318
|
interrupt() {
|
|
1284
1319
|
return this.rpc('session.cancel', { sessionId: this.sessionId });
|
|
@@ -1558,7 +1593,7 @@ class Anchor {
|
|
|
1558
1593
|
}
|
|
1559
1594
|
scheduleExit() {
|
|
1560
1595
|
clearTimeout(this.idleTimer);
|
|
1561
|
-
if (this.pendingInteractions.size) {
|
|
1596
|
+
if (this.status === 'running' || this.pendingInteractions.size || this.transport?.backgroundTasks?.active) {
|
|
1562
1597
|
this.idleTimer = null;
|
|
1563
1598
|
return;
|
|
1564
1599
|
}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/** Native background-work leases owned by the detached anchor, not its client.
|
|
2
|
+
* This intentionally consumes protocol envelopes rather than searching stdout,
|
|
3
|
+
* user prompts, assistant prose, or command output for apparent task statuses. */
|
|
4
|
+
const RUNNING = new Set([
|
|
5
|
+
'accepted',
|
|
6
|
+
'active',
|
|
7
|
+
'created',
|
|
8
|
+
'dispatched',
|
|
9
|
+
'in_progress',
|
|
10
|
+
'inprogress',
|
|
11
|
+
'pending',
|
|
12
|
+
'queued',
|
|
13
|
+
'running',
|
|
14
|
+
'started',
|
|
15
|
+
'working',
|
|
16
|
+
]);
|
|
17
|
+
const TERMINAL = new Set([
|
|
18
|
+
'aborted',
|
|
19
|
+
'cancelled',
|
|
20
|
+
'canceled',
|
|
21
|
+
'completed',
|
|
22
|
+
'complete',
|
|
23
|
+
'done',
|
|
24
|
+
'error',
|
|
25
|
+
'errored',
|
|
26
|
+
'exited',
|
|
27
|
+
'failed',
|
|
28
|
+
'failure',
|
|
29
|
+
'finished',
|
|
30
|
+
'idle',
|
|
31
|
+
'killed',
|
|
32
|
+
'rejected',
|
|
33
|
+
'result_ready',
|
|
34
|
+
'stopped',
|
|
35
|
+
'succeeded',
|
|
36
|
+
'success',
|
|
37
|
+
'terminated',
|
|
38
|
+
]);
|
|
39
|
+
const STATUS_TOOLS = /^(?:taskoutput|task_output|tasklist|task_list|taskstop|task_stop|list_agents|wait_agent|read_agent|subagent_status|subagent_wait|subagent_read_result|read_subagent|list_background_processes|read_background_output|job_list|job_output|job_kill|hub|shell_status|shell_wait|shell_list|manage_subagents|get_command_or_subagent_output)$/u;
|
|
40
|
+
const ID_KEYS = [
|
|
41
|
+
'task_id',
|
|
42
|
+
'taskId',
|
|
43
|
+
'agent_id',
|
|
44
|
+
'agentId',
|
|
45
|
+
'subagent_id',
|
|
46
|
+
'subagentId',
|
|
47
|
+
'agent_path',
|
|
48
|
+
'agentPath',
|
|
49
|
+
'delegation_id',
|
|
50
|
+
'delegationId',
|
|
51
|
+
'job_id',
|
|
52
|
+
'jobId',
|
|
53
|
+
'shell_id',
|
|
54
|
+
'shellId',
|
|
55
|
+
'rlm_child_id',
|
|
56
|
+
'rlmChildId',
|
|
57
|
+
'backgroundTaskId',
|
|
58
|
+
];
|
|
59
|
+
const MAX_HISTORY = 512;
|
|
60
|
+
const object = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : null);
|
|
61
|
+
const string = (value) => (typeof value === 'string' ? value : '');
|
|
62
|
+
const normalized = (value) => string(value)
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/[^a-z0-9]+/gu, '_')
|
|
65
|
+
.replace(/^_|_$/gu, '');
|
|
66
|
+
function decode(value) {
|
|
67
|
+
if (typeof value !== 'string')
|
|
68
|
+
return value;
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(value);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function status(value) {
|
|
77
|
+
if (object(value))
|
|
78
|
+
value = Object.keys(value).find((key) => RUNNING.has(normalized(key)) || TERMINAL.has(normalized(key)));
|
|
79
|
+
const key = normalized(value);
|
|
80
|
+
if (RUNNING.has(key))
|
|
81
|
+
return 'running';
|
|
82
|
+
if (TERMINAL.has(key) || /^exited_\d+$/u.test(key))
|
|
83
|
+
return 'terminal';
|
|
84
|
+
}
|
|
85
|
+
function background(value) {
|
|
86
|
+
const input = object(decode(value));
|
|
87
|
+
return Boolean(input &&
|
|
88
|
+
(input.run_in_background === true ||
|
|
89
|
+
input.runInBackground === true ||
|
|
90
|
+
input.background === true ||
|
|
91
|
+
input.is_background === true ||
|
|
92
|
+
input.isBackground === true ||
|
|
93
|
+
input.async === true ||
|
|
94
|
+
input.mode === 'background'));
|
|
95
|
+
}
|
|
96
|
+
function nativeIds(value) {
|
|
97
|
+
const record = object(value);
|
|
98
|
+
if (!record)
|
|
99
|
+
return [];
|
|
100
|
+
return ID_KEYS.flatMap((key) => typeof record[key] === 'string' || typeof record[key] === 'number' ? [String(record[key])] : []).slice(0, 1);
|
|
101
|
+
}
|
|
102
|
+
function textIds(value) {
|
|
103
|
+
const ids = [];
|
|
104
|
+
for (const pattern of [
|
|
105
|
+
/(?:task|agent|subagent|delegation|job|shell)[ _-]?id\s*[:=]\s*["'`]?([A-Za-z0-9_./:-]+)/giu,
|
|
106
|
+
/(?:background with ID:|Backgrounded as job|started background job)\s*([A-Za-z0-9_./:-]+)/giu,
|
|
107
|
+
/(?:background\s*\(PID:\s*|\[PID\s+)(\d+)/giu,
|
|
108
|
+
/\bShell\s+([A-Za-z0-9_.:-]+)(?:,|\n)/gu,
|
|
109
|
+
/\brlm_child_id=['"]([^'"]+)['"]/gu,
|
|
110
|
+
/\/delegation\/live\/([^/\s"']+)/gu,
|
|
111
|
+
])
|
|
112
|
+
for (const match of value.matchAll(pattern))
|
|
113
|
+
ids.push(match[1].replace(/[.,;]+$/u, ''));
|
|
114
|
+
return [...new Set(ids)];
|
|
115
|
+
}
|
|
116
|
+
function contents(value, depth = 0) {
|
|
117
|
+
if (depth > 10)
|
|
118
|
+
return '';
|
|
119
|
+
if (typeof value === 'string')
|
|
120
|
+
return value;
|
|
121
|
+
if (Array.isArray(value))
|
|
122
|
+
return value.map((child) => contents(child, depth + 1)).join('\n');
|
|
123
|
+
return object(value)
|
|
124
|
+
? contents(value.text ?? value.content ?? value.output ?? value.observation ?? '', depth + 1)
|
|
125
|
+
: '';
|
|
126
|
+
}
|
|
127
|
+
function textStatus(value) {
|
|
128
|
+
// Only provider status headers, not words inside a shell command or its output.
|
|
129
|
+
const reported = value.match(/(?:^|\n|,\s*)(?:status|state|final_sub_status)\s*[:=]\s*["']?([\w -]+)/iu)?.[1];
|
|
130
|
+
if (reported)
|
|
131
|
+
return status(reported.split(/[,\n]/u)[0].trim());
|
|
132
|
+
if (/^(?:Subagent completed:|Agent (?:completed|finished)|<task-notification>)/u.test(value))
|
|
133
|
+
return 'terminal';
|
|
134
|
+
if (/\b(?:Command moved to background|Command running in background with ID:|Agent started in background|Backgrounded as job|started background job)\b/u.test(value))
|
|
135
|
+
return 'running';
|
|
136
|
+
}
|
|
137
|
+
export class BackgroundTaskState {
|
|
138
|
+
activeIds = new Set();
|
|
139
|
+
calls = new Map();
|
|
140
|
+
aliases = new Map();
|
|
141
|
+
terminalIds = new Set();
|
|
142
|
+
children = new Set();
|
|
143
|
+
harness;
|
|
144
|
+
sessionId;
|
|
145
|
+
constructor(harness, sessionId = '') {
|
|
146
|
+
this.harness = harness;
|
|
147
|
+
this.sessionId = sessionId;
|
|
148
|
+
}
|
|
149
|
+
get active() {
|
|
150
|
+
return this.activeIds.size > 0;
|
|
151
|
+
}
|
|
152
|
+
clear() {
|
|
153
|
+
this.activeIds.clear();
|
|
154
|
+
this.calls.clear();
|
|
155
|
+
this.aliases.clear();
|
|
156
|
+
this.terminalIds.clear();
|
|
157
|
+
this.children.clear();
|
|
158
|
+
}
|
|
159
|
+
update(id, state, restart = false) {
|
|
160
|
+
if (typeof id !== 'string' || !id || id.length > 512)
|
|
161
|
+
return;
|
|
162
|
+
const key = this.aliases.get(id) || id;
|
|
163
|
+
const next = status(state);
|
|
164
|
+
if (next === 'terminal') {
|
|
165
|
+
this.activeIds.delete(key);
|
|
166
|
+
this.terminalIds.add(key);
|
|
167
|
+
while (this.terminalIds.size > MAX_HISTORY) {
|
|
168
|
+
const oldest = this.terminalIds.values().next().value;
|
|
169
|
+
this.terminalIds.delete(oldest);
|
|
170
|
+
this.children.delete(oldest);
|
|
171
|
+
for (const [alias, target] of this.aliases)
|
|
172
|
+
if (target === oldest)
|
|
173
|
+
this.aliases.delete(alias);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (next === 'running' && (restart || !this.terminalIds.has(key))) {
|
|
177
|
+
if (restart)
|
|
178
|
+
this.terminalIds.delete(key);
|
|
179
|
+
this.activeIds.add(key);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
bind(callId, ids) {
|
|
183
|
+
if (!ids.length)
|
|
184
|
+
return;
|
|
185
|
+
this.activeIds.delete(`call:${callId}`);
|
|
186
|
+
for (const id of ids)
|
|
187
|
+
this.update(id, 'running');
|
|
188
|
+
}
|
|
189
|
+
tool(id, name, input, output, failed = false) {
|
|
190
|
+
if (typeof id !== 'string' || !id)
|
|
191
|
+
return;
|
|
192
|
+
const args = object(decode(input));
|
|
193
|
+
const previous = this.calls.get(id);
|
|
194
|
+
const toolName = normalized(name).slice(0, 512) || previous?.name || '';
|
|
195
|
+
const launch = previous?.launch ||
|
|
196
|
+
background(args) ||
|
|
197
|
+
/^(?:spawn_agent|spawn_subagent|subagent_spawn|invoke_subagent)$/u.test(toolName);
|
|
198
|
+
const targets = args
|
|
199
|
+
? [
|
|
200
|
+
...nativeIds(args),
|
|
201
|
+
...(Array.isArray(args.task_ids) ? args.task_ids : []),
|
|
202
|
+
...(Array.isArray(args.ids) ? args.ids : []),
|
|
203
|
+
...(this.harness === 'poolside' && args.name ? [args.name] : []),
|
|
204
|
+
].filter((value) => typeof value === 'string')
|
|
205
|
+
: previous?.targets || [];
|
|
206
|
+
const delegation = previous?.delegation || (this.harness === 'hermes' && /\/delegation\/live\//u.test(string(name)));
|
|
207
|
+
if (delegation)
|
|
208
|
+
targets.push(...textIds(string(name)));
|
|
209
|
+
const call = { name: toolName, launch, targets, delegation };
|
|
210
|
+
this.calls.delete(id);
|
|
211
|
+
this.calls.set(id, call);
|
|
212
|
+
if (this.calls.size > MAX_HISTORY) {
|
|
213
|
+
for (const key of this.calls.keys()) {
|
|
214
|
+
// Keep pending launch correlation even during a burst of tool traffic.
|
|
215
|
+
if (key !== id && !this.activeIds.has(`call:${key}`))
|
|
216
|
+
this.calls.delete(key);
|
|
217
|
+
if (this.calls.size <= MAX_HISTORY)
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Provider-native lifecycle supplies Devin and Prime identities directly.
|
|
222
|
+
const nativeLaunch = (this.harness === 'devin' && toolName === 'run_subagent') || (this.harness === 'prime' && toolName === 'ipython');
|
|
223
|
+
if (launch && !nativeLaunch && !previous?.launch)
|
|
224
|
+
this.update(`call:${id}`, 'running');
|
|
225
|
+
if (failed && launch)
|
|
226
|
+
this.update(`call:${id}`, 'failed');
|
|
227
|
+
if (output === undefined || failed)
|
|
228
|
+
return;
|
|
229
|
+
const text = contents(output);
|
|
230
|
+
const outputLaunch = /^(?:bash|shell|run_shell_command)$/u.test(toolName) &&
|
|
231
|
+
/^(?:Command moved to background|Command running in background with ID:|Backgrounded as job|started background job)/u.test(text);
|
|
232
|
+
const hermes = this.harness === 'hermes' && (/^delegate/u.test(toolName) || delegation);
|
|
233
|
+
if (!(launch ||
|
|
234
|
+
outputLaunch ||
|
|
235
|
+
STATUS_TOOLS.test(toolName) ||
|
|
236
|
+
hermes ||
|
|
237
|
+
(this.harness === 'muse' && toolName.startsWith('subagent'))))
|
|
238
|
+
return;
|
|
239
|
+
const found = new Set();
|
|
240
|
+
const visit = (value, depth = 0) => {
|
|
241
|
+
if (depth > 10)
|
|
242
|
+
return;
|
|
243
|
+
let parsed = decode(value);
|
|
244
|
+
if (Array.isArray(parsed)) {
|
|
245
|
+
for (const child of parsed)
|
|
246
|
+
visit(child, depth + 1);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (object(parsed)) {
|
|
250
|
+
const ids = nativeIds(parsed);
|
|
251
|
+
// Native list rows use an unqualified id, unlike enclosing tool calls.
|
|
252
|
+
if (typeof parsed.id === 'string' && (parsed.status || parsed.state))
|
|
253
|
+
ids.push(parsed.id);
|
|
254
|
+
if (this.harness === 'antigravity' && typeof parsed.conversationId === 'string')
|
|
255
|
+
ids.push(parsed.conversationId);
|
|
256
|
+
for (const task of ids) {
|
|
257
|
+
found.add(task);
|
|
258
|
+
this.update(task, this.harness === 'muse' ? parsed.roster?.[0]?.state || parsed.status : (parsed.status ?? parsed.state));
|
|
259
|
+
}
|
|
260
|
+
if (!ids.length && !parsed.jobs && !parsed.agents && STATUS_TOOLS.test(toolName)) {
|
|
261
|
+
for (const task of targets)
|
|
262
|
+
this.update(task, parsed.status ?? parsed.state);
|
|
263
|
+
}
|
|
264
|
+
for (const [key, child] of Object.entries(parsed)) {
|
|
265
|
+
if (/prompt$|description$/iu.test(key) ||
|
|
266
|
+
['command', 'label', 'detailedContent', 'stdout', 'stderr'].includes(key))
|
|
267
|
+
continue;
|
|
268
|
+
// A task row's output is arbitrary program/agent text, not another status record.
|
|
269
|
+
if (status(parsed.status ?? parsed.state) && ['output', 'message', 'result'].includes(key))
|
|
270
|
+
continue;
|
|
271
|
+
visit(child, depth + 1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (typeof parsed === 'string') {
|
|
275
|
+
parsed = parsed
|
|
276
|
+
.replace(/<output>[\s\S]*?(?:<\/output>|$)/gu, '')
|
|
277
|
+
.split(/\n(?:output|stdout|stderr):\s*\n/iu)[0];
|
|
278
|
+
const xmlId = /<task_id>([^<]+)<\/task_id>/u.exec(parsed)?.[1];
|
|
279
|
+
const xmlStatus = /<status>([^<]+)<\/status>/u.exec(parsed)?.[1];
|
|
280
|
+
if (xmlId && xmlStatus) {
|
|
281
|
+
found.add(xmlId);
|
|
282
|
+
this.update(xmlId, xmlStatus);
|
|
283
|
+
}
|
|
284
|
+
for (const match of parsed.matchAll(/<task\s+([^>]+)>/gu)) {
|
|
285
|
+
const task = /\bid=["']([^"']+)["']/u.exec(match[1])?.[1];
|
|
286
|
+
const state = /\bstate=["']([^"']+)["']/u.exec(match[1])?.[1];
|
|
287
|
+
if (task) {
|
|
288
|
+
found.add(task);
|
|
289
|
+
this.update(task, state);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (this.harness === 'antigravity' && toolName === 'manage_subagents') {
|
|
293
|
+
const list = parsed.slice(parsed.indexOf('['));
|
|
294
|
+
if (Array.isArray(decode(list)))
|
|
295
|
+
visit(decode(list), depth + 1);
|
|
296
|
+
}
|
|
297
|
+
const ids = textIds(parsed);
|
|
298
|
+
for (const task of ids)
|
|
299
|
+
found.add(task);
|
|
300
|
+
// PID and job listings expose one status per line, not a global status.
|
|
301
|
+
for (const line of parsed.split('\n')) {
|
|
302
|
+
const pid = /\[PID\s+(\d+)\]\s+(RUNNING|EXITED)/u.exec(line);
|
|
303
|
+
const job = /^([\w.-]+)\s+\[[^\]]+\]\s+(\w+)/u.exec(line);
|
|
304
|
+
if (pid)
|
|
305
|
+
this.update(pid[1], pid[2]);
|
|
306
|
+
if (job)
|
|
307
|
+
this.update(job[1], job[2]);
|
|
308
|
+
}
|
|
309
|
+
const state = textStatus(parsed);
|
|
310
|
+
const addressed = ids.length ? ids : targets;
|
|
311
|
+
if (state)
|
|
312
|
+
for (const task of addressed)
|
|
313
|
+
this.update(task, state === 'terminal' ? 'completed' : 'running');
|
|
314
|
+
if (hermes && /\bresult\s+\|\s+terminal ok\b/u.test(parsed)) {
|
|
315
|
+
for (const task of [...ids, ...targets])
|
|
316
|
+
this.update(task, 'completed');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
visit(output);
|
|
321
|
+
if (launch || outputLaunch) {
|
|
322
|
+
this.bind(id, [...found]);
|
|
323
|
+
// A failed native launch may complete the tool with an error payload.
|
|
324
|
+
if (!found.size && object(output)?.isError === true)
|
|
325
|
+
this.update(`call:${id}`, 'failed');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
observe(event, sessionId = this.sessionId) {
|
|
329
|
+
if (event?.direction === 'out' || String(event?.source || '').endsWith('.stderr'))
|
|
330
|
+
return;
|
|
331
|
+
const frame = object(event?.json) || object(decode(event?.raw));
|
|
332
|
+
if (!frame)
|
|
333
|
+
return;
|
|
334
|
+
if (this.harness === 'codex') {
|
|
335
|
+
const params = frame.params || {};
|
|
336
|
+
const thread = params.threadId;
|
|
337
|
+
if (thread && thread !== sessionId && this.children.has(thread)) {
|
|
338
|
+
if (frame.method === 'turn/started')
|
|
339
|
+
this.update(thread, 'running', true);
|
|
340
|
+
if (['turn/completed', 'turn/failed', 'turn/aborted'].includes(frame.method))
|
|
341
|
+
this.update(thread, 'completed');
|
|
342
|
+
if (frame.method === 'thread/status/changed')
|
|
343
|
+
this.update(thread, params.status?.type);
|
|
344
|
+
}
|
|
345
|
+
if (thread && sessionId && thread !== sessionId && !this.children.has(thread))
|
|
346
|
+
return;
|
|
347
|
+
const item = params.item;
|
|
348
|
+
if (!['item/started', 'item/completed'].includes(frame.method) || !object(item))
|
|
349
|
+
return;
|
|
350
|
+
if (item.type === 'subAgentActivity' || item.type === 'sub_agent_activity') {
|
|
351
|
+
const id = item.agentThreadId || item.agent_thread_id || item.agentPath || item.agent_path;
|
|
352
|
+
if (typeof id === 'string') {
|
|
353
|
+
this.children.add(id);
|
|
354
|
+
const alias = item.agentPath || item.agent_path;
|
|
355
|
+
if (alias)
|
|
356
|
+
this.aliases.set(alias, id);
|
|
357
|
+
this.update(id, item.kind, frame.method === 'item/started' && item.kind === 'started');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (item.type === 'collabAgentToolCall') {
|
|
361
|
+
for (const id of Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : []) {
|
|
362
|
+
this.children.add(id);
|
|
363
|
+
if (['spawnAgent', 'spawn_agent', 'resumeAgent', 'resume_agent', 'sendInput', 'send_input'].includes(item.tool) &&
|
|
364
|
+
item.status !== 'failed')
|
|
365
|
+
this.update(id, 'running', frame.method === 'item/started');
|
|
366
|
+
if (['closeAgent', 'close_agent'].includes(item.tool) &&
|
|
367
|
+
frame.method === 'item/completed' &&
|
|
368
|
+
item.status === 'completed')
|
|
369
|
+
this.update(id, 'completed');
|
|
370
|
+
}
|
|
371
|
+
for (const [id, state] of Object.entries(item.agentsStates || item.agentsStatus || {})) {
|
|
372
|
+
this.children.add(id);
|
|
373
|
+
this.update(id, object(state)?.status ?? state);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (frame.type === 'system' && ['claude', 'letta'].includes(this.harness)) {
|
|
379
|
+
if (frame.subtype === 'task_started' && frame.is_backgrounded !== false)
|
|
380
|
+
this.update(frame.task_id, 'running', true);
|
|
381
|
+
if (frame.subtype === 'task_notification')
|
|
382
|
+
this.update(frame.task_id, 'completed');
|
|
383
|
+
}
|
|
384
|
+
const update = frame.params?.update;
|
|
385
|
+
if (object(update) && ['session/update', '_x.ai/session_notification'].includes(frame.method)) {
|
|
386
|
+
if (sessionId && frame.params.sessionId && frame.params.sessionId !== sessionId)
|
|
387
|
+
return;
|
|
388
|
+
const meta = update._meta || {};
|
|
389
|
+
const started = meta['cognition.ai/subagent_started'];
|
|
390
|
+
const ended = meta['cognition.ai/subagent_completed'];
|
|
391
|
+
if (started?.isBackground)
|
|
392
|
+
this.update(started.agentId, 'running', true);
|
|
393
|
+
if (ended)
|
|
394
|
+
this.update(ended.agentId, 'completed');
|
|
395
|
+
if (['tool_call', 'tool_call_update'].includes(update.sessionUpdate)) {
|
|
396
|
+
const known = this.calls.get(update.toolCallId);
|
|
397
|
+
const toolName = meta.toolName ||
|
|
398
|
+
meta.tool_name ||
|
|
399
|
+
meta['codebuddy.ai/toolName'] ||
|
|
400
|
+
meta['cognition.ai/inferenceToolName'] ||
|
|
401
|
+
meta['x.ai/tool']?.name ||
|
|
402
|
+
known?.name ||
|
|
403
|
+
update.title ||
|
|
404
|
+
string(update.toolCallId).split('__call_')[0];
|
|
405
|
+
const out = update.status !== 'pending' &&
|
|
406
|
+
(update.status === 'completed' ||
|
|
407
|
+
update.rawOutput !== undefined ||
|
|
408
|
+
(this.harness === 'hermes' && known?.delegation))
|
|
409
|
+
? this.harness === 'qwen'
|
|
410
|
+
? // Qwen's rawOutput is often only 'Listed N background agents';
|
|
411
|
+
// the native IDs and per-agent statuses live in content.
|
|
412
|
+
[update.rawOutput, update.content]
|
|
413
|
+
: (update.rawOutput ?? update.content)
|
|
414
|
+
: undefined;
|
|
415
|
+
this.tool(update.toolCallId, toolName, update.rawInput, out, update.status === 'failed');
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (this.harness === 'gemini' && frame.method === 'session/request_permission' && frame.params?.toolCall) {
|
|
420
|
+
const call = frame.params.toolCall;
|
|
421
|
+
this.tool(call.toolCallId, string(call.toolCallId).split('__call_')[0], call.rawInput);
|
|
422
|
+
}
|
|
423
|
+
if (['claude', 'letta'].includes(this.harness)) {
|
|
424
|
+
const message = frame.message || frame;
|
|
425
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
426
|
+
if (block.type === 'tool_use')
|
|
427
|
+
this.tool(block.id, block.name, block.input);
|
|
428
|
+
if (block.type === 'tool_result')
|
|
429
|
+
this.tool(block.tool_use_id, '', undefined, block.content, block.is_error === true);
|
|
430
|
+
}
|
|
431
|
+
if (frame.message_type === 'tool_call_message')
|
|
432
|
+
this.tool(frame.tool_call?.tool_call_id, frame.tool_call?.name, frame.tool_call?.arguments);
|
|
433
|
+
if (frame.message_type === 'tool_return_message')
|
|
434
|
+
this.tool(frame.tool_call_id, '', undefined, frame.tool_return, frame.status === 'error');
|
|
435
|
+
}
|
|
436
|
+
if (['omp', 'prime', 'pi'].includes(this.harness)) {
|
|
437
|
+
if (frame.type === 'tool_execution_start')
|
|
438
|
+
this.tool(frame.toolCallId, frame.toolName, frame.args);
|
|
439
|
+
if (frame.type === 'tool_execution_end')
|
|
440
|
+
this.tool(frame.toolCallId, frame.toolName, undefined, frame.result, frame.isError);
|
|
441
|
+
if (frame.type === 'rlm_child_update' && this.harness === 'prime')
|
|
442
|
+
this.update(frame.child?.id, frame.child?.status);
|
|
443
|
+
}
|
|
444
|
+
if (this.harness === 'deepseek' && frame.method === 'session/event') {
|
|
445
|
+
const event = frame.payload?.event;
|
|
446
|
+
const data = event?.data || {};
|
|
447
|
+
if (event?.type === 'tool/call')
|
|
448
|
+
this.tool(data.callId, data.name, data.arguments);
|
|
449
|
+
if (event?.type === 'tool/result')
|
|
450
|
+
for (const part of Array.isArray(data.message?.content) ? data.message.content : []) {
|
|
451
|
+
if (part.type === 'tool-result')
|
|
452
|
+
this.tool(part.toolCallId, '', undefined, part.content, part.isError);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (this.harness === 'antigravity' && frame.event === 'step_update') {
|
|
456
|
+
const step = frame.step_update;
|
|
457
|
+
for (const child of Array.isArray(step?.subagent_info?.subagents) ? step.subagent_info.subagents : [])
|
|
458
|
+
this.update(child.conversation_id, 'running');
|
|
459
|
+
const tool = step?.tool_info;
|
|
460
|
+
if (tool)
|
|
461
|
+
this.tool(String(step.step_index), tool.name, tool.parameters, tool.output);
|
|
462
|
+
}
|
|
463
|
+
if (this.harness === 'muse' && frame.payload?.kind === 'task_lifecycle') {
|
|
464
|
+
const event = frame.payload.event;
|
|
465
|
+
if (event?.kind === 'proposed')
|
|
466
|
+
this.tool(event.task_id, string(event.task_kind).replace(/^tool\./u, ''), undefined);
|
|
467
|
+
if (event?.kind === 'output')
|
|
468
|
+
this.tool(event.task_id, '', undefined, event.chunk);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|