@yeaft/webchat-agent 0.1.930 → 0.1.931
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/tools/agent.js +31 -2
- package/yeaft/tools/close-agent.js +19 -3
- package/yeaft/tools/send-message.js +25 -6
- package/yeaft/tools/wait-agent.js +119 -11
package/package.json
CHANGED
package/yeaft/tools/agent.js
CHANGED
|
@@ -191,7 +191,19 @@ Guidelines:
|
|
|
191
191
|
- Give a clear, focused mission — what "done" looks like
|
|
192
192
|
- Use expected_output when the return shape matters
|
|
193
193
|
- Add a budget only when you need an explicit safety cutoff
|
|
194
|
-
|
|
194
|
+
|
|
195
|
+
Orchestration loop you MUST follow:
|
|
196
|
+
1. SpawnAgent — fire-and-forget; the sub-agent is now running.
|
|
197
|
+
2. WaitAgent — blocks until the sub-agent goes idle / completes /
|
|
198
|
+
fails / closes (or times out). Returns the reply.
|
|
199
|
+
3. PromptAgent — optional: queue a follow-up; then WaitAgent again.
|
|
200
|
+
4. CloseAgent — finalize when done.
|
|
201
|
+
5. Reply to user — relay what the sub-agent found in your own words.
|
|
202
|
+
|
|
203
|
+
CRITICAL — SpawnAgent only KICKS OFF the sub-agent. It has NOT produced any
|
|
204
|
+
result yet. You MUST call WaitAgent next to collect the reply. Do NOT end your
|
|
205
|
+
turn right after SpawnAgent without calling WaitAgent or telling the user what
|
|
206
|
+
you just kicked off.`,
|
|
195
207
|
parameters: {
|
|
196
208
|
type: 'object',
|
|
197
209
|
properties: {
|
|
@@ -235,8 +247,19 @@ Guidelines:
|
|
|
235
247
|
isConcurrencySafe: () => false,
|
|
236
248
|
isReadOnly: () => false,
|
|
237
249
|
async execute(input, ctx) {
|
|
250
|
+
// NB: every envelope below puts `next_steps` (or `error_next_steps`) at
|
|
251
|
+
// the FIRST position because `agent/yeaft/tools/registry.js` caps tool
|
|
252
|
+
// output at 1 KiB by chopping the tail. Tail-positioned nudges get
|
|
253
|
+
// truncated when the rest of the envelope is large.
|
|
254
|
+
const ERROR_NEXT_STEPS =
|
|
255
|
+
'That call failed — see `error`. Either correct the arguments and ' +
|
|
256
|
+
'retry, or tell the user what went wrong. Do NOT end your turn ' +
|
|
257
|
+
'silently after an error envelope.';
|
|
258
|
+
|
|
238
259
|
const validation = validateSpec(input);
|
|
239
|
-
if (!validation.ok)
|
|
260
|
+
if (!validation.ok) {
|
|
261
|
+
return JSON.stringify({ next_steps: ERROR_NEXT_STEPS, error: validation.error });
|
|
262
|
+
}
|
|
240
263
|
const spec = validation.spec;
|
|
241
264
|
const { name, cwd } = input;
|
|
242
265
|
|
|
@@ -244,6 +267,7 @@ Guidelines:
|
|
|
244
267
|
for (const [, agent] of agents) {
|
|
245
268
|
if (agent.name === name && agent.status !== 'closed') {
|
|
246
269
|
return JSON.stringify({
|
|
270
|
+
next_steps: ERROR_NEXT_STEPS,
|
|
247
271
|
error: `Agent "${name}" already exists. Close it first or use a different name.`,
|
|
248
272
|
agentId: agent.id,
|
|
249
273
|
});
|
|
@@ -288,6 +312,7 @@ Guidelines:
|
|
|
288
312
|
agent.error = err && err.message ? err.message : String(err);
|
|
289
313
|
agent.diagnostics.push({ type: 'spawn_error', error: agent.error, at: Date.now() });
|
|
290
314
|
return JSON.stringify({
|
|
315
|
+
next_steps: ERROR_NEXT_STEPS,
|
|
291
316
|
error: `Failed to start sub-agent: ${agent.error}`,
|
|
292
317
|
agentId,
|
|
293
318
|
});
|
|
@@ -298,6 +323,10 @@ Guidelines:
|
|
|
298
323
|
}
|
|
299
324
|
|
|
300
325
|
return JSON.stringify({
|
|
326
|
+
next_steps:
|
|
327
|
+
'Sub-agent is now running — no reply yet. Call WaitAgent next to ' +
|
|
328
|
+
'collect its first turn. Do NOT end your turn here without either ' +
|
|
329
|
+
'waiting for the reply or telling the user what you just spawned.',
|
|
301
330
|
success: true,
|
|
302
331
|
agentId,
|
|
303
332
|
name,
|
|
@@ -10,7 +10,12 @@ export default defineTool({
|
|
|
10
10
|
description: `Close a sub-agent and release its resources.
|
|
11
11
|
|
|
12
12
|
Use when a sub-agent's task is complete or no longer needed.
|
|
13
|
-
The agent's result (if any) is returned before closing
|
|
13
|
+
The agent's final \`result\` (if any) is returned in the envelope before closing.
|
|
14
|
+
|
|
15
|
+
CRITICAL — closing the sub-agent is NOT the end of YOUR turn. After CloseAgent
|
|
16
|
+
you MUST relay the \`result\` to the user in your own reply (or summarize what
|
|
17
|
+
was accomplished). The user has not seen the sub-agent's reply — only you have.
|
|
18
|
+
Do NOT end your turn silently right after CloseAgent.`,
|
|
14
19
|
parameters: {
|
|
15
20
|
type: 'object',
|
|
16
21
|
properties: {
|
|
@@ -28,14 +33,21 @@ The agent's result (if any) is returned before closing.`,
|
|
|
28
33
|
isConcurrencySafe: () => false,
|
|
29
34
|
isReadOnly: () => false,
|
|
30
35
|
async execute(input, ctx) {
|
|
36
|
+
// NB: next_steps is the FIRST envelope field — the registry's 1 KiB
|
|
37
|
+
// tail-truncation would eat it if it lived at the end.
|
|
38
|
+
const ERROR_NEXT_STEPS =
|
|
39
|
+
'That call failed — see `error`. Either correct the arguments and ' +
|
|
40
|
+
'retry, or tell the user what went wrong. Do NOT end your turn ' +
|
|
41
|
+
'silently after an error envelope.';
|
|
42
|
+
|
|
31
43
|
const { agent_id, result } = input;
|
|
32
|
-
if (!agent_id) return JSON.stringify({ error: 'agent_id is required' });
|
|
44
|
+
if (!agent_id) return JSON.stringify({ next_steps: ERROR_NEXT_STEPS, error: 'agent_id is required' });
|
|
33
45
|
|
|
34
46
|
const agents = getAgentRegistry();
|
|
35
47
|
const agent = agents.get(agent_id);
|
|
36
48
|
|
|
37
49
|
if (!agent) {
|
|
38
|
-
return JSON.stringify({ error: `Agent not found: ${agent_id}` });
|
|
50
|
+
return JSON.stringify({ next_steps: ERROR_NEXT_STEPS, error: `Agent not found: ${agent_id}` });
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
if (result) {
|
|
@@ -54,6 +66,10 @@ The agent's result (if any) is returned before closing.`,
|
|
|
54
66
|
agent.status = 'closed';
|
|
55
67
|
|
|
56
68
|
return JSON.stringify({
|
|
69
|
+
next_steps:
|
|
70
|
+
'Sub-agent is closed. Now reply to the user — summarize what was ' +
|
|
71
|
+
'accomplished and surface the `result` text. Do NOT end your turn ' +
|
|
72
|
+
'without telling the user what happened.',
|
|
57
73
|
success: true,
|
|
58
74
|
agentId: agent_id,
|
|
59
75
|
name: agent.name,
|
|
@@ -14,7 +14,14 @@ export default defineTool({
|
|
|
14
14
|
description: `Send a follow-up prompt to a sub-agent you previously spawned.
|
|
15
15
|
|
|
16
16
|
Use this to give the sub-agent more work, additional instructions, or relay
|
|
17
|
-
information. The prompt is queued for the agent to process on its next turn
|
|
17
|
+
information. The prompt is queued for the agent to process on its next turn.
|
|
18
|
+
|
|
19
|
+
IMPORTANT — PromptAgent only QUEUES the message; it does NOT block. After this
|
|
20
|
+
returns you almost always want to call WaitAgent next to collect the reply.
|
|
21
|
+
Do NOT end your turn after PromptAgent without either (a) calling WaitAgent,
|
|
22
|
+
(b) explaining to the user what you just asked the sub-agent, or (c) calling
|
|
23
|
+
CloseAgent. The orchestration loop is
|
|
24
|
+
SpawnAgent → (PromptAgent ↔ WaitAgent)+ → CloseAgent → final reply to user.`,
|
|
18
25
|
parameters: {
|
|
19
26
|
type: 'object',
|
|
20
27
|
properties: {
|
|
@@ -32,22 +39,29 @@ information. The prompt is queued for the agent to process on its next turn.`,
|
|
|
32
39
|
isConcurrencySafe: () => false,
|
|
33
40
|
isReadOnly: () => false,
|
|
34
41
|
async execute(input, ctx) {
|
|
42
|
+
// NB: next_steps is the FIRST envelope field — the registry's 1 KiB
|
|
43
|
+
// tail-truncation would eat it if it lived at the end.
|
|
44
|
+
const ERROR_NEXT_STEPS =
|
|
45
|
+
'That call failed — see `error`. Either correct the arguments and ' +
|
|
46
|
+
'retry, or tell the user what went wrong. Do NOT end your turn ' +
|
|
47
|
+
'silently after an error envelope.';
|
|
48
|
+
|
|
35
49
|
const { agent_id, message } = input;
|
|
36
|
-
if (!agent_id) return JSON.stringify({ error: 'agent_id is required' });
|
|
37
|
-
if (!message) return JSON.stringify({ error: 'message is required' });
|
|
50
|
+
if (!agent_id) return JSON.stringify({ next_steps: ERROR_NEXT_STEPS, error: 'agent_id is required' });
|
|
51
|
+
if (!message) return JSON.stringify({ next_steps: ERROR_NEXT_STEPS, error: 'message is required' });
|
|
38
52
|
|
|
39
53
|
const agents = getAgentRegistry();
|
|
40
54
|
const agent = agents.get(agent_id);
|
|
41
55
|
|
|
42
56
|
if (!agent) {
|
|
43
|
-
return JSON.stringify({ error: `Agent not found: ${agent_id}` });
|
|
57
|
+
return JSON.stringify({ next_steps: ERROR_NEXT_STEPS, error: `Agent not found: ${agent_id}` });
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
if (agent.status === 'closed') {
|
|
47
|
-
return JSON.stringify({ error: `Agent "${agent.name}" is closed` });
|
|
61
|
+
return JSON.stringify({ next_steps: ERROR_NEXT_STEPS, error: `Agent "${agent.name}" is closed` });
|
|
48
62
|
}
|
|
49
63
|
if (agent.status === 'failed') {
|
|
50
|
-
return JSON.stringify({ error: `Agent "${agent.name}" has failed: ${agent.error || 'unknown error'}` });
|
|
64
|
+
return JSON.stringify({ next_steps: ERROR_NEXT_STEPS, error: `Agent "${agent.name}" has failed: ${agent.error || 'unknown error'}` });
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
// PR-M1: queue as a pending prompt the driver will pull. This wakes
|
|
@@ -65,6 +79,11 @@ information. The prompt is queued for the agent to process on its next turn.`,
|
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
return JSON.stringify({
|
|
82
|
+
next_steps:
|
|
83
|
+
'Message is queued — the sub-agent has NOT replied yet. Call WaitAgent ' +
|
|
84
|
+
'next to collect the reply, then relay it to the user. Do NOT end your ' +
|
|
85
|
+
'turn here without either waiting for the reply or telling the user ' +
|
|
86
|
+
'what you just asked.',
|
|
68
87
|
success: true,
|
|
69
88
|
agentId: agent_id,
|
|
70
89
|
name: agent.name,
|
|
@@ -1,16 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* wait-agent.js — Wait for a sub-agent to complete and get its result.
|
|
3
|
+
*
|
|
4
|
+
* Loop-stall fix (2026-06-11):
|
|
5
|
+
*
|
|
6
|
+
* Symptom — when the LLM called WaitAgent inside a sub-agent orchestration,
|
|
7
|
+
* the assistant turn ended silently right after the tool returned. The user
|
|
8
|
+
* saw a stuck "tool ran" panel and no follow-up text or further tool calls.
|
|
9
|
+
*
|
|
10
|
+
* Cause — the engine loop is correct (it appends the tool result and re-enters
|
|
11
|
+
* `adapter.stream()`), but the LLM was reading the bare JSON envelope (status +
|
|
12
|
+
* result + turns…) as a "complete answer" and emitting `end_turn` with no
|
|
13
|
+
* text. The previous description ("Returns the agent's final result or current
|
|
14
|
+
* status") gave it zero guidance about what to do next.
|
|
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.
|
|
3
20
|
*/
|
|
4
21
|
|
|
5
22
|
import { defineTool } from './types.js';
|
|
6
23
|
import { getAgentRegistry } from './agent.js';
|
|
7
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Build the status-specific next-step guidance the LLM reads after a wait.
|
|
27
|
+
*
|
|
28
|
+
* The wording is imperative and names actual tools so the model has a clear
|
|
29
|
+
* action to take — leaving it implicit was the bug.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} status
|
|
32
|
+
* @param {boolean} [timedOut]
|
|
33
|
+
*/
|
|
34
|
+
function nextStepsFor(status, timedOut = false) {
|
|
35
|
+
if (timedOut) {
|
|
36
|
+
return (
|
|
37
|
+
'Sub-agent is still running. Either (a) call WaitAgent again with a ' +
|
|
38
|
+
'larger timeout_ms to keep waiting, (b) call CloseAgent if you want to ' +
|
|
39
|
+
'cut it short and use partial output, or (c) explain to the user that ' +
|
|
40
|
+
'the agent is still working and ask whether to keep waiting. Do NOT ' +
|
|
41
|
+
'end your turn silently.'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
switch (status) {
|
|
45
|
+
case 'idle':
|
|
46
|
+
return (
|
|
47
|
+
'Sub-agent finished one turn and is idle. The `result` above is its ' +
|
|
48
|
+
'reply — relay it to the user in your own words, or send a follow-up ' +
|
|
49
|
+
'via PromptAgent, or finalize via CloseAgent. Do NOT end your turn ' +
|
|
50
|
+
'silently without telling the user what the sub-agent said.'
|
|
51
|
+
);
|
|
52
|
+
case 'completed':
|
|
53
|
+
return (
|
|
54
|
+
'Sub-agent finished successfully (terminal). Summarize the `result` ' +
|
|
55
|
+
'for the user in your own reply. Do NOT end your turn with no text.'
|
|
56
|
+
);
|
|
57
|
+
case 'closed':
|
|
58
|
+
return (
|
|
59
|
+
'Sub-agent was closed (terminal). Report the final `result` to the ' +
|
|
60
|
+
'user in your reply. Do NOT end your turn silently.'
|
|
61
|
+
);
|
|
62
|
+
case 'failed':
|
|
63
|
+
return (
|
|
64
|
+
'Sub-agent failed — see `error`. Decide whether to retry with a fresh ' +
|
|
65
|
+
'SpawnAgent, adjust the mission, or report the failure to the user. ' +
|
|
66
|
+
'Do NOT end your turn silently.'
|
|
67
|
+
);
|
|
68
|
+
default:
|
|
69
|
+
return (
|
|
70
|
+
'Decide what to do next: PromptAgent to send follow-up work, ' +
|
|
71
|
+
'CloseAgent to finalize, or WaitAgent again. Always tell the user ' +
|
|
72
|
+
'what just happened — do NOT end your turn silently.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
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
|
+
function errorNextSteps() {
|
|
85
|
+
return (
|
|
86
|
+
'That call failed — see `error`. Either correct the arguments and retry, ' +
|
|
87
|
+
'or tell the user what went wrong. Do NOT end your turn silently after an ' +
|
|
88
|
+
'error envelope; the user has not seen the error, only you have.'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
8
92
|
export default defineTool({
|
|
9
93
|
name: 'WaitAgent',
|
|
10
|
-
description: `Wait for a sub-agent to complete its
|
|
94
|
+
description: `Wait for a sub-agent to complete its current turn and retrieve its reply.
|
|
95
|
+
|
|
96
|
+
Returns a JSON envelope with the sub-agent's status, latest \`result\` text, and
|
|
97
|
+
an explicit \`next_steps\` field telling you what to do next. Read \`next_steps\`
|
|
98
|
+
every time — the wait is part of an orchestration loop, not a terminal answer.
|
|
11
99
|
|
|
12
|
-
|
|
13
|
-
|
|
100
|
+
CRITICAL — after WaitAgent returns you MUST take one of these actions:
|
|
101
|
+
• status='idle' / 'completed' / 'closed': RELAY the \`result\` to the user
|
|
102
|
+
in your own words (or send follow-up work via PromptAgent, or finalize
|
|
103
|
+
via CloseAgent).
|
|
104
|
+
• status='failed': report the failure to the user OR retry with a fresh
|
|
105
|
+
SpawnAgent.
|
|
106
|
+
• timedOut=true: call WaitAgent again with a larger timeout, OR CloseAgent
|
|
107
|
+
to cut it short, OR tell the user the agent is still working.
|
|
108
|
+
|
|
109
|
+
NEVER end your turn silently right after WaitAgent — the user has not seen the
|
|
110
|
+
sub-agent's reply yet; only you have. The orchestration loop is
|
|
111
|
+
SpawnAgent → (PromptAgent ↔ WaitAgent)+ → CloseAgent → final reply to user.
|
|
14
112
|
|
|
15
113
|
The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
16
114
|
parameters: {
|
|
@@ -34,21 +132,29 @@ The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
|
34
132
|
isReadOnly: () => true,
|
|
35
133
|
async execute(input, ctx) {
|
|
36
134
|
const { agent_id, timeout_ms = 30000 } = input;
|
|
37
|
-
|
|
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
|
+
if (!agent_id) {
|
|
141
|
+
return JSON.stringify({ next_steps: errorNextSteps(), error: 'agent_id is required' });
|
|
142
|
+
}
|
|
38
143
|
if (typeof timeout_ms !== 'number' || !Number.isFinite(timeout_ms) || timeout_ms < 0 || timeout_ms > 300000) {
|
|
39
|
-
return JSON.stringify({ error: 'timeout_ms must be a number between 0 and 300000' });
|
|
144
|
+
return JSON.stringify({ next_steps: errorNextSteps(), error: 'timeout_ms must be a number between 0 and 300000' });
|
|
40
145
|
}
|
|
41
146
|
|
|
42
147
|
const agents = getAgentRegistry();
|
|
43
148
|
const agent = agents.get(agent_id);
|
|
44
149
|
|
|
45
150
|
if (!agent) {
|
|
46
|
-
return JSON.stringify({ error: `Agent not found: ${agent_id}` });
|
|
151
|
+
return JSON.stringify({ next_steps: errorNextSteps(), error: `Agent not found: ${agent_id}` });
|
|
47
152
|
}
|
|
48
153
|
|
|
49
|
-
//
|
|
154
|
+
// Terminal states return immediately.
|
|
50
155
|
if (agent.status === 'completed' || agent.status === 'closed' || agent.status === 'failed') {
|
|
51
156
|
return JSON.stringify({
|
|
157
|
+
next_steps: nextStepsFor(agent.status),
|
|
52
158
|
agentId: agent_id,
|
|
53
159
|
name: agent.name,
|
|
54
160
|
status: agent.status,
|
|
@@ -59,13 +165,14 @@ The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
|
59
165
|
});
|
|
60
166
|
}
|
|
61
167
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
168
|
+
// 'idle' means the sub-agent finished its current turn and is waiting
|
|
169
|
+
// for the next SendMessage. That IS a useful return point for the
|
|
170
|
+
// parent — surface lastResult and let parent decide what's next.
|
|
65
171
|
const deadline = Date.now() + timeout_ms;
|
|
66
172
|
while (Date.now() < deadline) {
|
|
67
173
|
if (agent.status === 'idle' || agent.status === 'completed' || agent.status === 'closed' || agent.status === 'failed') {
|
|
68
174
|
return JSON.stringify({
|
|
175
|
+
next_steps: nextStepsFor(agent.status),
|
|
69
176
|
agentId: agent_id,
|
|
70
177
|
name: agent.name,
|
|
71
178
|
status: agent.status,
|
|
@@ -77,13 +184,14 @@ The default wait is 30000ms. Callers may request up to 300000ms (5 minutes).`,
|
|
|
77
184
|
}
|
|
78
185
|
|
|
79
186
|
if (ctx?.signal?.aborted) {
|
|
80
|
-
return JSON.stringify({ error: 'Wait cancelled', agentId: agent_id });
|
|
187
|
+
return JSON.stringify({ next_steps: errorNextSteps(), error: 'Wait cancelled', agentId: agent_id });
|
|
81
188
|
}
|
|
82
189
|
|
|
83
190
|
await new Promise(r => setTimeout(r, 200));
|
|
84
191
|
}
|
|
85
192
|
|
|
86
193
|
return JSON.stringify({
|
|
194
|
+
next_steps: nextStepsFor(agent.status, true),
|
|
87
195
|
agentId: agent_id,
|
|
88
196
|
name: agent.name,
|
|
89
197
|
status: agent.status,
|