@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.
Files changed (40) hide show
  1. package/README.md +4 -5
  2. package/bin/core/AgentCore.d.ts +2 -1
  3. package/bin/core/AgentCore.d.ts.map +1 -1
  4. package/bin/core/AgentCore.js +15 -7
  5. package/bin/core/AgentCore.js.map +1 -1
  6. package/bin/core/AgentCoreTypes.d.ts +7 -3
  7. package/bin/core/AgentCoreTypes.d.ts.map +1 -1
  8. package/bin/runtime/server/http/Server.d.ts.map +1 -1
  9. package/bin/runtime/server/http/Server.js +9 -2
  10. package/bin/runtime/server/http/Server.js.map +1 -1
  11. package/bin/runtime/server/http/control/SessionRoutes.d.ts.map +1 -1
  12. package/bin/runtime/server/http/control/SessionRoutes.js +27 -0
  13. package/bin/runtime/server/http/control/SessionRoutes.js.map +1 -1
  14. package/bin/runtime/server/http/control/StreamBySession.d.ts +22 -0
  15. package/bin/runtime/server/http/control/StreamBySession.d.ts.map +1 -0
  16. package/bin/runtime/server/http/control/StreamBySession.js +135 -0
  17. package/bin/runtime/server/http/control/StreamBySession.js.map +1 -0
  18. package/bin/sdk/AgentSdkTypes.d.ts +12 -1
  19. package/bin/sdk/AgentSdkTypes.d.ts.map +1 -1
  20. package/bin/sdk/Session.d.ts +14 -0
  21. package/bin/sdk/Session.d.ts.map +1 -1
  22. package/bin/sdk/Session.js +32 -2
  23. package/bin/sdk/Session.js.map +1 -1
  24. package/bin/sdk/session/ServicePort.d.ts +4 -0
  25. package/bin/sdk/session/ServicePort.d.ts.map +1 -1
  26. package/bin/sdk/session/ServicePort.js +1 -0
  27. package/bin/sdk/session/ServicePort.js.map +1 -1
  28. package/bin/session/Executor.js +1 -1
  29. package/bin/session/Executor.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/core/AgentCore.ts +20 -11
  32. package/src/core/AgentCoreTypes.ts +7 -3
  33. package/src/runtime/server/http/Server.ts +11 -2
  34. package/src/runtime/server/http/control/SessionRoutes.ts +28 -0
  35. package/src/runtime/server/http/control/StreamBySession.ts +198 -0
  36. package/src/sdk/AgentSdkTypes.ts +13 -1
  37. package/src/sdk/Session.ts +40 -2
  38. package/src/sdk/session/ServicePort.ts +5 -0
  39. package/src/session/Executor.ts +1 -1
  40. 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
+ }
@@ -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
- * - 该钩子只在 session 首次创建时触发一次,适合做实例级默认装配。
119
+ * - 若同时传入 `model`,则会先写入默认模型,再执行这里的宿主覆写逻辑。
120
+ * - 该钩子对每个 session 只触发一次,适合做实例级默认装配。
121
+ * - 触发时机可能来自显式 `agent.session()`,也可能来自该 session 的首次执行入口。
110
122
  */
111
123
  configureSession?: (session: Session) => Promise<void> | void;
112
124
  }
@@ -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. Call session.set({ model }) first or let the host configure the session during creation.`,
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. Call session.set({ model }) first or let the host configure the session during creation.`,
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: () => {
@@ -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;