@downcity/agent 1.1.17 → 1.1.19
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/README.md +4 -5
- package/bin/core/AgentCore.d.ts +2 -1
- package/bin/core/AgentCore.d.ts.map +1 -1
- package/bin/core/AgentCore.js +15 -7
- package/bin/core/AgentCore.js.map +1 -1
- package/bin/core/AgentCoreTypes.d.ts +7 -3
- package/bin/core/AgentCoreTypes.d.ts.map +1 -1
- 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/sdk/AgentSdkTypes.d.ts +12 -1
- package/bin/sdk/AgentSdkTypes.d.ts.map +1 -1
- package/bin/sdk/Session.d.ts +14 -0
- package/bin/sdk/Session.d.ts.map +1 -1
- package/bin/sdk/Session.js +32 -2
- package/bin/sdk/Session.js.map +1 -1
- package/bin/sdk/session/ServicePort.d.ts +4 -0
- package/bin/sdk/session/ServicePort.d.ts.map +1 -1
- package/bin/sdk/session/ServicePort.js +1 -0
- package/bin/sdk/session/ServicePort.js.map +1 -1
- package/bin/session/Executor.js +1 -1
- package/bin/session/Executor.js.map +1 -1
- package/package.json +1 -1
- package/src/core/AgentCore.ts +20 -11
- package/src/core/AgentCoreTypes.ts +7 -3
- 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/sdk/AgentSdkTypes.ts +13 -1
- package/src/sdk/Session.ts +40 -2
- package/src/sdk/session/ServicePort.ts +5 -0
- package/src/session/Executor.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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/sdk/AgentSdkTypes.ts
CHANGED
|
@@ -54,6 +54,16 @@ export interface AgentOptions {
|
|
|
54
54
|
*/
|
|
55
55
|
instruction?: string | string[];
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* 当前 agent 为新建 session 提供的默认模型实例。
|
|
59
|
+
*
|
|
60
|
+
* 关键点(中文)
|
|
61
|
+
* - SDK 仍不负责“选择哪个模型”,这里只接收宿主已经创建好的 `LanguageModel`。
|
|
62
|
+
* - 该模型会作为 session 首次执行前的默认注入值。
|
|
63
|
+
* - 若同时提供 `configureSession`,则先应用这里的默认模型,再允许宿主继续覆写。
|
|
64
|
+
*/
|
|
65
|
+
model?: LanguageModel;
|
|
66
|
+
|
|
57
67
|
/**
|
|
58
68
|
* 当前 agent 显式持有的 service 实例集合。
|
|
59
69
|
*
|
|
@@ -106,7 +116,9 @@ export interface AgentOptions {
|
|
|
106
116
|
*
|
|
107
117
|
* 关键点(中文)
|
|
108
118
|
* - SDK 不负责默认模型策略,宿主可在这里统一为 session 注入 model 等运行配置。
|
|
109
|
-
* -
|
|
119
|
+
* - 若同时传入 `model`,则会先写入默认模型,再执行这里的宿主覆写逻辑。
|
|
120
|
+
* - 该钩子对每个 session 只触发一次,适合做实例级默认装配。
|
|
121
|
+
* - 触发时机可能来自显式 `agent.session()`,也可能来自该 session 的首次执行入口。
|
|
110
122
|
*/
|
|
111
123
|
configureSession?: (session: Session) => Promise<void> | void;
|
|
112
124
|
}
|
package/src/sdk/Session.ts
CHANGED
|
@@ -92,6 +92,15 @@ type SessionOptions = {
|
|
|
92
92
|
* 读取当前 agent 显式注册 plugin 的 system blocks。
|
|
93
93
|
*/
|
|
94
94
|
getPluginSystemBlocks: () => Promise<AgentSessionSystemBlock[]>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 在执行前确保当前 session 已完成宿主侧默认配置。
|
|
98
|
+
*
|
|
99
|
+
* 关键点(中文)
|
|
100
|
+
* - 这里通常由 `AgentCore` 注入,用于补齐默认 model、宿主覆写等一次性装配。
|
|
101
|
+
* - 所有执行入口都应通过这里兜底,避免只在 SDK `agent.session()` 链路上做配置。
|
|
102
|
+
*/
|
|
103
|
+
ensureConfigured?: (session: Session) => Promise<void>;
|
|
95
104
|
};
|
|
96
105
|
|
|
97
106
|
/**
|
|
@@ -107,6 +116,7 @@ export class Session {
|
|
|
107
116
|
private readonly getInstructionSystemBlocks: SessionOptions["getInstructionSystemBlocks"];
|
|
108
117
|
private readonly getServiceSystemBlocks: SessionOptions["getServiceSystemBlocks"];
|
|
109
118
|
private readonly getPluginSystemBlocks: SessionOptions["getPluginSystemBlocks"];
|
|
119
|
+
private readonly ensureConfiguredHook?: SessionOptions["ensureConfigured"];
|
|
110
120
|
private readonly historyStore: JsonlSessionHistoryStore;
|
|
111
121
|
private readonly historyComposer: JsonlSessionHistoryComposer;
|
|
112
122
|
private readonly executor: Executor;
|
|
@@ -114,6 +124,7 @@ export class Session {
|
|
|
114
124
|
private createdAt = Date.now();
|
|
115
125
|
private timezone = resolveSystemTimezone();
|
|
116
126
|
private initializePromise: Promise<this> | null = null;
|
|
127
|
+
private ensureConfiguredPromise: Promise<void> | null = null;
|
|
117
128
|
private servicePort: SessionPort | null = null;
|
|
118
129
|
|
|
119
130
|
constructor(options: SessionOptions) {
|
|
@@ -125,6 +136,7 @@ export class Session {
|
|
|
125
136
|
this.getInstructionSystemBlocks = options.getInstructionSystemBlocks;
|
|
126
137
|
this.getServiceSystemBlocks = options.getServiceSystemBlocks;
|
|
127
138
|
this.getPluginSystemBlocks = options.getPluginSystemBlocks;
|
|
139
|
+
this.ensureConfiguredHook = options.ensureConfigured;
|
|
128
140
|
if (!this.id) {
|
|
129
141
|
throw new Error("Session requires a non-empty sessionId");
|
|
130
142
|
}
|
|
@@ -338,9 +350,10 @@ export class Session {
|
|
|
338
350
|
if (!query) {
|
|
339
351
|
throw new Error("session.run requires a non-empty query");
|
|
340
352
|
}
|
|
353
|
+
await this.ensureReadyForExecution();
|
|
341
354
|
if (!this.sessionConfig.model) {
|
|
342
355
|
throw new Error(
|
|
343
|
-
`Session "${this.id}" requires a configured model.
|
|
356
|
+
`Session "${this.id}" requires a configured model. Pass model to new Agent({ model }), call session.set({ model }) first, or let the host configure the session during creation.`,
|
|
344
357
|
);
|
|
345
358
|
}
|
|
346
359
|
await this.appendUserMessage({ text: query });
|
|
@@ -366,9 +379,10 @@ export class Session {
|
|
|
366
379
|
if (!query) {
|
|
367
380
|
throw new Error("session.stream requires a non-empty query");
|
|
368
381
|
}
|
|
382
|
+
await this.ensureReadyForExecution();
|
|
369
383
|
if (!this.sessionConfig.model) {
|
|
370
384
|
throw new Error(
|
|
371
|
-
`Session "${this.id}" requires a configured model.
|
|
385
|
+
`Session "${this.id}" requires a configured model. Pass model to new Agent({ model }), call session.set({ model }) first, or let the host configure the session during creation.`,
|
|
372
386
|
);
|
|
373
387
|
}
|
|
374
388
|
const queue = new AsyncQueue<AgentSessionStreamEvent>();
|
|
@@ -478,6 +492,9 @@ export class Session {
|
|
|
478
492
|
sessionId: this.id,
|
|
479
493
|
executor: this.executor,
|
|
480
494
|
historyStore: this.historyStore,
|
|
495
|
+
ensureReadyForExecution: async () => {
|
|
496
|
+
await this.ensureReadyForExecution();
|
|
497
|
+
},
|
|
481
498
|
touchMetadata: async () => {
|
|
482
499
|
await this.touchMetadata();
|
|
483
500
|
},
|
|
@@ -485,6 +502,27 @@ export class Session {
|
|
|
485
502
|
return this.servicePort;
|
|
486
503
|
}
|
|
487
504
|
|
|
505
|
+
/**
|
|
506
|
+
* 在执行前确保 session 已完成初始化与宿主装配。
|
|
507
|
+
*/
|
|
508
|
+
async ensureReadyForExecution(): Promise<void> {
|
|
509
|
+
await this.initialize();
|
|
510
|
+
if (this.ensureConfiguredPromise) {
|
|
511
|
+
await this.ensureConfiguredPromise;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
this.ensureConfiguredPromise = (async () => {
|
|
515
|
+
if (!this.ensureConfiguredHook) return;
|
|
516
|
+
await this.ensureConfiguredHook(this);
|
|
517
|
+
})();
|
|
518
|
+
try {
|
|
519
|
+
await this.ensureConfiguredPromise;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
this.ensureConfiguredPromise = null;
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
488
526
|
private async touchMetadata(): Promise<void> {
|
|
489
527
|
await touchSessionMetadata({
|
|
490
528
|
projectRoot: this.projectRoot,
|
|
@@ -25,6 +25,10 @@ export interface CreateSessionServicePortParams {
|
|
|
25
25
|
* 当前 session 历史持久化端口。
|
|
26
26
|
*/
|
|
27
27
|
historyStore: SessionHistoryStore;
|
|
28
|
+
/**
|
|
29
|
+
* 在执行前确保当前 session 已完成初始化与宿主级配置。
|
|
30
|
+
*/
|
|
31
|
+
ensureReadyForExecution: () => Promise<void>;
|
|
28
32
|
/**
|
|
29
33
|
* session 更新后需要同步执行的持久化回调。
|
|
30
34
|
*/
|
|
@@ -42,6 +46,7 @@ export function createSessionServicePort(
|
|
|
42
46
|
getExecutor: () => params.executor.getExecutor(),
|
|
43
47
|
getHistoryStore: () => params.historyStore,
|
|
44
48
|
run: async (runParams) => {
|
|
49
|
+
await params.ensureReadyForExecution();
|
|
45
50
|
return await params.executor.run(runParams);
|
|
46
51
|
},
|
|
47
52
|
clearExecutor: () => {
|
package/src/session/Executor.ts
CHANGED
|
@@ -717,7 +717,7 @@ export class Executor implements SessionExecutor {
|
|
|
717
717
|
const model = this.getModel();
|
|
718
718
|
if (!model) {
|
|
719
719
|
throw new Error(
|
|
720
|
-
`Executor for session "${this.sessionId}" requires a configured model
|
|
720
|
+
`Executor for session "${this.sessionId}" requires a configured model. Pass model to new Agent({ model }), call session.set({ model }), or let the host configure the session before execution.`,
|
|
721
721
|
);
|
|
722
722
|
}
|
|
723
723
|
return model;
|