@ynhcj/xiaoyi-channel 0.0.189-next → 0.0.190-next
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/dist/index.js +17 -4
- package/dist/src/outbound.js +11 -55
- package/dist/src/subagent-wait-state.d.ts +13 -8
- package/dist/src/subagent-wait-state.js +22 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -181,14 +181,27 @@ function registerSubagentHooks(api) {
|
|
|
181
181
|
}
|
|
182
182
|
});
|
|
183
183
|
// subagent_ended: fires when a subagent run terminates (complete/error/killed).
|
|
184
|
-
//
|
|
185
|
-
//
|
|
184
|
+
// This is the PRIMARY delivery tracking mechanism. When all expected
|
|
185
|
+
// subagents have ended and parent has settled, we finalize the A2A session.
|
|
186
186
|
api.on("subagent_ended", async (event, ctx) => {
|
|
187
187
|
const requesterSessionKey = ctx?.requesterSessionKey;
|
|
188
188
|
if (!requesterSessionKey)
|
|
189
189
|
return;
|
|
190
|
-
markSubagentEnded(requesterSessionKey);
|
|
191
|
-
console.log(`[XY-SUBAGENT] ended, sessionKey=${event?.targetSessionKey?.slice(0, 30)}, outcome=${event?.outcome},
|
|
190
|
+
const transition = markSubagentEnded(requesterSessionKey);
|
|
191
|
+
console.log(`[XY-SUBAGENT] ended, sessionKey=${event?.targetSessionKey?.slice(0, 30)}, outcome=${event?.outcome}, complete=${transition?.isComplete ?? false}, shouldFinalize=${transition?.shouldFinalize ?? false}`);
|
|
192
|
+
if (transition?.shouldFinalize) {
|
|
193
|
+
// All subagents ended and parent already settled — finalize A2A session
|
|
194
|
+
const { resolveXYConfig } = await import("./src/config.js");
|
|
195
|
+
const { deliverSubagentFinalResult } = await import("./src/outbound.js");
|
|
196
|
+
const { getXYRuntime } = await import("./src/runtime.js");
|
|
197
|
+
const config = resolveXYConfig(getXYRuntime().config);
|
|
198
|
+
await deliverSubagentFinalResult({
|
|
199
|
+
config,
|
|
200
|
+
state: transition.state,
|
|
201
|
+
reason: "all-subagents-ended-after-parent-settled",
|
|
202
|
+
});
|
|
203
|
+
console.log(`[XY-SUBAGENT] Finalized A2A session after all subagents ended`);
|
|
204
|
+
}
|
|
192
205
|
});
|
|
193
206
|
}
|
|
194
207
|
function registerFullHooks(api) {
|
package/dist/src/outbound.js
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveXYConfig } from "./config.js";
|
|
|
6
6
|
import { XYFileUploadService } from "./file-upload.js";
|
|
7
7
|
import { XYPushService } from "./push.js";
|
|
8
8
|
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
9
|
-
import { getWaitState,
|
|
9
|
+
import { getWaitState, addCompletionText, clearWaitState, } from "./subagent-wait-state.js";
|
|
10
10
|
import { decrementTaskIdRef } from "./task-manager.js";
|
|
11
11
|
import { sendA2AResponse, sendStatusUpdate } from "./formatter.js";
|
|
12
12
|
import { savePushData } from "./utils/pushdata-manager.js";
|
|
@@ -139,67 +139,23 @@ export const xyOutbound = {
|
|
|
139
139
|
sendText: async ({ cfg, to, text, accountId }) => {
|
|
140
140
|
// Resolve configuration
|
|
141
141
|
const config = resolveXYConfig(cfg);
|
|
142
|
-
// ── Subagent completion
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
// which differs from wait state's taskId (no match → skip)
|
|
148
|
-
// - Subagent completions: ALS is null, resolveTarget enhanced
|
|
149
|
-
// from wait state's taskId (match → intercept)
|
|
150
|
-
// - Cron: to=DEFAULT_PUSH_MARKER (no match)
|
|
151
|
-
// - Message tool: ALS enhances with current taskId (no match
|
|
152
|
-
// unless the agent happens to be on the same yielded task)
|
|
142
|
+
// ── Subagent completion text capture ─────────────────────────
|
|
143
|
+
// Subagent completions may arrive here when openclaw's announce
|
|
144
|
+
// flow delivers through the channel outbound. We capture the text
|
|
145
|
+
// for the final A2A response but do NOT track delivery count here.
|
|
146
|
+
// Delivery tracking is done by the subagent_ended hook (index.ts).
|
|
153
147
|
if (to && to !== DEFAULT_PUSH_MARKER) {
|
|
154
148
|
const [targetSessionId, targetTaskId] = String(to).split("::");
|
|
155
149
|
const waitState = getWaitState(targetSessionId, targetTaskId);
|
|
156
150
|
const alsCtx = getCurrentSessionContext();
|
|
157
|
-
// Only intercept when ALS is null OR the target taskId matches
|
|
158
|
-
// the wait state (not the current ALS taskId). This ensures
|
|
159
|
-
// we don't intercept normal agent turn deliveries.
|
|
160
151
|
const isFromWaitState = waitState &&
|
|
161
152
|
(!alsCtx || alsCtx.taskId !== targetTaskId);
|
|
162
153
|
if (isFromWaitState && !waitState.finalizationClaimed) {
|
|
163
154
|
const log = logger.withContext(waitState.sessionId, waitState.taskId);
|
|
164
|
-
log.log(`[xyOutbound.sendText] Subagent completion
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (shouldFinalize) {
|
|
169
|
-
// All completions arrived and parent already settled → finalize
|
|
170
|
-
log.log(`[xyOutbound.sendText] Finalizing subagent results`);
|
|
171
|
-
await deliverSubagentFinalResult({ config, state, reason: "all-subagent-results-delivered-after-parent-settled", text: text });
|
|
172
|
-
// Subagent completion delivered via A2A, skip push
|
|
173
|
-
return {
|
|
174
|
-
channel: "xiaoyi-channel",
|
|
175
|
-
messageId: state.artifactId,
|
|
176
|
-
chatId: to,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
if (isComplete) {
|
|
180
|
-
// All completions arrived but parent hasn't settled yet → defer to onSettled
|
|
181
|
-
log.log(`[xyOutbound.sendText] All subagent results arrived before parent settled, deferring finalization`);
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
// Still waiting for more completions → send progress via A2A, skip push
|
|
185
|
-
log.log(`[xyOutbound.sendText] Subagent progress ${state.deliveredCompletions}/${state.expectedCompletions}`);
|
|
186
|
-
try {
|
|
187
|
-
await sendStatusUpdate({
|
|
188
|
-
config,
|
|
189
|
-
sessionId: state.sessionId,
|
|
190
|
-
taskId: state.taskId,
|
|
191
|
-
messageId: state.messageId,
|
|
192
|
-
text: `已收到子任务结果 ${state.deliveredCompletions}/${state.expectedCompletions},继续等待其他子任务~`,
|
|
193
|
-
state: "working",
|
|
194
|
-
useLatestTask: false,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
log.error(`[xyOutbound.sendText] Failed to send subagent progress status:`, err);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// Skip push delivery for subagent completions — only send via A2A
|
|
155
|
+
log.log(`[xyOutbound.sendText] Subagent completion text captured, len=${text?.length ?? 0}`);
|
|
156
|
+
// Store the text for later use in final response
|
|
157
|
+
addCompletionText(targetSessionId, targetTaskId, text);
|
|
158
|
+
// Skip push delivery — subagent completions go through A2A
|
|
203
159
|
return {
|
|
204
160
|
channel: "xiaoyi-channel",
|
|
205
161
|
messageId: waitState.artifactId,
|
|
@@ -207,7 +163,7 @@ export const xyOutbound = {
|
|
|
207
163
|
};
|
|
208
164
|
}
|
|
209
165
|
}
|
|
210
|
-
// ── End subagent
|
|
166
|
+
// ── End subagent text capture ───────────────────────────────
|
|
211
167
|
// Handle default push marker (for cron jobs without explicit target)
|
|
212
168
|
let actualTo = to;
|
|
213
169
|
if (to === DEFAULT_PUSH_MARKER) {
|
|
@@ -41,20 +41,25 @@ export declare function unregisterSessionKeyMapping(sessionKey: string): void;
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function markSubagentSpawned(sessionKey: string): number;
|
|
43
43
|
/**
|
|
44
|
-
* Called from subagent_ended hook. Marks a subagent run as ended
|
|
45
|
-
*
|
|
44
|
+
* Called from subagent_ended hook. Marks a subagent run as ended AND
|
|
45
|
+
* counts it as a delivery. This is the primary completion tracking
|
|
46
|
+
* mechanism because subagent completions may not route through
|
|
47
|
+
* xyOutbound.sendText (they go through openclaw's internal gateway
|
|
48
|
+
* agent path with potentially deliver=false).
|
|
49
|
+
*
|
|
50
|
+
* Returns transition with shouldFinalize if all completions have ended.
|
|
46
51
|
*/
|
|
47
|
-
export declare function markSubagentEnded(sessionKey: string):
|
|
48
|
-
/**
|
|
49
|
-
* Called from xyOutbound.sendText when a subagent completion is delivered.
|
|
50
|
-
* Increments delivered count and returns transition state.
|
|
51
|
-
*/
|
|
52
|
-
export declare function markCompletionDelivered(sessionId: string, taskId: string, text?: string): SubagentWaitTransition | null;
|
|
52
|
+
export declare function markSubagentEnded(sessionKey: string): SubagentWaitTransition | null;
|
|
53
53
|
/**
|
|
54
54
|
* Called from bot.ts onSettled. Marks parent dispatcher as settled.
|
|
55
55
|
* Returns transition with shouldFinalize if all completions already arrived.
|
|
56
56
|
*/
|
|
57
57
|
export declare function markParentSettled(sessionId: string, taskId: string): SubagentWaitTransition | null;
|
|
58
|
+
/**
|
|
59
|
+
* Store a completion text snippet captured from xyOutbound.sendText.
|
|
60
|
+
* Does NOT increment delivery count (that's done by markSubagentEnded).
|
|
61
|
+
*/
|
|
62
|
+
export declare function addCompletionText(sessionId: string, taskId: string, text: string): void;
|
|
58
63
|
export declare function attachHeartbeat(sessionId: string, taskId: string, stopHeartbeat: () => void): void;
|
|
59
64
|
export declare function getWaitState(sessionId: string, taskId?: string): SubagentWaitState | null;
|
|
60
65
|
export declare function hasWaitState(sessionId: string, taskId?: string): boolean;
|
|
@@ -121,39 +121,30 @@ export function markSubagentSpawned(sessionKey) {
|
|
|
121
121
|
return state.expectedCompletions;
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
|
-
* Called from subagent_ended hook. Marks a subagent run as ended
|
|
125
|
-
*
|
|
124
|
+
* Called from subagent_ended hook. Marks a subagent run as ended AND
|
|
125
|
+
* counts it as a delivery. This is the primary completion tracking
|
|
126
|
+
* mechanism because subagent completions may not route through
|
|
127
|
+
* xyOutbound.sendText (they go through openclaw's internal gateway
|
|
128
|
+
* agent path with potentially deliver=false).
|
|
129
|
+
*
|
|
130
|
+
* Returns transition with shouldFinalize if all completions have ended.
|
|
126
131
|
*/
|
|
127
132
|
export function markSubagentEnded(sessionKey) {
|
|
128
133
|
const mapped = resolveSessionIdFromSessionKey(sessionKey);
|
|
129
134
|
if (!mapped)
|
|
130
|
-
return;
|
|
135
|
+
return null;
|
|
131
136
|
const { sessionId, taskId } = mapped;
|
|
132
|
-
const states = getStatesArray(sessionId);
|
|
133
|
-
const state = states.find((s) => s.taskId === taskId);
|
|
134
|
-
if (!state)
|
|
135
|
-
return;
|
|
136
|
-
logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Subagent ended, delivered=${state.deliveredCompletions}/${state.expectedCompletions}`);
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Called from xyOutbound.sendText when a subagent completion is delivered.
|
|
140
|
-
* Increments delivered count and returns transition state.
|
|
141
|
-
*/
|
|
142
|
-
export function markCompletionDelivered(sessionId, taskId, text) {
|
|
143
137
|
const states = getStatesArray(sessionId);
|
|
144
138
|
const state = states.find((s) => s.taskId === taskId);
|
|
145
139
|
if (!state) {
|
|
146
|
-
logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT]
|
|
140
|
+
logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Subagent ended but no wait state found`);
|
|
147
141
|
return null;
|
|
148
142
|
}
|
|
149
143
|
state.deliveredCompletions += 1;
|
|
150
|
-
if (text?.trim()) {
|
|
151
|
-
state.completionTexts.push(text.trim());
|
|
152
|
-
}
|
|
153
144
|
const complete = isComplete(state);
|
|
154
145
|
const shouldFinalize = claimFinalizationIfReady(state);
|
|
155
146
|
waitStates.set(sessionId, states);
|
|
156
|
-
logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT]
|
|
147
|
+
logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Subagent ended, delivered=${state.deliveredCompletions}/${state.expectedCompletions}, complete=${complete}, shouldFinalize=${shouldFinalize}`);
|
|
157
148
|
return { state, isComplete: complete, shouldFinalize };
|
|
158
149
|
}
|
|
159
150
|
/**
|
|
@@ -174,6 +165,18 @@ export function markParentSettled(sessionId, taskId) {
|
|
|
174
165
|
logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Parent settled, completions=${state.deliveredCompletions}/${state.expectedCompletions}, shouldFinalize=${shouldFinalize}`);
|
|
175
166
|
return { state, isComplete: complete, shouldFinalize };
|
|
176
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Store a completion text snippet captured from xyOutbound.sendText.
|
|
170
|
+
* Does NOT increment delivery count (that's done by markSubagentEnded).
|
|
171
|
+
*/
|
|
172
|
+
export function addCompletionText(sessionId, taskId, text) {
|
|
173
|
+
const states = getStatesArray(sessionId);
|
|
174
|
+
const state = states.find((s) => s.taskId === taskId);
|
|
175
|
+
if (!state || !text.trim())
|
|
176
|
+
return;
|
|
177
|
+
state.completionTexts.push(text.trim());
|
|
178
|
+
waitStates.set(sessionId, states);
|
|
179
|
+
}
|
|
177
180
|
export function attachHeartbeat(sessionId, taskId, stopHeartbeat) {
|
|
178
181
|
const states = getStatesArray(sessionId);
|
|
179
182
|
const state = states.find((s) => s.taskId === taskId);
|