@downcity/agent 1.1.18 → 1.1.20
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/bin/runtime/server/http/Server.d.ts.map +1 -1
- package/bin/runtime/server/http/Server.js +9 -2
- package/bin/runtime/server/http/Server.js.map +1 -1
- package/bin/runtime/server/http/control/SessionRoutes.d.ts.map +1 -1
- package/bin/runtime/server/http/control/SessionRoutes.js +27 -0
- package/bin/runtime/server/http/control/SessionRoutes.js.map +1 -1
- package/bin/runtime/server/http/control/StreamBySession.d.ts +22 -0
- package/bin/runtime/server/http/control/StreamBySession.d.ts.map +1 -0
- package/bin/runtime/server/http/control/StreamBySession.js +135 -0
- package/bin/runtime/server/http/control/StreamBySession.js.map +1 -0
- package/bin/session/Executor.d.ts.map +1 -1
- package/bin/session/Executor.js +1 -1
- package/bin/session/Executor.js.map +1 -1
- package/bin/session/core-engine/CoreEngineSignals.d.ts +1 -1
- package/bin/session/core-engine/CoreEngineSignals.d.ts.map +1 -1
- package/bin/session/core-engine/CoreEngineSignals.js +23 -1
- package/bin/session/core-engine/CoreEngineSignals.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/server/http/Server.ts +11 -2
- package/src/runtime/server/http/control/SessionRoutes.ts +28 -0
- package/src/runtime/server/http/control/StreamBySession.ts +198 -0
- package/src/session/Executor.ts +4 -1
- package/src/session/core-engine/CoreEngineSignals.ts +23 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
toUiMessageTimeline,
|
|
31
31
|
} from "./Helpers.js";
|
|
32
32
|
import { executeBySessionId } from "./ExecuteBySession.js";
|
|
33
|
+
import { createControlSessionStreamResponse } from "./StreamBySession.js";
|
|
33
34
|
const CONSOLEUI_SESSION_ID = "consoleui-chat-main";
|
|
34
35
|
|
|
35
36
|
function normalizeSystemText(input: string | null | undefined): string {
|
|
@@ -411,4 +412,31 @@ export function registerControlSessionRoutes(
|
|
|
411
412
|
}
|
|
412
413
|
});
|
|
413
414
|
}
|
|
415
|
+
|
|
416
|
+
for (const routePath of buildControlRouteAliases("/sessions/:sessionId/stream")) {
|
|
417
|
+
app.post(routePath, async (c) => {
|
|
418
|
+
try {
|
|
419
|
+
const runtime = params.getAgentRuntime();
|
|
420
|
+
const sessionId = decodeMaybe(String(c.req.param("sessionId") || "").trim());
|
|
421
|
+
const body = (await c.req.json().catch(() => ({}))) as Partial<ControlSessionExecuteRequestBody>;
|
|
422
|
+
const instructions = String(body.instructions || "").trim();
|
|
423
|
+
if (!sessionId) {
|
|
424
|
+
return c.json({ success: false, error: "Missing sessionId" }, 400);
|
|
425
|
+
}
|
|
426
|
+
if (!instructions) {
|
|
427
|
+
return c.json({ success: false, error: "Missing instructions" }, 400);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return await createControlSessionStreamResponse({
|
|
431
|
+
agentState: runtime,
|
|
432
|
+
executionContext: params.getAgentContext(),
|
|
433
|
+
sessionId,
|
|
434
|
+
instructions,
|
|
435
|
+
attachments: Array.isArray(body.attachments) ? body.attachments : undefined,
|
|
436
|
+
});
|
|
437
|
+
} catch (error) {
|
|
438
|
+
return c.json({ success: false, error: String(error) }, 500);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
414
442
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control session 流式执行 helper。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - 仅用于单 agent control API 的本地 session 流式返回。
|
|
6
|
+
* - chatKey 命中的平台 chat 队列仍保持原有入队语义,不在这里伪造“流式完成”。
|
|
7
|
+
* - 输出协议复用 SDK `AgentSessionStreamEvent` 的 NDJSON 事件行,便于 CLI 直接消费。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AgentContext, SessionPort } from "@/core/AgentContextTypes.js";
|
|
11
|
+
import type { AgentRuntime } from "@/core/AgentCoreTypes.js";
|
|
12
|
+
import type {
|
|
13
|
+
ControlSessionExecuteAttachmentInput,
|
|
14
|
+
} from "@/runtime/server/http/control/types/ControlSessionExecute.js";
|
|
15
|
+
import { buildExecuteInputText } from "@/runtime/server/http/control/Helpers.js";
|
|
16
|
+
import { drainDeferredPersistedUserMessages } from "@session/SessionRunScope.js";
|
|
17
|
+
import { mapUiMessageChunkToSdkEvent } from "@/sdk/StreamEvents.js";
|
|
18
|
+
import type { AgentSessionStreamEvent } from "@/sdk/AgentSdkTypes.js";
|
|
19
|
+
import type {
|
|
20
|
+
SessionRunResult,
|
|
21
|
+
SessionUiMessageChunkCallback,
|
|
22
|
+
} from "@/session/types/SessionRun.js";
|
|
23
|
+
import { resolveDispatchTargetByChatKey } from "@/service/builtins/chat/runtime/ChatkeySend.js";
|
|
24
|
+
import {
|
|
25
|
+
pickLastSuccessfulChatSendText,
|
|
26
|
+
resolveAssistantMessageForPersistence,
|
|
27
|
+
} from "@/service/builtins/chat/runtime/UserVisibleText.js";
|
|
28
|
+
|
|
29
|
+
type StreamableSessionPort = SessionPort & {
|
|
30
|
+
/**
|
|
31
|
+
* 流式执行当前 session。
|
|
32
|
+
*
|
|
33
|
+
* 说明(中文)
|
|
34
|
+
* - `SessionPort` 对外接口当前未显式暴露 `onUiMessageChunkCallback`,
|
|
35
|
+
* 但本地 executor 已支持该回调,这里按本地 control runtime 语义做窄化使用。
|
|
36
|
+
*/
|
|
37
|
+
run(params: {
|
|
38
|
+
query: string;
|
|
39
|
+
onUiMessageChunkCallback?: SessionUiMessageChunkCallback;
|
|
40
|
+
}): Promise<SessionRunResult>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const NDJSON_CONTENT_TYPE = "application/x-ndjson; charset=utf-8";
|
|
44
|
+
|
|
45
|
+
function encodeNdjsonLine(
|
|
46
|
+
encoder: TextEncoder,
|
|
47
|
+
value: AgentSessionStreamEvent,
|
|
48
|
+
): Uint8Array {
|
|
49
|
+
return encoder.encode(`${JSON.stringify(value)}\n`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveStreamEventFromUiChunk(params: {
|
|
53
|
+
chunk: Parameters<NonNullable<SessionUiMessageChunkCallback>>[0];
|
|
54
|
+
toolNameByCallId: Map<string, string>;
|
|
55
|
+
}): AgentSessionStreamEvent | null {
|
|
56
|
+
const { chunk, toolNameByCallId } = params;
|
|
57
|
+
if (chunk.type === "tool-input-start") {
|
|
58
|
+
toolNameByCallId.set(chunk.toolCallId, chunk.toolName);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const event = mapUiMessageChunkToSdkEvent(chunk);
|
|
63
|
+
if (!event) return null;
|
|
64
|
+
|
|
65
|
+
if (event.type === "tool-call" || event.type === "tool-error") {
|
|
66
|
+
toolNameByCallId.set(event.toolCallId, event.toolName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
(event.type === "tool-result" || event.type === "tool-error") &&
|
|
71
|
+
event.toolName === "unknown"
|
|
72
|
+
) {
|
|
73
|
+
const toolName = toolNameByCallId.get(event.toolCallId);
|
|
74
|
+
return toolName ? { ...event, toolName } : event;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return event;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 为指定 session 创建流式执行响应。
|
|
82
|
+
*/
|
|
83
|
+
export async function createControlSessionStreamResponse(params: {
|
|
84
|
+
agentState: AgentRuntime;
|
|
85
|
+
executionContext: AgentContext;
|
|
86
|
+
sessionId: string;
|
|
87
|
+
instructions: string;
|
|
88
|
+
attachments?: ControlSessionExecuteAttachmentInput[];
|
|
89
|
+
}): Promise<Response> {
|
|
90
|
+
const sessionId = String(params.sessionId || "").trim();
|
|
91
|
+
const instructions = String(params.instructions || "").trim();
|
|
92
|
+
if (!sessionId) {
|
|
93
|
+
return Response.json({ success: false, error: "Missing sessionId" }, { status: 400 });
|
|
94
|
+
}
|
|
95
|
+
if (!instructions) {
|
|
96
|
+
return Response.json({ success: false, error: "Missing instructions" }, { status: 400 });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const executeInput = await buildExecuteInputText({
|
|
100
|
+
projectRoot: params.agentState.rootPath,
|
|
101
|
+
sessionId,
|
|
102
|
+
instructions,
|
|
103
|
+
attachments: params.attachments,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const dispatchTarget = await resolveDispatchTargetByChatKey({
|
|
107
|
+
context: params.executionContext,
|
|
108
|
+
chatKey: sessionId,
|
|
109
|
+
});
|
|
110
|
+
if (dispatchTarget) {
|
|
111
|
+
return Response.json(
|
|
112
|
+
{
|
|
113
|
+
success: false,
|
|
114
|
+
error:
|
|
115
|
+
"Streaming execute does not support queued chat sessions. Use the non-stream execute API for chatKey routes.",
|
|
116
|
+
},
|
|
117
|
+
{ status: 409 },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const session = params.agentState.getSession(sessionId) as StreamableSessionPort;
|
|
122
|
+
const encoder = new TextEncoder();
|
|
123
|
+
|
|
124
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
125
|
+
start(controller) {
|
|
126
|
+
void (async () => {
|
|
127
|
+
const toolNameByCallId = new Map<string, string>();
|
|
128
|
+
|
|
129
|
+
const pushEvent = (event: AgentSessionStreamEvent): void => {
|
|
130
|
+
controller.enqueue(encodeNdjsonLine(encoder, event));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await session.appendUserMessage({
|
|
135
|
+
text: executeInput,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result = await session.run({
|
|
139
|
+
query: executeInput,
|
|
140
|
+
onUiMessageChunkCallback: async (chunk) => {
|
|
141
|
+
const event = resolveStreamEventFromUiChunk({
|
|
142
|
+
chunk,
|
|
143
|
+
toolNameByCallId,
|
|
144
|
+
});
|
|
145
|
+
if (!event) return;
|
|
146
|
+
pushEvent(event);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const userVisible = pickLastSuccessfulChatSendText(
|
|
151
|
+
result.assistantMessage,
|
|
152
|
+
).trim();
|
|
153
|
+
try {
|
|
154
|
+
const messageForPersistence = resolveAssistantMessageForPersistence(
|
|
155
|
+
result.assistantMessage,
|
|
156
|
+
);
|
|
157
|
+
if (messageForPersistence) {
|
|
158
|
+
await session.appendAssistantMessage({
|
|
159
|
+
message: messageForPersistence,
|
|
160
|
+
fallbackText: userVisible,
|
|
161
|
+
extra: {
|
|
162
|
+
via: "tui_session_stream",
|
|
163
|
+
note: "assistant_message_missing",
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const deferredInjectedMessages = drainDeferredPersistedUserMessages(
|
|
168
|
+
sessionId,
|
|
169
|
+
);
|
|
170
|
+
for (const message of deferredInjectedMessages) {
|
|
171
|
+
await session.appendUserMessage({
|
|
172
|
+
message,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// ignore persistence follow-up errors after stream completion
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
pushEvent({
|
|
180
|
+
type: "error",
|
|
181
|
+
error: error instanceof Error ? error.message : String(error),
|
|
182
|
+
});
|
|
183
|
+
} finally {
|
|
184
|
+
controller.close();
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return new Response(stream, {
|
|
191
|
+
status: 200,
|
|
192
|
+
headers: {
|
|
193
|
+
"Content-Type": NDJSON_CONTENT_TYPE,
|
|
194
|
+
"Cache-Control": "no-cache, no-transform",
|
|
195
|
+
"X-Accel-Buffering": "no",
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
package/src/session/Executor.ts
CHANGED
|
@@ -617,7 +617,10 @@ export class Executor implements SessionExecutor {
|
|
|
617
617
|
textOnlyContinuationCount += 1;
|
|
618
618
|
incompleteResponseRecoveryCount = 0;
|
|
619
619
|
const continuationMessage = this.historyStore.userText({
|
|
620
|
-
text: buildTextOnlyContinuationNudge(
|
|
620
|
+
text: buildTextOnlyContinuationNudge(
|
|
621
|
+
textOnlyContinuationCount,
|
|
622
|
+
textOnlyContinuationReason,
|
|
623
|
+
),
|
|
621
624
|
metadata: {
|
|
622
625
|
sessionId,
|
|
623
626
|
extra: {
|
|
@@ -63,6 +63,18 @@ const TEXT_ONLY_CONTINUATION_PATTERNS: ReadonlyArray<{
|
|
|
63
63
|
name: string;
|
|
64
64
|
pattern: RegExp;
|
|
65
65
|
}> = [
|
|
66
|
+
{
|
|
67
|
+
name: "pseudo_tool_protocol_dsml",
|
|
68
|
+
pattern: /<\s*||DSML||(?:tool_calls|invoke|parameter)\b/i,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "pseudo_tool_protocol_ascii",
|
|
72
|
+
pattern: /<\s*(?:tool_calls?|function_call|invoke)\b/i,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "pseudo_tool_protocol_json",
|
|
76
|
+
pattern: /"tool_calls"\s*:\s*\[/i,
|
|
77
|
+
},
|
|
66
78
|
{ name: "zh_start_now", pattern: /我现在开始/ },
|
|
67
79
|
{ name: "zh_next_will", pattern: /接下来我会/ },
|
|
68
80
|
{ name: "zh_will_do", pattern: /我会(?:先|继续|开始|基于|按)/ },
|
|
@@ -405,8 +417,19 @@ export function detectTextOnlyContinuationReason(
|
|
|
405
417
|
*/
|
|
406
418
|
export function buildTextOnlyContinuationNudge(
|
|
407
419
|
continuationIndex: number,
|
|
420
|
+
reason?: string | null,
|
|
408
421
|
): string {
|
|
409
422
|
const round = Math.max(1, continuationIndex);
|
|
423
|
+
const normalizedReason = String(reason || "").trim();
|
|
424
|
+
if (normalizedReason.startsWith("pseudo_tool_protocol")) {
|
|
425
|
+
return [
|
|
426
|
+
`系统续跑提醒(第 ${round} 次):上一轮输出了伪工具调用协议文本,但没有产生真实工具调用。`,
|
|
427
|
+
"不要把 DSML、XML、JSON tool_calls 或任何工具协议当作普通正文输出。",
|
|
428
|
+
"如果确实需要工具,请立刻使用当前运行时提供的原生工具重新发起调用;只能使用实际可用的工具名。",
|
|
429
|
+
"如果目标工具不可用,请明确说明不可用,并改用可用工具完成任务或说明受阻原因。",
|
|
430
|
+
"只有在任务真正完成、明确受阻、或必须等待用户提供信息时才停止。",
|
|
431
|
+
].join("\n");
|
|
432
|
+
}
|
|
410
433
|
return [
|
|
411
434
|
`系统续跑提醒(第 ${round} 次):继续执行当前任务。`,
|
|
412
435
|
"不要只描述计划、下一步或“我接下来会做什么”。",
|