@downcity/agent 1.1.21 → 1.1.23

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 (116) hide show
  1. package/README.md +6 -5
  2. package/bin/core/AgentContextTypes.d.ts +21 -2
  3. package/bin/core/AgentContextTypes.d.ts.map +1 -1
  4. package/bin/index.d.ts +5 -1
  5. package/bin/index.d.ts.map +1 -1
  6. package/bin/index.js.map +1 -1
  7. package/bin/runtime/server/http/Server.d.ts +1 -1
  8. package/bin/runtime/server/http/Server.d.ts.map +1 -1
  9. package/bin/runtime/server/http/Server.js +4 -0
  10. package/bin/runtime/server/http/Server.js.map +1 -1
  11. package/bin/runtime/server/http/control/ExecuteBySession.js +1 -1
  12. package/bin/runtime/server/http/control/ExecuteBySession.js.map +1 -1
  13. package/bin/runtime/server/http/control/StreamBySession.d.ts +3 -3
  14. package/bin/runtime/server/http/control/StreamBySession.js +6 -6
  15. package/bin/runtime/server/http/control/StreamBySession.js.map +1 -1
  16. package/bin/runtime/server/http/execute/execute.js +1 -1
  17. package/bin/runtime/server/http/execute/execute.js.map +1 -1
  18. package/bin/runtime/server/http/sdk/Router.d.ts +14 -0
  19. package/bin/runtime/server/http/sdk/Router.d.ts.map +1 -0
  20. package/bin/runtime/server/http/sdk/Router.js +18 -0
  21. package/bin/runtime/server/http/sdk/Router.js.map +1 -0
  22. package/bin/runtime/server/http/sdk/SessionRoutes.d.ts +15 -0
  23. package/bin/runtime/server/http/sdk/SessionRoutes.d.ts.map +1 -0
  24. package/bin/runtime/server/http/sdk/SessionRoutes.js +190 -0
  25. package/bin/runtime/server/http/sdk/SessionRoutes.js.map +1 -0
  26. package/bin/sdk/AgentSdkTypes.d.ts +1 -124
  27. package/bin/sdk/AgentSdkTypes.d.ts.map +1 -1
  28. package/bin/sdk/RemoteAgent.d.ts +27 -8
  29. package/bin/sdk/RemoteAgent.d.ts.map +1 -1
  30. package/bin/sdk/RemoteAgent.js +266 -38
  31. package/bin/sdk/RemoteAgent.js.map +1 -1
  32. package/bin/sdk/Session.d.ts +31 -10
  33. package/bin/sdk/Session.d.ts.map +1 -1
  34. package/bin/sdk/Session.js +181 -73
  35. package/bin/sdk/Session.js.map +1 -1
  36. package/bin/sdk/SessionEventMapper.d.ts +32 -0
  37. package/bin/sdk/SessionEventMapper.d.ts.map +1 -0
  38. package/bin/sdk/{StreamEvents.js → SessionEventMapper.js} +43 -28
  39. package/bin/sdk/SessionEventMapper.js.map +1 -0
  40. package/bin/sdk/session/ServicePort.d.ts +51 -4
  41. package/bin/sdk/session/ServicePort.d.ts.map +1 -1
  42. package/bin/sdk/session/ServicePort.js +17 -10
  43. package/bin/sdk/session/ServicePort.js.map +1 -1
  44. package/bin/sdk/session/runtime/SessionEventHub.d.ts +24 -0
  45. package/bin/sdk/session/runtime/SessionEventHub.d.ts.map +1 -0
  46. package/bin/sdk/session/runtime/SessionEventHub.js +39 -0
  47. package/bin/sdk/session/runtime/SessionEventHub.js.map +1 -0
  48. package/bin/sdk/session/runtime/SessionPromptRuntime.d.ts +68 -0
  49. package/bin/sdk/session/runtime/SessionPromptRuntime.d.ts.map +1 -0
  50. package/bin/sdk/session/runtime/SessionPromptRuntime.js +170 -0
  51. package/bin/sdk/session/runtime/SessionPromptRuntime.js.map +1 -0
  52. package/bin/service/builtins/chat/runtime/ChatQueueWorker.js +1 -1
  53. package/bin/service/builtins/chat/runtime/ChatQueueWorker.js.map +1 -1
  54. package/bin/service/builtins/chat/runtime/ChatSession.d.ts +1 -1
  55. package/bin/service/builtins/chat/runtime/ChatSession.js +1 -1
  56. package/bin/service/builtins/contact/runtime/ChatRuntime.js +1 -1
  57. package/bin/service/builtins/contact/runtime/ChatRuntime.js.map +1 -1
  58. package/bin/session/Executor.d.ts.map +1 -1
  59. package/bin/session/Executor.js +1 -1
  60. package/bin/session/Executor.js.map +1 -1
  61. package/bin/session/core-engine/CoreEngineSignals.d.ts +1 -1
  62. package/bin/session/core-engine/CoreEngineSignals.d.ts.map +1 -1
  63. package/bin/session/core-engine/CoreEngineSignals.js +1 -23
  64. package/bin/session/core-engine/CoreEngineSignals.js.map +1 -1
  65. package/bin/types/sdk/AgentSessionEvent.d.ts +160 -0
  66. package/bin/types/sdk/AgentSessionEvent.d.ts.map +1 -0
  67. package/bin/types/sdk/AgentSessionEvent.js +9 -0
  68. package/bin/types/sdk/AgentSessionEvent.js.map +1 -0
  69. package/bin/types/sdk/AgentSessionPrompt.d.ts +21 -0
  70. package/bin/types/sdk/AgentSessionPrompt.d.ts.map +1 -0
  71. package/bin/types/sdk/AgentSessionPrompt.js +9 -0
  72. package/bin/types/sdk/AgentSessionPrompt.js.map +1 -0
  73. package/bin/types/sdk/AgentSessionTurn.d.ts +59 -0
  74. package/bin/types/sdk/AgentSessionTurn.d.ts.map +1 -0
  75. package/bin/types/sdk/AgentSessionTurn.js +10 -0
  76. package/bin/types/sdk/AgentSessionTurn.js.map +1 -0
  77. package/bin/types/sdk/AgentUiChunkEvent.d.ts +100 -0
  78. package/bin/types/sdk/AgentUiChunkEvent.d.ts.map +1 -0
  79. package/bin/types/sdk/AgentUiChunkEvent.js +9 -0
  80. package/bin/types/sdk/AgentUiChunkEvent.js.map +1 -0
  81. package/package.json +2 -1
  82. package/scripts/session-prompt-runtime.test.mjs +167 -0
  83. package/src/core/AgentContextTypes.ts +24 -2
  84. package/src/index.ts +11 -3
  85. package/src/runtime/server/http/Server.ts +5 -1
  86. package/src/runtime/server/http/control/ExecuteBySession.ts +1 -1
  87. package/src/runtime/server/http/control/StreamBySession.ts +11 -11
  88. package/src/runtime/server/http/execute/execute.ts +1 -1
  89. package/src/runtime/server/http/sdk/Router.ts +22 -0
  90. package/src/runtime/server/http/sdk/SessionRoutes.ts +232 -0
  91. package/src/sdk/AgentSdkTypes.ts +1 -137
  92. package/src/sdk/RemoteAgent.ts +368 -49
  93. package/src/sdk/Session.ts +235 -86
  94. package/src/sdk/{StreamEvents.ts → SessionEventMapper.ts} +50 -40
  95. package/src/sdk/session/ServicePort.ts +74 -12
  96. package/src/sdk/session/runtime/SessionEventHub.ts +47 -0
  97. package/src/sdk/session/runtime/SessionPromptRuntime.ts +269 -0
  98. package/src/service/builtins/chat/runtime/ChatQueueWorker.ts +1 -1
  99. package/src/service/builtins/chat/runtime/ChatSession.ts +1 -1
  100. package/src/service/builtins/contact/runtime/ChatRuntime.ts +1 -1
  101. package/src/session/Executor.ts +1 -4
  102. package/src/session/README.md +8 -7
  103. package/src/session/core-engine/CoreEngineSignals.ts +0 -23
  104. package/src/types/sdk/AgentSessionEvent.ts +195 -0
  105. package/src/types/sdk/AgentSessionPrompt.ts +21 -0
  106. package/src/types/sdk/AgentSessionTurn.ts +65 -0
  107. package/src/types/sdk/AgentUiChunkEvent.ts +108 -0
  108. package/tsconfig.tsbuildinfo +1 -1
  109. package/bin/sdk/AsyncQueue.d.ts +0 -26
  110. package/bin/sdk/AsyncQueue.d.ts.map +0 -1
  111. package/bin/sdk/AsyncQueue.js +0 -75
  112. package/bin/sdk/AsyncQueue.js.map +0 -1
  113. package/bin/sdk/StreamEvents.d.ts +0 -36
  114. package/bin/sdk/StreamEvents.d.ts.map +0 -1
  115. package/bin/sdk/StreamEvents.js.map +0 -1
  116. package/src/sdk/AsyncQueue.ts +0 -92
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @file 验证 SessionPromptRuntime 的 actor 队列语义。
3
+ *
4
+ * 关键点(中文)
5
+ * - 测试编译后的 bin 输出,避免把测试文件混入 package 源码导出面。
6
+ * - 重点锁住 prompt merge 与排队到下一 turn 的行为,防止后续重构破坏交互模型。
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+
12
+ import { SessionPromptRuntime } from "../bin/sdk/session/runtime/SessionPromptRuntime.js";
13
+
14
+ function createDeferred() {
15
+ let resolve;
16
+ const promise = new Promise((innerResolve) => {
17
+ resolve = innerResolve;
18
+ });
19
+ return {
20
+ promise,
21
+ resolve,
22
+ };
23
+ }
24
+
25
+ function createUserMessage(query, index) {
26
+ return {
27
+ id: `u:test:${String(index)}`,
28
+ role: "user",
29
+ metadata: {
30
+ v: 1,
31
+ ts: Date.now(),
32
+ sessionId: "test",
33
+ source: "sdk",
34
+ kind: "normal",
35
+ },
36
+ parts: [{ type: "text", text: query }],
37
+ };
38
+ }
39
+
40
+ async function waitUntil(readValue) {
41
+ for (let index = 0; index < 20; index += 1) {
42
+ const value = readValue();
43
+ if (value) return value;
44
+ await new Promise((resolve) => setTimeout(resolve, 0));
45
+ }
46
+ throw new Error("Timed out while waiting for test condition");
47
+ }
48
+
49
+ async function isSettled(promise) {
50
+ const marker = {};
51
+ const result = await Promise.race([
52
+ promise.then(
53
+ () => true,
54
+ () => true,
55
+ ),
56
+ new Promise((resolve) => setTimeout(() => resolve(marker), 0)),
57
+ ]);
58
+ return result !== marker;
59
+ }
60
+
61
+ test("SessionPromptRuntime merges queued prompts at the next step boundary", async () => {
62
+ const events = [];
63
+ const persisted = [];
64
+ const executionFinished = createDeferred();
65
+ let stepMerge = null;
66
+
67
+ const runtime = new SessionPromptRuntime({
68
+ sessionId: "test",
69
+ publish: (event) => {
70
+ events.push(event);
71
+ },
72
+ createAndPersistUserMessage: async (query) => {
73
+ const message = createUserMessage(query, persisted.length + 1);
74
+ persisted.push(message);
75
+ return message;
76
+ },
77
+ executeTurn: async (input) => {
78
+ stepMerge = input.onStepMerge;
79
+ await executionFinished.promise;
80
+ return {
81
+ text: "done",
82
+ success: true,
83
+ };
84
+ },
85
+ });
86
+
87
+ const firstTurn = await runtime.prompt("first");
88
+ const secondTurnPromise = runtime.prompt("second");
89
+ const merge = await waitUntil(() => stepMerge);
90
+
91
+ assert.equal(await isSettled(secondTurnPromise), false);
92
+
93
+ const mergedMessages = await merge();
94
+ const secondTurn = await secondTurnPromise;
95
+
96
+ assert.equal(secondTurn.id, firstTurn.id);
97
+ assert.deepEqual(
98
+ mergedMessages.map((message) => message.parts[0]?.text),
99
+ ["second"],
100
+ );
101
+ assert.deepEqual(
102
+ persisted.map((message) => message.parts[0]?.text),
103
+ ["first", "second"],
104
+ );
105
+
106
+ executionFinished.resolve();
107
+ const result = await firstTurn.finished;
108
+
109
+ assert.equal(result.success, true);
110
+ assert.equal(result.text, "done");
111
+ assert.equal(secondTurn.result, result);
112
+ assert.deepEqual(
113
+ events.map((event) => event.type),
114
+ ["turn-start", "turn-finish"],
115
+ );
116
+ });
117
+
118
+ test("SessionPromptRuntime moves unmerged prompts into the next turn", async () => {
119
+ const events = [];
120
+ const finishQueue = [];
121
+
122
+ const runtime = new SessionPromptRuntime({
123
+ sessionId: "test",
124
+ publish: (event) => {
125
+ events.push(event);
126
+ },
127
+ createAndPersistUserMessage: async (query) => {
128
+ return createUserMessage(query, events.length + 1);
129
+ },
130
+ executeTurn: async (input) => {
131
+ const deferred = createDeferred();
132
+ finishQueue.push({
133
+ query: input.query,
134
+ deferred,
135
+ });
136
+ await deferred.promise;
137
+ return {
138
+ text: `done:${input.query}`,
139
+ success: true,
140
+ };
141
+ },
142
+ });
143
+
144
+ const firstTurn = await runtime.prompt("first");
145
+ const secondTurnPromise = runtime.prompt("second");
146
+ const firstExecution = await waitUntil(() => finishQueue[0]);
147
+
148
+ assert.equal(await isSettled(secondTurnPromise), false);
149
+
150
+ firstExecution.deferred.resolve();
151
+ const firstResult = await firstTurn.finished;
152
+ const secondTurn = await secondTurnPromise;
153
+ const secondExecution = await waitUntil(() => finishQueue[1]);
154
+
155
+ assert.notEqual(secondTurn.id, firstTurn.id);
156
+ assert.equal(firstResult.text, "done:first");
157
+ assert.equal(secondExecution.query, "second");
158
+
159
+ secondExecution.deferred.resolve();
160
+ const secondResult = await secondTurn.finished;
161
+
162
+ assert.equal(secondResult.text, "done:second");
163
+ assert.deepEqual(
164
+ events.map((event) => event.type),
165
+ ["turn-start", "turn-finish", "turn-start", "turn-finish"],
166
+ );
167
+ });
@@ -34,6 +34,12 @@ import type {
34
34
  SessionRunResult,
35
35
  } from "@/session/types/SessionRun.js";
