@yeaft/webchat-agent 0.1.951 → 0.1.953
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/workbench/transfer.js +74 -1
- package/yeaft/engine.js +48 -3
- package/yeaft/sub-agent/liveness.js +101 -0
- package/yeaft/sub-agent/notifications.js +257 -0
- package/yeaft/sub-agent/output-log.js +237 -0
- package/yeaft/sub-agent/runner.js +332 -146
- package/yeaft/sub-agent/status.js +81 -0
- package/yeaft/tools/agent.js +70 -10
- package/yeaft/tools/close-agent.js +55 -9
- package/yeaft/tools/list-agents.js +40 -11
- package/yeaft/tools/send-message.js +36 -11
- package/yeaft/tools/wait-agent.js +168 -110
|
@@ -1,69 +1,103 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* wait-agent.js — Wait for a sub-agent
|
|
2
|
+
* wait-agent.js — Wait for a sub-agent's next state change.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Returns a JSON envelope shaped for the model to make an explicit
|
|
5
|
+
* decision. The shape is stable across statuses; the `next_steps` field
|
|
6
|
+
* always names the exact tool the model should call next.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* Status semantics returned:
|
|
9
|
+
* - completed / closed / failed / abandoned (terminal): include
|
|
10
|
+
* `error` (when applicable), final `result`, and outputFile path
|
|
11
|
+
* for re-reading. We also drain the agent's queued re-entry
|
|
12
|
+
* notification so the engine doesn't redeliver it on the next user
|
|
13
|
+
* turn.
|
|
14
|
+
* - idle: sub-agent finished a turn and is parked, waiting for a
|
|
15
|
+
* PromptAgent or CloseAgent. `result` carries the last
|
|
16
|
+
* assistant text. Distinct from terminal — the parent can
|
|
17
|
+
* still send more work.
|
|
18
|
+
* - running with timedOut=true: we waited and the agent is still
|
|
19
|
+
* processing. The envelope flags `runningInBackground:
|
|
20
|
+
* true` (the sub-agent IS continuing — it does NOT need
|
|
21
|
+
* another PromptAgent to keep going) and recommends either
|
|
22
|
+
* another WaitAgent or CloseAgent. `result` carries the
|
|
23
|
+
* mid-stream preview (the driver keeps lastResult fresh
|
|
24
|
+
* from every text_delta).
|
|
9
25
|
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Fix — every WaitAgent response now carries an explicit, status-dependent
|
|
17
|
-
* `next_steps` field that names the exact tool to call next, and the tool
|
|
18
|
-
* description spells out the full SpawnAgent → PromptAgent → WaitAgent →
|
|
19
|
-
* CloseAgent loop. The same nudge pattern lives on the companion tools.
|
|
26
|
+
* Every envelope includes `outputFile` (durable per-agent JSONL log) and
|
|
27
|
+
* `liveness` (toolUseCount, tokenCount, msSinceLastEvent, recentTools,
|
|
28
|
+
* lastEventType) so the model can distinguish "stuck" from "still
|
|
29
|
+
* working" and can Read the log directly when it needs the full
|
|
30
|
+
* timeline.
|
|
20
31
|
*/
|
|
21
32
|
|
|
22
33
|
import { defineTool } from './types.js';
|
|
23
|
-
import { getAgentRegistry } from './agent.js';
|
|
34
|
+
import { agentBelongsToCaller, getAgentRegistry } from './agent.js';
|
|
35
|
+
import { isTerminalAgentStatus, STATUS } from '../sub-agent/status.js';
|
|
36
|
+
import { snapshotLiveness } from '../sub-agent/liveness.js';
|
|
37
|
+
import { consumeNotificationForAgent } from '../sub-agent/notifications.js';
|
|
24
38
|
|
|
25
39
|
/**
|
|
26
40
|
* Build the status-specific next-step guidance the LLM reads after a wait.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
41
|
+
* Imperative + names actual tools. Always appears as the FIRST field on
|
|
42
|
+
* the envelope (the registry's 1 KiB tail-truncation would otherwise eat
|
|
43
|
+
* tail-positioned nudges when `result` is long).
|
|
30
44
|
*
|
|
31
45
|
* @param {string} status
|
|
32
|
-
* @param {boolean} [
|
|
46
|
+
* @param {{ timedOut?: boolean, runningInBackground?: boolean, budgetExceeded?: boolean }} [opts]
|
|
33
47
|
*/
|
|
34
|
-
function nextStepsFor(status,
|
|
35
|
-
if (
|
|
48
|
+
function nextStepsFor(status, opts = {}) {
|
|
49
|
+
if (opts.budgetExceeded) {
|
|
36
50
|
return (
|
|
37
|
-
'Sub-agent
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
51
|
+
'Sub-agent stopped because an explicit budget limit was reached. ' +
|
|
52
|
+
'Use `partial_output`, `budget_reason`, and `budget_usage` to decide ' +
|
|
53
|
+
'whether to relay the partial result, spawn a fresh agent with a ' +
|
|
54
|
+
'larger budget, or report the cutoff to the user. Do NOT present this ' +
|
|
55
|
+
'as an ordinary successful completion.'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (opts.timedOut) {
|
|
59
|
+
return (
|
|
60
|
+
'Sub-agent is STILL RUNNING in the background — it does NOT need ' +
|
|
61
|
+
'another PromptAgent to keep going. Decide: (a) call WaitAgent again ' +
|
|
62
|
+
'with a larger timeout_ms to keep waiting, (b) call CloseAgent if ' +
|
|
63
|
+
'you want to cut it short and use partial output, or (c) tell the ' +
|
|
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.'
|
|
42
67
|
);
|
|
43
68
|
}
|
|
44
69
|
switch (status) {
|
|
45
|
-
case
|
|
70
|
+
case STATUS.IDLE:
|
|
46
71
|
return (
|
|
47
|
-
'Sub-agent finished one turn and is idle. The
|
|
48
|
-
'reply — relay it to the user in your own
|
|
49
|
-
'
|
|
50
|
-
'silently without telling the
|
|
72
|
+
'Sub-agent finished one turn and is idle (queue empty). The ' +
|
|
73
|
+
'`result` above is its reply — relay it to the user in your own ' +
|
|
74
|
+
'words, or send a follow-up via PromptAgent, or finalize via ' +
|
|
75
|
+
'CloseAgent. Do NOT end your turn silently without telling the ' +
|
|
76
|
+
'user what the sub-agent said.'
|
|
51
77
|
);
|
|
52
|
-
case
|
|
78
|
+
case STATUS.COMPLETED:
|
|
53
79
|
return (
|
|
54
|
-
'Sub-agent finished successfully (terminal). Summarize the
|
|
55
|
-
'for the user in your own reply. Do NOT end your turn
|
|
80
|
+
'Sub-agent finished successfully (terminal). Summarize the ' +
|
|
81
|
+
'`result` for the user in your own reply. Do NOT end your turn ' +
|
|
82
|
+
'with no text.'
|
|
56
83
|
);
|
|
57
|
-
case
|
|
84
|
+
case STATUS.CLOSED:
|
|
58
85
|
return (
|
|
59
|
-
'Sub-agent was closed (terminal). Report the final `result` to
|
|
60
|
-
'user in your reply. Do NOT end your turn silently.'
|
|
86
|
+
'Sub-agent was closed (terminal). Report the final `result` to ' +
|
|
87
|
+
'the user in your reply. Do NOT end your turn silently.'
|
|
61
88
|
);
|
|
62
|
-
case
|
|
89
|
+
case STATUS.FAILED:
|
|
63
90
|
return (
|
|
64
|
-
'Sub-agent failed — see `error`. Decide whether to retry with a
|
|
65
|
-
'SpawnAgent, adjust the mission, or report the failure to
|
|
66
|
-
'Do NOT end your turn silently.'
|
|
91
|
+
'Sub-agent failed — see `error`. Decide whether to retry with a ' +
|
|
92
|
+
'fresh SpawnAgent, adjust the mission, or report the failure to ' +
|
|
93
|
+
'the user. Do NOT end your turn silently.'
|
|
94
|
+
);
|
|
95
|
+
case STATUS.ABANDONED:
|
|
96
|
+
return (
|
|
97
|
+
'Sub-agent was abandoned by the idle watchdog — it sat idle too ' +
|
|
98
|
+
'long without a follow-up. The last `result` is its final reply. ' +
|
|
99
|
+
'Either relay it to the user or SpawnAgent fresh with a new ' +
|
|
100
|
+
'mission. Do NOT end your turn silently.'
|
|
67
101
|
);
|
|
68
102
|
default:
|
|
69
103
|
return (
|
|
@@ -74,40 +108,84 @@ function nextStepsFor(status, timedOut = false) {
|
|
|
74
108
|
}
|
|
75
109
|
}
|
|
76
110
|
|
|
77
|
-
/**
|
|
78
|
-
* Nudge for the `{error: ...}` error envelopes. The error path used to ship a
|
|
79
|
-
* naked `{error}` blob — same shape that caused the silent-end_turn bug on the
|
|
80
|
-
* happy path. Tell the LLM what to do about an error (correct the call, or
|
|
81
|
-
* report the failure to the user) so it does not end its turn silently after a
|
|
82
|
-
* fat-finger like a wrong agent_id.
|
|
83
|
-
*/
|
|
84
111
|
function errorNextSteps() {
|
|
85
112
|
return (
|
|
86
|
-
'That call failed — see `error`. Either correct the arguments and
|
|
87
|
-
'or tell the user what went wrong. Do NOT end your turn
|
|
88
|
-
'error envelope; the user has not seen the error,
|
|
113
|
+
'That call failed — see `error`. Either correct the arguments and ' +
|
|
114
|
+
'retry, or tell the user what went wrong. Do NOT end your turn ' +
|
|
115
|
+
'silently after an error envelope; the user has not seen the error, ' +
|
|
116
|
+
'only you have.'
|
|
89
117
|
);
|
|
90
118
|
}
|
|
91
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Build the envelope for a single status snapshot. `result` is taken
|
|
122
|
+
* from `agent.result` (final) if present, else from `agent.lastResult`
|
|
123
|
+
* (mid-stream preview).
|
|
124
|
+
*/
|
|
125
|
+
function buildEnvelope(agent, { timedOut = false } = {}) {
|
|
126
|
+
const status = agent.status;
|
|
127
|
+
const budgetResult = agent.result && typeof agent.result === 'object'
|
|
128
|
+
&& agent.result.status === 'budget_exceeded'
|
|
129
|
+
? agent.result
|
|
130
|
+
: null;
|
|
131
|
+
const resultText = budgetResult
|
|
132
|
+
? (budgetResult.partial_output || '')
|
|
133
|
+
: ((typeof agent.result === 'string' && agent.result)
|
|
134
|
+
? agent.result
|
|
135
|
+
: (agent.lastResult || ''));
|
|
136
|
+
const env = {
|
|
137
|
+
next_steps: nextStepsFor(status, { timedOut, budgetExceeded: !!budgetResult }),
|
|
138
|
+
agentId: agent.id,
|
|
139
|
+
name: agent.name,
|
|
140
|
+
status,
|
|
141
|
+
error: agent.error || null,
|
|
142
|
+
outputFile: agent.outputFile || null,
|
|
143
|
+
liveness: snapshotLiveness(agent.liveness),
|
|
144
|
+
messages: Array.isArray(agent.messages) ? agent.messages.length : 0,
|
|
145
|
+
turns: agent.usage?.turns || 0,
|
|
146
|
+
};
|
|
147
|
+
if (timedOut) {
|
|
148
|
+
env.timedOut = true;
|
|
149
|
+
env.runningInBackground = true;
|
|
150
|
+
env.message = `Agent "${agent.name}" is still running in the background.`;
|
|
151
|
+
}
|
|
152
|
+
if (budgetResult) {
|
|
153
|
+
env.budgetExceeded = true;
|
|
154
|
+
env.budget_status = budgetResult.status;
|
|
155
|
+
env.budget_reason = budgetResult.reason || null;
|
|
156
|
+
env.partial_output = budgetResult.partial_output || '';
|
|
157
|
+
env.budget_usage = budgetResult.usage || null;
|
|
158
|
+
}
|
|
159
|
+
env.result = resultText;
|
|
160
|
+
return env;
|
|
161
|
+
}
|
|
162
|
+
|
|
92
163
|
export default defineTool({
|
|
93
164
|
name: 'WaitAgent',
|
|
94
|
-
description: `Wait for a sub-agent
|
|
165
|
+
description: `Wait for a sub-agent's next state change (turn end, terminal, or wait-timeout) and retrieve a status envelope.
|
|
95
166
|
|
|
96
|
-
Returns
|
|
97
|
-
|
|
98
|
-
|
|
167
|
+
Returns JSON with explicit \`status\`, latest \`result\` text, \`liveness\`
|
|
168
|
+
counters (toolUseCount, tokenCount, msSinceLastEvent, recentTools), the
|
|
169
|
+
durable \`outputFile\` path you can Read at any time, and a status-specific
|
|
170
|
+
\`next_steps\` directive telling you what to call next.
|
|
99
171
|
|
|
100
|
-
|
|
101
|
-
•
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
•
|
|
105
|
-
|
|
106
|
-
• timedOut=true
|
|
107
|
-
|
|
172
|
+
Status semantics:
|
|
173
|
+
• completed / closed / failed / abandoned → terminal. The sub-agent will
|
|
174
|
+
do nothing more. Relay the result to the user (or retry / report the
|
|
175
|
+
failure).
|
|
176
|
+
• idle → the sub-agent finished a turn and is parked with an empty
|
|
177
|
+
queue. You can PromptAgent for follow-up or CloseAgent to finalize.
|
|
178
|
+
• timedOut=true (with status='running' and runningInBackground=true) →
|
|
179
|
+
the wait elapsed but the sub-agent is STILL working. It does NOT need
|
|
180
|
+
another PromptAgent. Either WaitAgent again with a larger timeout,
|
|
181
|
+
CloseAgent to cut it short, or tell the user it's still working.
|
|
108
182
|
|
|
109
|
-
|
|
110
|
-
|
|
183
|
+
CRITICAL — after WaitAgent returns you MUST take one of these actions:
|
|
184
|
+
• status terminal: relay/retry/report.
|
|
185
|
+
• status idle: reply to user OR PromptAgent OR CloseAgent.
|
|
186
|
+
• timedOut: re-wait, cut short, or report progress.
|
|
187
|
+
NEVER end your turn silently right after WaitAgent — the user has not seen
|
|
188
|
+
the sub-agent's reply yet; only you have. The orchestration loop is
|
|
111
189
|
SpawnAgent → (PromptAgent ↔ WaitAgent)+ → CloseAgent → final reply to user.
|
|
112
190
|
|
|
113
191
|
The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
@@ -132,11 +210,6 @@ The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
|
132
210
|
isReadOnly: () => true,
|
|
133
211
|
async execute(input, ctx) {
|
|
134
212
|
const { agent_id, timeout_ms = 30000 } = input;
|
|
135
|
-
// NB: `next_steps` is intentionally the FIRST field in every envelope
|
|
136
|
-
// below. `agent/yeaft/tools/registry.js` caps each tool result at
|
|
137
|
-
// TOOL_RESULT_MAX_BYTES (1 KiB) by truncating the tail — if `next_steps`
|
|
138
|
-
// were last, a long `result` would push the directive off the end and the
|
|
139
|
-
// LLM would never see the very nudge this PR delivers.
|
|
140
213
|
if (!agent_id) {
|
|
141
214
|
return JSON.stringify({ next_steps: errorNextSteps(), error: 'agent_id is required' });
|
|
142
215
|
}
|
|
@@ -150,56 +223,41 @@ The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
|
150
223
|
if (!agent) {
|
|
151
224
|
return JSON.stringify({ next_steps: errorNextSteps(), error: `Agent not found: ${agent_id}` });
|
|
152
225
|
}
|
|
226
|
+
if (!agentBelongsToCaller(agent, ctx)) {
|
|
227
|
+
return JSON.stringify({ next_steps: errorNextSteps(), error: `Agent not found: ${agent_id}` });
|
|
228
|
+
}
|
|
153
229
|
|
|
154
|
-
// Terminal
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
});
|
|
230
|
+
// Terminal already — return immediately and drain the notification
|
|
231
|
+
// so the engine doesn't redeliver it on the next user turn.
|
|
232
|
+
if (isTerminalAgentStatus(agent.status)) {
|
|
233
|
+
consumeNotificationForAgent(agent.id);
|
|
234
|
+
return JSON.stringify(buildEnvelope(agent));
|
|
235
|
+
}
|
|
236
|
+
if (agent.status === STATUS.IDLE) {
|
|
237
|
+
return JSON.stringify(buildEnvelope(agent));
|
|
238
|
+
}
|
|
239
|
+
if (ctx?.signal?.aborted) {
|
|
240
|
+
return JSON.stringify({ next_steps: errorNextSteps(), error: 'Wait cancelled', agentId: agent_id });
|
|
166
241
|
}
|
|
167
242
|
|
|
168
|
-
//
|
|
169
|
-
// for the next SendMessage. That IS a useful return point for the
|
|
170
|
-
// parent — surface lastResult and let parent decide what's next.
|
|
243
|
+
// Block until next interesting state change, capped at timeout_ms.
|
|
171
244
|
const deadline = Date.now() + timeout_ms;
|
|
172
245
|
while (Date.now() < deadline) {
|
|
173
|
-
if (agent.status
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
result: agent.result || agent.lastResult || '',
|
|
180
|
-
error: agent.error || null,
|
|
181
|
-
messages: agent.messages.length,
|
|
182
|
-
turns: agent.usage?.turns || 0,
|
|
183
|
-
});
|
|
246
|
+
if (isTerminalAgentStatus(agent.status)) {
|
|
247
|
+
consumeNotificationForAgent(agent.id);
|
|
248
|
+
return JSON.stringify(buildEnvelope(agent));
|
|
249
|
+
}
|
|
250
|
+
if (agent.status === STATUS.IDLE) {
|
|
251
|
+
return JSON.stringify(buildEnvelope(agent));
|
|
184
252
|
}
|
|
185
|
-
|
|
186
253
|
if (ctx?.signal?.aborted) {
|
|
187
254
|
return JSON.stringify({ next_steps: errorNextSteps(), error: 'Wait cancelled', agentId: agent_id });
|
|
188
255
|
}
|
|
189
|
-
|
|
190
256
|
await new Promise(r => setTimeout(r, 200));
|
|
191
257
|
}
|
|
192
258
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
name: agent.name,
|
|
197
|
-
status: agent.status,
|
|
198
|
-
timedOut: true,
|
|
199
|
-
message: `Agent "${agent.name}" is still running after ${timeout_ms}ms`,
|
|
200
|
-
result: agent.lastResult || '',
|
|
201
|
-
messages: agent.messages.length,
|
|
202
|
-
turns: agent.usage?.turns || 0,
|
|
203
|
-
});
|
|
259
|
+
// Wait elapsed; the sub-agent is still running. Surface mid-stream
|
|
260
|
+
// preview + liveness so the parent has actionable signal.
|
|
261
|
+
return JSON.stringify(buildEnvelope(agent, { timedOut: true }));
|
|
204
262
|
},
|
|
205
263
|
});
|