@yeaft/webchat-agent 0.1.973 → 0.1.974
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/package.json +1 -1
- package/yeaft/sub-agent/liveness.js +34 -0
- package/yeaft/sub-agent/notifications.js +16 -15
- package/yeaft/sub-agent/runner.js +56 -2
- package/yeaft/tools/agent.js +19 -16
- package/yeaft/tools/list-agents.js +14 -5
- package/yeaft/tools/wait-agent.js +27 -14
package/package.json
CHANGED
|
@@ -99,3 +99,37 @@ export function snapshotLiveness(liveness, now = Date.now()) {
|
|
|
99
99
|
recentTools: liveness.recentTools.slice(),
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
|
|
103
|
+
export const DEFAULT_STALL_THRESHOLD_MS = 120000;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Add a stable "is this likely stuck?" diagnostic to a liveness snapshot.
|
|
107
|
+
* If no event has ever arrived, fall back to createdAt / usage.startedAt so
|
|
108
|
+
* a silent running child can still become stale.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} agent
|
|
111
|
+
* @param {{ now?: number, thresholdMs?: number }} [opts]
|
|
112
|
+
*/
|
|
113
|
+
export function diagnoseAgentLiveness(agent, opts = {}) {
|
|
114
|
+
const now = typeof opts.now === 'number' ? opts.now : Date.now();
|
|
115
|
+
const thresholdMs = typeof opts.thresholdMs === 'number' && opts.thresholdMs > 0
|
|
116
|
+
? opts.thresholdMs
|
|
117
|
+
: DEFAULT_STALL_THRESHOLD_MS;
|
|
118
|
+
const liveness = snapshotLiveness(agent?.liveness, now);
|
|
119
|
+
const fallbackAt = agent?.createdAt || agent?.usage?.startedAt || null;
|
|
120
|
+
const activityAt = liveness.lastEventAt || fallbackAt;
|
|
121
|
+
const msSinceActivity = activityAt ? Math.max(0, now - activityAt) : null;
|
|
122
|
+
const stale = agent?.status === 'running'
|
|
123
|
+
&& msSinceActivity !== null
|
|
124
|
+
&& msSinceActivity >= thresholdMs;
|
|
125
|
+
return {
|
|
126
|
+
...liveness,
|
|
127
|
+
msSinceLastEvent: liveness.msSinceLastEvent ?? msSinceActivity,
|
|
128
|
+
stale,
|
|
129
|
+
stalled: stale,
|
|
130
|
+
stallThresholdMs: thresholdMs,
|
|
131
|
+
diagnostic: stale
|
|
132
|
+
? `No sub-agent activity for ${msSinceActivity}ms; treat it as stalled instead of waiting in a loop.`
|
|
133
|
+
: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* Problem this solves:
|
|
5
5
|
* The original sub-agent protocol was purely pull-based — the parent had
|
|
6
6
|
* to keep calling WaitAgent to discover that its child had finished. If
|
|
7
|
-
* the parent forgot, the child's terminal state was never
|
|
8
|
-
* the orchestration "hung" from the user's perspective.
|
|
9
|
-
* claude-code's `<task-notification>` XML re-entry pattern:
|
|
10
|
-
* child reaches a terminal state we *push* a
|
|
11
|
-
* that the parent will see the next time it
|
|
7
|
+
* the parent forgot, the child's progress or terminal state was never
|
|
8
|
+
* surfaced and the orchestration "hung" from the user's perspective.
|
|
9
|
+
* Modeled on claude-code's `<task-notification>` XML re-entry pattern:
|
|
10
|
+
* when a child finishes a turn or reaches a terminal state we *push* a
|
|
11
|
+
* notification onto a queue that the parent will see the next time it
|
|
12
|
+
* talks to its engine.
|
|
12
13
|
*
|
|
13
14
|
* This module is the queue. Two entry points consume it:
|
|
14
15
|
*
|
|
@@ -16,11 +17,11 @@
|
|
|
16
17
|
* before returning).
|
|
17
18
|
* 2. Engine.query() — when started with a user prompt, it asks
|
|
18
19
|
* `consumePendingNotifications({ sessionId, parentVpId })` for any queued
|
|
19
|
-
* terminal events from sub-agents that the parent hasn't yet
|
|
20
|
+
* idle/terminal events from sub-agents that the parent hasn't yet
|
|
20
21
|
* acknowledged, and prepends a short XML block to the user
|
|
21
22
|
* message. The XML block is human-readable for the model and
|
|
22
|
-
* explicitly tells it "your sub-agent X
|
|
23
|
-
* away; here's the result and what to do next".
|
|
23
|
+
* explicitly tells it "your sub-agent X produced progress while you
|
|
24
|
+
* were away; here's the result and what to do next".
|
|
24
25
|
*
|
|
25
26
|
* The queue is in-memory only. We do NOT persist across process
|
|
26
27
|
* restarts because (a) sub-agents themselves don't survive restart,
|
|
@@ -75,9 +76,9 @@ function bucketKey(scope, sessionId) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
|
-
* Enqueue a terminal notification for an agent. Idempotent per
|
|
79
|
-
* a second call with the same agentId is a no-op
|
|
80
|
-
*
|
|
79
|
+
* Enqueue a progress/terminal notification for an agent. Idempotent per
|
|
80
|
+
* agent while queued — a second call with the same agentId is a no-op
|
|
81
|
+
* until the previous notice is replaced or consumed.
|
|
81
82
|
*
|
|
82
83
|
* @param {{ agentId: string, agentName: string, status: string, result?: string, error?: string|null, outputFile?: string|null, turns?: number, parentVpId?: string|null, parentSessionId?: string|null, sessionId?: string|null, budgetExceeded?: boolean, budgetReason?: string|null, budgetUsage?: object|null }} input
|
|
83
84
|
* @returns {SubAgentNotification|null} the queued record (null if a dup)
|
|
@@ -210,10 +211,10 @@ export function formatNotificationsForPrompt(notifs) {
|
|
|
210
211
|
const parts = [];
|
|
211
212
|
parts.push('<sub-agent-notifications>');
|
|
212
213
|
parts.push(
|
|
213
|
-
'The following sub-agent(s) reached a terminal
|
|
214
|
-
'away. The user has NOT seen any of this — only
|
|
215
|
-
'either (a) relay the result(s) to the user in your
|
|
216
|
-
'on the result(s) before replying. Do NOT ignore these.',
|
|
214
|
+
'The following sub-agent(s) produced progress or reached a terminal ' +
|
|
215
|
+
'state while you were away. The user has NOT seen any of this — only ' +
|
|
216
|
+
'you have. You MUST either (a) relay the result(s) to the user in your ' +
|
|
217
|
+
'reply, or (b) act on the result(s) before replying. Do NOT ignore these.',
|
|
217
218
|
);
|
|
218
219
|
for (const n of notifs) {
|
|
219
220
|
parts.push('');
|
|
@@ -41,7 +41,7 @@ import { buildSpawnedPreamble } from './spawned-prompt.js';
|
|
|
41
41
|
import { STATUS, isTerminalAgentStatus } from './status.js';
|
|
42
42
|
import { createOutputLog } from './output-log.js';
|
|
43
43
|
import { makeLiveness, bumpLivenessFromEvent } from './liveness.js';
|
|
44
|
-
import { enqueueTerminalNotification } from './notifications.js';
|
|
44
|
+
import { consumeNotificationForAgent, enqueueTerminalNotification } from './notifications.js';
|
|
45
45
|
// NOTE: tickAgent lives in `../tools/agent.js`, which itself imports this
|
|
46
46
|
// module (startSubAgent). To avoid the ES-module circular-import gotcha
|
|
47
47
|
// where one side sees an undefined export at module-init time, we import
|
|
@@ -223,8 +223,43 @@ export function startSubAgent(agent, deps = {}) {
|
|
|
223
223
|
* CloseAgent (status=='closed') OR the idle watchdog firing
|
|
224
224
|
* (status=='abandoned').
|
|
225
225
|
*/
|
|
226
|
+
function buildWallTimeBudgetResult(agent, reason) {
|
|
227
|
+
return {
|
|
228
|
+
status: 'budget_exceeded',
|
|
229
|
+
partial_output: agent.partial_output || agent.lastResult || agent.result || '',
|
|
230
|
+
reason,
|
|
231
|
+
usage: { ...(agent.usage || {}) },
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function armWallTimeWatchdog(agent, deps) {
|
|
236
|
+
const wallTimeMs = agent?.budget?.wall_time_ms;
|
|
237
|
+
if (typeof wallTimeMs !== 'number' || !Number.isFinite(wallTimeMs) || wallTimeMs <= 0) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const startedAt = agent.usage?.startedAt || Date.now();
|
|
241
|
+
const remainingMs = Math.max(0, startedAt + wallTimeMs - Date.now());
|
|
242
|
+
const timer = setTimeout(() => {
|
|
243
|
+
if (isTerminalAgentStatus(agent.status)) return;
|
|
244
|
+
const reason = `wall_time_ms (${wallTimeMs}) exceeded`;
|
|
245
|
+
agent.result = buildWallTimeBudgetResult(agent, reason);
|
|
246
|
+
agent.partial_output = agent.result.partial_output || '';
|
|
247
|
+
if (agent.abortController && !agent.abortController.signal.aborted) {
|
|
248
|
+
try { agent.abortController.abort(reason); } catch { /* ignore */ }
|
|
249
|
+
}
|
|
250
|
+
transitionTerminal(agent, STATUS.COMPLETED, {
|
|
251
|
+
error: reason,
|
|
252
|
+
diagnostic: 'wall_time_watchdog',
|
|
253
|
+
deps,
|
|
254
|
+
});
|
|
255
|
+
}, remainingMs);
|
|
256
|
+
timer.unref?.();
|
|
257
|
+
return timer;
|
|
258
|
+
}
|
|
259
|
+
|
|
226
260
|
async function driveSubAgent(agent, subEngine, vpPersona, deps) {
|
|
227
261
|
const onEvent = typeof deps.onEvent === 'function' ? deps.onEvent : null;
|
|
262
|
+
const wallTimeWatchdog = armWallTimeWatchdog(agent, deps);
|
|
228
263
|
const idleAbandonMs = typeof deps.idleAbandonMs === 'number' && deps.idleAbandonMs > 0
|
|
229
264
|
? deps.idleAbandonMs : IDLE_ABANDON_MS;
|
|
230
265
|
|
|
@@ -262,6 +297,21 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
|
|
|
262
297
|
agent.status = STATUS.IDLE;
|
|
263
298
|
agent.idleSince = Date.now();
|
|
264
299
|
emit({ type: 'sub_agent_status', status: STATUS.IDLE });
|
|
300
|
+
if (agent.result || agent.lastResult) {
|
|
301
|
+
try {
|
|
302
|
+
enqueueTerminalNotification({
|
|
303
|
+
agentId: agent.id,
|
|
304
|
+
agentName: agent.name,
|
|
305
|
+
status: STATUS.IDLE,
|
|
306
|
+
result: typeof agent.result === 'string' ? agent.result : (agent.lastResult || ''),
|
|
307
|
+
error: null,
|
|
308
|
+
outputFile: agent.outputFile || null,
|
|
309
|
+
turns: agent.usage?.turns || 0,
|
|
310
|
+
parentVpId: agent.parentVpId || deps.parentVpId || null,
|
|
311
|
+
parentSessionId: agent.parentSessionId || deps.parentSessionId || null,
|
|
312
|
+
});
|
|
313
|
+
} catch { /* best-effort notification */ }
|
|
314
|
+
}
|
|
265
315
|
|
|
266
316
|
const reason = await waitUntilResumed(agent, idleAbandonMs);
|
|
267
317
|
if (reason === 'abandoned') {
|
|
@@ -398,6 +448,7 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
|
|
|
398
448
|
emit({ type: 'sub_agent_turn_end', content: assistantText });
|
|
399
449
|
}
|
|
400
450
|
} finally {
|
|
451
|
+
if (wallTimeWatchdog) clearTimeout(wallTimeWatchdog);
|
|
401
452
|
// Always clean up driver-owned resources. We intentionally do NOT
|
|
402
453
|
// unset agent.result / agent.lastResult / agent.liveness / agent.
|
|
403
454
|
// outputFile — those are observable by the parent after termination.
|
|
@@ -451,7 +502,10 @@ function finalizeTerminal(agent, status, { error, deps } = {}) {
|
|
|
451
502
|
}
|
|
452
503
|
|
|
453
504
|
// Push the re-entry notification so the parent learns about this
|
|
454
|
-
// even if it forgot to call WaitAgent.
|
|
505
|
+
// even if it forgot to call WaitAgent. A prior idle notification may
|
|
506
|
+
// still be indexed by agentId after the parent consumed the queue; remove
|
|
507
|
+
// it so terminal state can replace that non-terminal progress notice.
|
|
508
|
+
try { consumeNotificationForAgent(agent.id); } catch { /* ignore */ }
|
|
455
509
|
try {
|
|
456
510
|
const budgetResult = agent.result && typeof agent.result === 'object'
|
|
457
511
|
&& agent.result.status === 'budget_exceeded'
|
package/yeaft/tools/agent.js
CHANGED
|
@@ -25,7 +25,7 @@ import { randomUUID } from 'crypto';
|
|
|
25
25
|
import { getPersona, listPersonaIds } from '../personas.js';
|
|
26
26
|
import { startSubAgent } from '../sub-agent/runner.js';
|
|
27
27
|
import { STATUS, isTerminalAgentStatus } from '../sub-agent/status.js';
|
|
28
|
-
import { makeLiveness } from '../sub-agent/liveness.js';
|
|
28
|
+
import { diagnoseAgentLiveness, makeLiveness } from '../sub-agent/liveness.js';
|
|
29
29
|
|
|
30
30
|
/** In-memory sub-agent registry. */
|
|
31
31
|
const agents = new Map();
|
|
@@ -232,18 +232,16 @@ Guidelines:
|
|
|
232
232
|
- Use expected_output when the return shape matters
|
|
233
233
|
- Add a budget only when you need an explicit safety cutoff
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
1. SpawnAgent
|
|
237
|
-
2.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
5. Reply to user — relay what the sub-agent found in your own words.
|
|
235
|
+
Async orchestration:
|
|
236
|
+
1. SpawnAgent — starts the sub-agent as a background task and returns immediately.
|
|
237
|
+
2. Continue — keep working in the parent VP; do not block just to poll.
|
|
238
|
+
3. ListAgents — non-blocking status check when you need progress/liveness.
|
|
239
|
+
4. PromptAgent — optional follow-up if the sub-agent is idle and needs guidance.
|
|
240
|
+
5. CloseAgent — stop or finalize a sub-agent when it is no longer needed.
|
|
242
241
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
you just kicked off.`,
|
|
242
|
+
Completion/failure is delivered through sub-agent notifications on later parent
|
|
243
|
+
turns. WaitAgent remains available only as a short compatibility poll; do not
|
|
244
|
+
use it as the default workflow or call it repeatedly in a loop.`,
|
|
247
245
|
parameters: {
|
|
248
246
|
type: 'object',
|
|
249
247
|
properties: {
|
|
@@ -381,11 +379,13 @@ you just kicked off.`,
|
|
|
381
379
|
// tests). Leave the record in 'created' so existing tests still work.
|
|
382
380
|
}
|
|
383
381
|
|
|
382
|
+
const liveness = diagnoseAgentLiveness(agent);
|
|
384
383
|
return JSON.stringify({
|
|
385
384
|
next_steps:
|
|
386
|
-
'Sub-agent is
|
|
387
|
-
'
|
|
388
|
-
'
|
|
385
|
+
'Sub-agent is running in the background. Continue the parent task; ' +
|
|
386
|
+
'use ListAgents for a non-blocking status check, PromptAgent only ' +
|
|
387
|
+
'when the sub-agent is idle and needs more input, and rely on ' +
|
|
388
|
+
'completion notifications on later turns. Do not call WaitAgent in a loop.',
|
|
389
389
|
success: true,
|
|
390
390
|
agentId,
|
|
391
391
|
name,
|
|
@@ -393,7 +393,10 @@ you just kicked off.`,
|
|
|
393
393
|
budget: spec.budget || null,
|
|
394
394
|
status: agent.status,
|
|
395
395
|
outputFile: agent.outputFile || null,
|
|
396
|
-
|
|
396
|
+
liveness,
|
|
397
|
+
stale: liveness.stale,
|
|
398
|
+
stalled: liveness.stalled,
|
|
399
|
+
message: `Sub-agent "${name}" spawned (${agentId}) as an async background task. Use ListAgents to monitor it without blocking. Read \`outputFile\` for the durable event log at any time.`,
|
|
397
400
|
});
|
|
398
401
|
},
|
|
399
402
|
});
|
|
@@ -12,16 +12,17 @@
|
|
|
12
12
|
import { defineTool } from './types.js';
|
|
13
13
|
import { agentBelongsToCaller, getAgentRegistry } from './agent.js';
|
|
14
14
|
import { isTerminalAgentStatus } from '../sub-agent/status.js';
|
|
15
|
-
import {
|
|
15
|
+
import { diagnoseAgentLiveness } from '../sub-agent/liveness.js';
|
|
16
16
|
|
|
17
17
|
export default defineTool({
|
|
18
18
|
name: 'ListAgents',
|
|
19
19
|
description: `List all sub-agents and their current status.
|
|
20
20
|
|
|
21
21
|
Returns id, name, status, mission/task summary, durable outputFile path,
|
|
22
|
-
liveness counters (toolUseCount, tokenCount, msSinceLastEvent, recentTools)
|
|
23
|
-
and message count for each agent. Use
|
|
24
|
-
|
|
22
|
+
liveness counters (toolUseCount, tokenCount, msSinceLastEvent, recentTools),
|
|
23
|
+
stale/stalled diagnostics, result tail, and message count for each agent. Use
|
|
24
|
+
this as the primary non-blocking monitor for async sub-agent work, and Read
|
|
25
|
+
\`outputFile\` for any single agent if you need its full timeline.
|
|
25
26
|
|
|
26
27
|
By default only non-closed agents are returned. Pass include_closed=true
|
|
27
28
|
to also list closed/failed/abandoned/completed agents.`,
|
|
@@ -49,7 +50,10 @@ to also list closed/failed/abandoned/completed agents.`,
|
|
|
49
50
|
for (const [id, agent] of agents) {
|
|
50
51
|
if (!agentBelongsToCaller(agent, ctx)) continue;
|
|
51
52
|
if (!includeTerminal && isTerminalAgentStatus(agent.status)) continue;
|
|
52
|
-
const liveness =
|
|
53
|
+
const liveness = diagnoseAgentLiveness(agent, { now });
|
|
54
|
+
const resultText = (typeof agent.result === 'string' && agent.result)
|
|
55
|
+
? agent.result
|
|
56
|
+
: (agent.lastResult || '');
|
|
53
57
|
agentList.push({
|
|
54
58
|
id,
|
|
55
59
|
name: agent.name,
|
|
@@ -59,8 +63,13 @@ to also list closed/failed/abandoned/completed agents.`,
|
|
|
59
63
|
liveness,
|
|
60
64
|
lastEventAt: liveness.lastEventAt,
|
|
61
65
|
msSinceLastEvent: liveness.msSinceLastEvent,
|
|
66
|
+
lastEventType: liveness.lastEventType,
|
|
67
|
+
stale: liveness.stale,
|
|
68
|
+
stalled: liveness.stalled,
|
|
69
|
+
diagnostic: liveness.diagnostic,
|
|
62
70
|
error: agent.error || null,
|
|
63
71
|
hasResult: Boolean(agent.result || agent.lastResult),
|
|
72
|
+
resultTail: resultText ? resultText.slice(-1000) : '',
|
|
64
73
|
messages: Array.isArray(agent.messages) ? agent.messages.length : 0,
|
|
65
74
|
turns: agent.usage?.turns || 0,
|
|
66
75
|
createdAt: agent.createdAt,
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { defineTool } from './types.js';
|
|
34
34
|
import { agentBelongsToCaller, getAgentRegistry } from './agent.js';
|
|
35
35
|
import { isTerminalAgentStatus, STATUS } from '../sub-agent/status.js';
|
|
36
|
-
import {
|
|
36
|
+
import { diagnoseAgentLiveness } from '../sub-agent/liveness.js';
|
|
37
37
|
import { consumeNotificationForAgent } from '../sub-agent/notifications.js';
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -43,7 +43,7 @@ import { consumeNotificationForAgent } from '../sub-agent/notifications.js';
|
|
|
43
43
|
* tail-positioned nudges when `result` is long).
|
|
44
44
|
*
|
|
45
45
|
* @param {string} status
|
|
46
|
-
* @param {{ timedOut?: boolean, runningInBackground?: boolean, budgetExceeded?: boolean }} [opts]
|
|
46
|
+
* @param {{ timedOut?: boolean, runningInBackground?: boolean, budgetExceeded?: boolean, stale?: boolean }} [opts]
|
|
47
47
|
*/
|
|
48
48
|
function nextStepsFor(status, opts = {}) {
|
|
49
49
|
if (opts.budgetExceeded) {
|
|
@@ -55,15 +55,20 @@ function nextStepsFor(status, opts = {}) {
|
|
|
55
55
|
'as an ordinary successful completion.'
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
|
+
if (opts.timedOut && opts.stale) {
|
|
59
|
+
return (
|
|
60
|
+
'Sub-agent still has a running record but appears stalled. Do NOT keep ' +
|
|
61
|
+
'calling WaitAgent in a loop. Use ListAgents/outputFile to inspect it, ' +
|
|
62
|
+
'CloseAgent if you want to stop it, or report the stalled background ' +
|
|
63
|
+
'task and start a fresh agent if needed.'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
58
66
|
if (opts.timedOut) {
|
|
59
67
|
return (
|
|
60
|
-
'Sub-agent is
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'user the agent is still working and ask whether to keep waiting. ' +
|
|
65
|
-
'Read `outputFile` for the full event timeline. Do NOT end your turn ' +
|
|
66
|
-
'silently.'
|
|
68
|
+
'Sub-agent is running in the background; it does not need another ' +
|
|
69
|
+
'PromptAgent to keep going. Continue the main task or tell the user it ' +
|
|
70
|
+
'is still running. Use ListAgents later for a non-blocking status check; ' +
|
|
71
|
+
'only call WaitAgent again if the user explicitly wants to wait.'
|
|
67
72
|
);
|
|
68
73
|
}
|
|
69
74
|
switch (status) {
|
|
@@ -124,6 +129,7 @@ function errorNextSteps() {
|
|
|
124
129
|
*/
|
|
125
130
|
function buildEnvelope(agent, { timedOut = false } = {}) {
|
|
126
131
|
const status = agent.status;
|
|
132
|
+
const liveness = diagnoseAgentLiveness(agent);
|
|
127
133
|
const budgetResult = agent.result && typeof agent.result === 'object'
|
|
128
134
|
&& agent.result.status === 'budget_exceeded'
|
|
129
135
|
? agent.result
|
|
@@ -134,13 +140,18 @@ function buildEnvelope(agent, { timedOut = false } = {}) {
|
|
|
134
140
|
? agent.result
|
|
135
141
|
: (agent.lastResult || ''));
|
|
136
142
|
const env = {
|
|
137
|
-
next_steps: nextStepsFor(status, { timedOut, budgetExceeded: !!budgetResult }),
|
|
143
|
+
next_steps: nextStepsFor(status, { timedOut, budgetExceeded: !!budgetResult, stale: liveness.stale }),
|
|
138
144
|
agentId: agent.id,
|
|
139
145
|
name: agent.name,
|
|
140
146
|
status,
|
|
141
147
|
error: agent.error || null,
|
|
142
148
|
outputFile: agent.outputFile || null,
|
|
143
|
-
liveness
|
|
149
|
+
liveness,
|
|
150
|
+
stale: liveness.stale,
|
|
151
|
+
stalled: liveness.stalled,
|
|
152
|
+
msSinceLastEvent: liveness.msSinceLastEvent,
|
|
153
|
+
lastEventType: liveness.lastEventType,
|
|
154
|
+
diagnostic: liveness.diagnostic,
|
|
144
155
|
messages: Array.isArray(agent.messages) ? agent.messages.length : 0,
|
|
145
156
|
turns: agent.usage?.turns || 0,
|
|
146
157
|
};
|
|
@@ -188,7 +199,7 @@ NEVER end your turn silently right after WaitAgent — the user has not seen
|
|
|
188
199
|
the sub-agent's reply yet; only you have. The orchestration loop is
|
|
189
200
|
SpawnAgent → (PromptAgent ↔ WaitAgent)+ → CloseAgent → final reply to user.
|
|
190
201
|
|
|
191
|
-
The default wait is
|
|
202
|
+
Compatibility tool. The default wait is a short 5000ms poll. Callers may request up to 300000ms (5 minutes), but this is no longer the primary sub-agent workflow; prefer SpawnAgent + ListAgents + completion notifications for async background work.`,
|
|
192
203
|
parameters: {
|
|
193
204
|
type: 'object',
|
|
194
205
|
properties: {
|
|
@@ -200,7 +211,7 @@ The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
|
200
211
|
type: 'number',
|
|
201
212
|
minimum: 0,
|
|
202
213
|
maximum: 300000,
|
|
203
|
-
description: 'Maximum time to wait in milliseconds (default:
|
|
214
|
+
description: 'Maximum time to wait in milliseconds (default: 5000 short poll, max: 300000 / 5 minutes)',
|
|
204
215
|
},
|
|
205
216
|
},
|
|
206
217
|
required: ['agent_id'],
|
|
@@ -209,7 +220,7 @@ The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
|
209
220
|
isConcurrencySafe: () => true,
|
|
210
221
|
isReadOnly: () => true,
|
|
211
222
|
async execute(input, ctx) {
|
|
212
|
-
const { agent_id, timeout_ms =
|
|
223
|
+
const { agent_id, timeout_ms = 5000 } = input;
|
|
213
224
|
if (!agent_id) {
|
|
214
225
|
return JSON.stringify({ next_steps: errorNextSteps(), error: 'agent_id is required' });
|
|
215
226
|
}
|
|
@@ -234,6 +245,7 @@ The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
|
234
245
|
return JSON.stringify(buildEnvelope(agent));
|
|
235
246
|
}
|
|
236
247
|
if (agent.status === STATUS.IDLE) {
|
|
248
|
+
consumeNotificationForAgent(agent.id);
|
|
237
249
|
return JSON.stringify(buildEnvelope(agent));
|
|
238
250
|
}
|
|
239
251
|
if (ctx?.signal?.aborted) {
|
|
@@ -248,6 +260,7 @@ The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
|
248
260
|
return JSON.stringify(buildEnvelope(agent));
|
|
249
261
|
}
|
|
250
262
|
if (agent.status === STATUS.IDLE) {
|
|
263
|
+
consumeNotificationForAgent(agent.id);
|
|
251
264
|
return JSON.stringify(buildEnvelope(agent));
|
|
252
265
|
}
|
|
253
266
|
if (ctx?.signal?.aborted) {
|