36
36
  import type { SessionHistoryStore } from "@/session/store/history/SessionHistoryStore.js";
37
+ import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
38
+ import type {
39
+ AgentSessionSubscriber,
40
+ AgentSessionUnsubscribe,
41
+ } from "@/types/sdk/AgentSessionEvent.js";
42
+ import type { AgentSessionTurnHandle } from "@/types/sdk/AgentSessionTurn.js";
37
43
 
38
44
  /**
39
45
  * 跨 service 调用参数。
@@ -116,9 +122,9 @@ export interface SessionPort {
116
122
  */
117
123
  getHistoryStore(): SessionHistoryStore;
118
124
  /**
119
- * 执行当前 session 的一次请求。
125
+ * 执行当前 session 的一次内部请求。
120
126
  */
121
- run(params: {
127
+ execute(params: {
122
128
  /**
123
129
  * 本轮输入文本。
124
130
  */
@@ -132,6 +138,22 @@ export interface SessionPort {
132
138
  */
133
139
  onAssistantStepCallback?: SessionAssistantStepCallback;
134
140
  }): Promise<SessionRunResult>;
141
+ /**
142
+ * 向当前 session actor 追加一条新的 prompt。
143
+ *
144
+ * 关键点(中文)
145
+ * - 这是面向 SDK / transport 的统一交互输入入口。
146
+ * - 返回值只有在当前输入被绑定到某个 turn 后才会兑现。
147
+ */
148
+ prompt(input: AgentSessionPromptInput): Promise<AgentSessionTurnHandle>;
149
+ /**
150
+ * 订阅当前 session 后续产生的 future 事件。
151
+ *
152
+ * 关键点(中文)
153
+ * - 只广播订阅之后产生的事件。
154
+ * - 历史消息仍通过 `getHistoryStore()` / SDK `history()` 读取。
155
+ */
156
+ subscribe(subscriber: AgentSessionSubscriber): AgentSessionUnsubscribe;
135
157
  /**
136
158
  * 清理当前 session 的 executor 缓存。
137
159
  */
package/src/index.ts CHANGED
@@ -24,15 +24,23 @@ export type {
24
24
  AgentSessionConfigSnapshot,
25
25
  AgentSessionForkInput,
26
26
  AgentSessionMetadata,
27
- AgentSessionRunInput,
28
- AgentSessionRunResult,
29
27
  AgentSessionSetInput,
30
- AgentSessionStreamEvent,
31
28
  AgentSessionSystemBlock,
32
29
  AgentSessionSystemBlockSource,
33
30
  AgentSessionSystemSessionInfo,
34
31
  AgentSessionSystemSnapshot,
35
32
  } from "./sdk/AgentSdkTypes.js";
33
+ export type {
34
+ AgentSessionEvent,
35
+ AgentSessionSubscriber,
36
+ AgentSessionUnsubscribe,
37
+ } from "./types/sdk/AgentSessionEvent.js";
38
+ export type { AgentSessionPromptInput } from "./types/sdk/AgentSessionPrompt.js";
39
+ export type {
40
+ AgentSessionTurnHandle,
41
+ AgentSessionTurnResult,
42
+ } from "./types/sdk/AgentSessionTurn.js";
43
+ export type { AgentUiChunkEvent } from "./types/sdk/AgentUiChunkEvent.js";
36
44
  export type {
37
45
  AgentRuntime as AgentCoreRuntime,
38
46
  AgentRuntimeBase as AgentCoreRuntimeBase,
@@ -22,6 +22,7 @@ import {
22
22
  import { createPluginsRouter } from "@/runtime/server/http/plugins/plugins.js";
23
23
  import { createStaticRouter } from "@/runtime/server/http/static/static.js";
24
24
  import { createControlRouter } from "@/runtime/server/http/control/ControlRouter.js";
25
+ import { createSdkRouter } from "@/runtime/server/http/sdk/Router.js";
25
26
  import {
26
27
  registerBuiltinPluginHttpRoutes,
27
28
  } from "@/plugin/core/HttpRoutes.js";
@@ -38,7 +39,7 @@ export interface ServerStartOptions {
38
39
  /** HTTP 服务监听主机。 */
39
40
  host: string;
40
41
  /** 可选实例级 agent core。 */
41
- core?: Pick<AgentCore, "getContext" | "getRuntime">;
42
+ core?: Pick<AgentCore, "getContext" | "getRuntime" | "session" | "sessions">;
42
43
  /** 可选实例级 runtime 读取函数。 */
43
44
  getAgentRuntime?: () => AgentRuntime;
44
45
  /** 可选实例级 context 读取函数。 */
@@ -122,6 +123,9 @@ export function createServerApp(
122
123
  getAgentRuntime: bindings.getAgentRuntime,
123
124
  getAgentContext: bindings.getAgentContext,
124
125
  }));
126
+ if (options.core) {
127
+ app.route("/", createSdkRouter(options.core));
128
+ }
125
129
  registerBuiltinPluginHttpRoutes({
126
130
  app,
127
131
  getContext: bindings.getAgentContext,
@@ -112,7 +112,7 @@ export async function executeBySessionId(params: {
112
112
  text: executeInput,
113
113
  });
114
114
 
115
- const result = await session.run({
115
+ const result = await session.execute({
116
116
  query: executeInput,
117
117
  });
118
118
 
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Control session 流式执行 helper。
2
+ * Control session 增量执行 helper。
3
3
  *
4
4
  * 关键点(中文)
5
- * - 仅用于单 agent control API 的本地 session 流式返回。
5
+ * - 仅用于单 agent control API 的本地 session 增量返回。
6
6
  * - chatKey 命中的平台 chat 队列仍保持原有入队语义,不在这里伪造“流式完成”。
7
- * - 输出协议复用 SDK `AgentSessionStreamEvent` 的 NDJSON 事件行,便于 CLI 直接消费。
7
+ * - 输出协议使用内部 `AgentUiChunkEvent` 的 NDJSON 事件行,便于 CLI 直接消费。
8
8
  */
9
9
 
10
10
  import type { AgentContext, SessionPort } from "@/core/AgentContextTypes.js";
@@ -14,8 +14,8 @@ import type {
14
14
  } from "@/runtime/server/http/control/types/ControlSessionExecute.js";
15
15
  import { buildExecuteInputText } from "@/runtime/server/http/control/Helpers.js";
16
16
  import { drainDeferredPersistedUserMessages } from "@session/SessionRunScope.js";
17
- import { mapUiMessageChunkToSdkEvent } from "@/sdk/StreamEvents.js";
18
- import type { AgentSessionStreamEvent } from "@/sdk/AgentSdkTypes.js";
17
+ import { mapUiMessageChunkToAgentEvent } from "@/sdk/SessionEventMapper.js";
18
+ import type { AgentUiChunkEvent } from "@/types/sdk/AgentUiChunkEvent.js";
19
19
  import type {
20
20
  SessionRunResult,
21
21
  SessionUiMessageChunkCallback,
@@ -34,7 +34,7 @@ type StreamableSessionPort = SessionPort & {
34
34
  * - `SessionPort` 对外接口当前未显式暴露 `onUiMessageChunkCallback`,
35
35
  * 但本地 executor 已支持该回调,这里按本地 control runtime 语义做窄化使用。
36
36
  */
37
- run(params: {
37
+ execute(params: {
38
38
  query: string;
39
39
  onUiMessageChunkCallback?: SessionUiMessageChunkCallback;
40
40
  }): Promise<SessionRunResult>;
@@ -44,7 +44,7 @@ const NDJSON_CONTENT_TYPE = "application/x-ndjson; charset=utf-8";
44
44
 
45
45
  function encodeNdjsonLine(
46
46
  encoder: TextEncoder,
47
- value: AgentSessionStreamEvent,
47
+ value: AgentUiChunkEvent,
48
48
  ): Uint8Array {
49
49
  return encoder.encode(`${JSON.stringify(value)}\n`);
50
50
  }
@@ -52,14 +52,14 @@ function encodeNdjsonLine(
52
52
  function resolveStreamEventFromUiChunk(params: {
53
53
  chunk: Parameters<NonNullable<SessionUiMessageChunkCallback>>[0];
54
54
  toolNameByCallId: Map<string, string>;
55
- }): AgentSessionStreamEvent | null {
55
+ }): AgentUiChunkEvent | null {
56
56
  const { chunk, toolNameByCallId } = params;
57
57
  if (chunk.type === "tool-input-start") {
58
58
  toolNameByCallId.set(chunk.toolCallId, chunk.toolName);
59
59
  return null;
60
60
  }
61
61
 
62
- const event = mapUiMessageChunkToSdkEvent(chunk);
62
+ const event = mapUiMessageChunkToAgentEvent(chunk);
63
63
  if (!event) return null;
64
64
 
65
65
  if (event.type === "tool-call" || event.type === "tool-error") {
@@ -126,7 +126,7 @@ export async function createControlSessionStreamResponse(params: {
126
126
  void (async () => {
127
127
  const toolNameByCallId = new Map<string, string>();
128
128
 
129
- const pushEvent = (event: AgentSessionStreamEvent): void => {
129
+ const pushEvent = (event: AgentUiChunkEvent): void => {
130
130
  controller.enqueue(encodeNdjsonLine(encoder, event));
131
131
  };
132
132
 
@@ -135,7 +135,7 @@ export async function createControlSessionStreamResponse(params: {
135
135
  text: executeInput,
136
136
  });
137
137
 
138
- const result = await session.run({
138
+ const result = await session.execute({
139
139
  query: executeInput,
140
140
  onUiMessageChunkCallback: async (chunk) => {
141
141
  const event = resolveStreamEventFromUiChunk({
@@ -94,7 +94,7 @@ export function createExecuteRouter(
94
94
  text: String(instructions),
95
95
  });
96
96
 
97
- const result = await session.run({
97
+ const result = await session.execute({
98
98
  query: String(instructions),
99
99
  });
100
100
 
@@ -0,0 +1,22 @@
1
+ /**
2
+ * SDK HTTP 路由入口模块。
3
+ *
4
+ * 关键点(中文)
5
+ * - 这里专门承载 `RemoteAgent` 对应的最小 SDK transport。
6
+ * - 路由面围绕 Session actor 公开能力展开,不混入 control UI 语义。
7
+ */
8
+
9
+ import { Hono } from "hono";
10
+ import type { AgentCore } from "@/core/AgentCore.js";
11
+ import { registerSdkSessionRoutes } from "@/runtime/server/http/sdk/SessionRoutes.js";
12
+
13
+ /**
14
+ * 创建 SDK HTTP router。
15
+ */
16
+ export function createSdkRouter(
17
+ core: Pick<AgentCore, "session" | "sessions">,
18
+ ): Hono {
19
+ const router = new Hono();
20
+ registerSdkSessionRoutes(router, core);
21
+ return router;
22
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * SDK HTTP session 路由。
3
+ *
4
+ * 关键点(中文)
5
+ * - 这组路由面向 `RemoteAgent`,只暴露最小 Session actor 使用面。
6
+ * - 当前公开输入收口到 `prompt()`,公开输出收口到 `events` 长连接。
7
+ * - 不复用 control API 的控制台语义,避免 transport 面混入非 SDK 约束。
8
+ */
9
+
10
+ import { Hono } from "hono";
11
+ import type { AgentCore } from "@/core/AgentCore.js";
12
+ import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
13
+ import type { AgentSessionEvent } from "@/types/sdk/AgentSessionEvent.js";
14
+
15
+ const NDJSON_CONTENT_TYPE = "application/x-ndjson; charset=utf-8";
16
+ const SDK_EVENTS_READY_FRAME = {
17
+ type: "sdk-events-ready",
18
+ } as const;
19
+
20
+ /**
21
+ * 注册 SDK session 路由。
22
+ */
23
+ export function registerSdkSessionRoutes(
24
+ app: Hono,
25
+ core: Pick<AgentCore, "session" | "sessions">,
26
+ ): void {
27
+ app.get("/api/sdk/sessions", async (c) => {
28
+ try {
29
+ const sessions = await core.sessions();
30
+ return c.json({
31
+ success: true,
32
+ sessions,
33
+ });
34
+ } catch (error) {
35
+ return c.json(
36
+ {
37
+ success: false,
38
+ error: error instanceof Error ? error.message : String(error),
39
+ },
40
+ 500,
41
+ );
42
+ }
43
+ });
44
+
45
+ app.post("/api/sdk/sessions", async (c) => {
46
+ try {
47
+ const body = (await c.req.json().catch(() => ({}))) as {
48
+ sessionId?: unknown;
49
+ };
50
+ const rawSessionId = String(body.sessionId || "").trim();
51
+ const session = await core.session(rawSessionId || undefined);
52
+ return c.json({
53
+ success: true,
54
+ session: await session.toMetadata(),
55
+ });
56
+ } catch (error) {
57
+ return c.json(
58
+ {
59
+ success: false,
60
+ error: error instanceof Error ? error.message : String(error),
61
+ },
62
+ 500,
63
+ );
64
+ }
65
+ });
66
+
67
+ app.post("/api/sdk/sessions/:sessionId/prompt", async (c) => {
68
+ try {
69
+ const sessionId = String(c.req.param("sessionId") || "").trim();
70
+ if (!sessionId) {
71
+ return c.json({ success: false, error: "Missing sessionId" }, 400);
72
+ }
73
+ const body = (await c.req.json()) as AgentSessionPromptInput;
74
+ const session = await core.session(sessionId);
75
+ const turn = await session.prompt(body);
76
+ return c.json({
77
+ success: true,
78
+ turn: {
79
+ id: turn.id,
80
+ },
81
+ });
82
+ } catch (error) {
83
+ return c.json(
84
+ {
85
+ success: false,
86
+ error: error instanceof Error ? error.message : String(error),
87
+ },
88
+ 500,
89
+ );
90
+ }
91
+ });
92
+
93
+ app.get("/api/sdk/sessions/:sessionId/events", async (c) => {
94
+ const sessionId = String(c.req.param("sessionId") || "").trim();
95
+ if (!sessionId) {
96
+ return c.json({ success: false, error: "Missing sessionId" }, 400);
97
+ }
98
+
99
+ try {
100
+ const session = await core.session(sessionId);
101
+ const encoder = new TextEncoder();
102
+ const requestSignal = c.req.raw.signal;
103
+
104
+ let cleanupEventsConnection = (): void => {};
105
+ const stream = new ReadableStream<Uint8Array>({
106
+ cancel() {
107
+ cleanupEventsConnection();
108
+ },
109
+ start(controller) {
110
+ const writeLine = (value: unknown): void => {
111
+ controller.enqueue(encoder.encode(`${JSON.stringify(value)}\n`));
112
+ };
113
+
114
+ const unsubscribe = session.subscribe((event) => {
115
+ writeLine(event);
116
+ });
117
+
118
+ const closeStream = (): void => {
119
+ cleanupEventsConnection();
120
+ try {
121
+ controller.close();
122
+ } catch {
123
+ // ignore duplicate close attempts
124
+ }
125
+ };
126
+
127
+ cleanupEventsConnection = (): void => {
128
+ unsubscribe();
129
+ requestSignal.removeEventListener("abort", closeStream);
130
+ };
131
+
132
+ if (requestSignal.aborted) {
133
+ closeStream();
134
+ return;
135
+ }
136
+
137
+ requestSignal.addEventListener("abort", closeStream, { once: true });
138
+ writeLine(SDK_EVENTS_READY_FRAME);
139
+ },
140
+ });
141
+
142
+ return new Response(stream, {
143
+ status: 200,
144
+ headers: {
145
+ "Content-Type": NDJSON_CONTENT_TYPE,
146
+ "Cache-Control": "no-cache, no-transform",
147
+ "X-Accel-Buffering": "no",
148
+ },
149
+ });
150
+ } catch (error) {
151
+ return c.json(
152
+ {
153
+ success: false,
154
+ error: error instanceof Error ? error.message : String(error),
155
+ },
156
+ 500,
157
+ );
158
+ }
159
+ });
160
+
161
+ app.get("/api/sdk/sessions/:sessionId/messages", async (c) => {
162
+ try {
163
+ const sessionId = String(c.req.param("sessionId") || "").trim();
164
+ if (!sessionId) {
165
+ return c.json({ success: false, error: "Missing sessionId" }, 400);
166
+ }
167
+ const session = await core.session(sessionId);
168
+ return c.json({
169
+ success: true,
170
+ messages: await session.history(),
171
+ });
172
+ } catch (error) {
173
+ return c.json(
174
+ {
175
+ success: false,
176
+ error: error instanceof Error ? error.message : String(error),
177
+ },
178
+ 500,
179
+ );
180
+ }
181
+ });
182
+
183
+ app.get("/api/sdk/sessions/:sessionId/system", async (c) => {
184
+ try {
185
+ const sessionId = String(c.req.param("sessionId") || "").trim();
186
+ if (!sessionId) {
187
+ return c.json({ success: false, error: "Missing sessionId" }, 400);
188
+ }
189
+ const session = await core.session(sessionId);
190
+ return c.json({
191
+ success: true,
192
+ system: await session.system(),
193
+ });
194
+ } catch (error) {
195
+ return c.json(
196
+ {
197
+ success: false,
198
+ error: error instanceof Error ? error.message : String(error),
199
+ },
200
+ 500,
201
+ );
202
+ }
203
+ });
204
+
205
+ app.post("/api/sdk/sessions/:sessionId/fork", async (c) => {
206
+ try {
207
+ const sessionId = String(c.req.param("sessionId") || "").trim();
208
+ if (!sessionId) {
209
+ return c.json({ success: false, error: "Missing sessionId" }, 400);
210
+ }
211
+ const body = (await c.req.json().catch(() => ({}))) as {
212
+ messageId?: unknown;
213
+ };
214
+ const session = await core.session(sessionId);
215
+ const forked = await session.fork(
216
+ String(body.messageId || "").trim() || undefined,
217
+ );
218
+ return c.json({
219
+ success: true,
220
+ session: await forked.toMetadata(),
221
+ });
222
+ } catch (error) {
223
+ return c.json(
224
+ {
225
+ success: false,
226
+ error: error instanceof Error ? error.message : String(error),
227
+ },
228
+ 500,
229
+ );
230
+ }
231
+ });
232
+ }