londai 0.1.0-alpha.0

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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin-create.d.ts +3 -0
  3. package/dist/bin-create.d.ts.map +1 -0
  4. package/dist/bin-create.js +3 -0
  5. package/dist/bin-create.js.map +1 -0
  6. package/dist/bin.d.ts +3 -0
  7. package/dist/bin.d.ts.map +1 -0
  8. package/dist/bin.js +3 -0
  9. package/dist/bin.js.map +1 -0
  10. package/dist/config-loader.d.ts +12 -0
  11. package/dist/config-loader.d.ts.map +1 -0
  12. package/dist/config-loader.js +22 -0
  13. package/dist/config-loader.js.map +1 -0
  14. package/dist/define-agent.d.ts +34 -0
  15. package/dist/define-agent.d.ts.map +1 -0
  16. package/dist/define-agent.js +38 -0
  17. package/dist/define-agent.js.map +1 -0
  18. package/dist/define-config.d.ts +27 -0
  19. package/dist/define-config.d.ts.map +1 -0
  20. package/dist/define-config.js +88 -0
  21. package/dist/define-config.js.map +1 -0
  22. package/dist/define-skill.d.ts +10 -0
  23. package/dist/define-skill.d.ts.map +1 -0
  24. package/dist/define-skill.js +45 -0
  25. package/dist/define-skill.js.map +1 -0
  26. package/dist/define-tool.d.ts +27 -0
  27. package/dist/define-tool.d.ts.map +1 -0
  28. package/dist/define-tool.js +44 -0
  29. package/dist/define-tool.js.map +1 -0
  30. package/dist/errors.d.ts +29 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +88 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/index.d.ts +25 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +24 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/logger.d.ts +12 -0
  39. package/dist/logger.d.ts.map +1 -0
  40. package/dist/logger.js +69 -0
  41. package/dist/logger.js.map +1 -0
  42. package/dist/londai.d.ts +15 -0
  43. package/dist/londai.d.ts.map +1 -0
  44. package/dist/londai.js +74 -0
  45. package/dist/londai.js.map +1 -0
  46. package/dist/registry.d.ts +57 -0
  47. package/dist/registry.d.ts.map +1 -0
  48. package/dist/registry.js +130 -0
  49. package/dist/registry.js.map +1 -0
  50. package/dist/server/abort.d.ts +23 -0
  51. package/dist/server/abort.d.ts.map +1 -0
  52. package/dist/server/abort.js +35 -0
  53. package/dist/server/abort.js.map +1 -0
  54. package/dist/server/agent-builder.d.ts +49 -0
  55. package/dist/server/agent-builder.d.ts.map +1 -0
  56. package/dist/server/agent-builder.js +107 -0
  57. package/dist/server/agent-builder.js.map +1 -0
  58. package/dist/server/app.d.ts +21 -0
  59. package/dist/server/app.d.ts.map +1 -0
  60. package/dist/server/app.js +34 -0
  61. package/dist/server/app.js.map +1 -0
  62. package/dist/server/middleware/auth.d.ts +23 -0
  63. package/dist/server/middleware/auth.d.ts.map +1 -0
  64. package/dist/server/middleware/auth.js +102 -0
  65. package/dist/server/middleware/auth.js.map +1 -0
  66. package/dist/server/middleware/error.d.ts +10 -0
  67. package/dist/server/middleware/error.d.ts.map +1 -0
  68. package/dist/server/middleware/error.js +17 -0
  69. package/dist/server/middleware/error.js.map +1 -0
  70. package/dist/server/middleware/rate-limit.d.ts +18 -0
  71. package/dist/server/middleware/rate-limit.d.ts.map +1 -0
  72. package/dist/server/middleware/rate-limit.js +55 -0
  73. package/dist/server/middleware/rate-limit.js.map +1 -0
  74. package/dist/server/routes/approve.d.ts +11 -0
  75. package/dist/server/routes/approve.d.ts.map +1 -0
  76. package/dist/server/routes/approve.js +38 -0
  77. package/dist/server/routes/approve.js.map +1 -0
  78. package/dist/server/routes/chat-completion.d.ts +14 -0
  79. package/dist/server/routes/chat-completion.d.ts.map +1 -0
  80. package/dist/server/routes/chat-completion.js +70 -0
  81. package/dist/server/routes/chat-completion.js.map +1 -0
  82. package/dist/server/routes/chat-stream.d.ts +29 -0
  83. package/dist/server/routes/chat-stream.d.ts.map +1 -0
  84. package/dist/server/routes/chat-stream.js +190 -0
  85. package/dist/server/routes/chat-stream.js.map +1 -0
  86. package/dist/server/routes/common.d.ts +47 -0
  87. package/dist/server/routes/common.d.ts.map +1 -0
  88. package/dist/server/routes/common.js +143 -0
  89. package/dist/server/routes/common.js.map +1 -0
  90. package/dist/server/routes/session.d.ts +22 -0
  91. package/dist/server/routes/session.d.ts.map +1 -0
  92. package/dist/server/routes/session.js +228 -0
  93. package/dist/server/routes/session.js.map +1 -0
  94. package/dist/skills-loader.d.ts +46 -0
  95. package/dist/skills-loader.d.ts.map +1 -0
  96. package/dist/skills-loader.js +113 -0
  97. package/dist/skills-loader.js.map +1 -0
  98. package/dist/types.d.ts +128 -0
  99. package/dist/types.d.ts.map +1 -0
  100. package/dist/types.js +9 -0
  101. package/dist/types.js.map +1 -0
  102. package/package.json +47 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * `POST /chat/stream` — SSE route mapping `AgentEvent` → wire events.
3
+ *
4
+ * Wire format:
5
+ * - `event: delta` data: "text chunk"
6
+ * - `event: tool_start` data: {callId, name, args}
7
+ * - `event: tool_end` data: {callId, isError, details?}
8
+ * - `event: approval_required` data: {conversationId, callId, toolName, args}
9
+ * - `event: error` data: {code, message}
10
+ * - `event: done` data: ""
11
+ *
12
+ * Abort: `c.req.raw.signal` is plowed into the agent via `trackRequest`. Client
13
+ * disconnect → `agent.abort()` → loop encodes stopReason "aborted" → we emit
14
+ * `event: error` (aborted) and close.
15
+ *
16
+ * Session integration (milestone 04):
17
+ * - If a `SessionRepo` is configured, loads conversation history and persists new
18
+ * messages after the prompt settles.
19
+ * - Sends `x-londai-conversation` header with the conversation id.
20
+ */
21
+ import { FrameworkError } from "../../errors.js";
22
+ import { requireModels } from "../../registry.js";
23
+ import { finishRun, trackRequest } from "../abort.js";
24
+ import { buildAgent } from "../agent-builder.js";
25
+ import { parseChatBody, persistNewMessages, resolveConversationContext } from "./common.js";
26
+ export function registerChatStreamRoute(app, deps) {
27
+ const { spec } = deps;
28
+ app.post("/chat/stream", async (c) => {
29
+ const models = requireModels();
30
+ const requestSignal = c.req.raw.signal;
31
+ const body = await parseChatBody(c);
32
+ const sessionCtx = await resolveConversationContext(body.conversationId, c, {
33
+ explicitConversationId: body.conversationIdProvided,
34
+ });
35
+ // Resolve final conversation id. If session is active, sessionCtx carries it.
36
+ const conversationId = sessionCtx?.conversationId ?? body.conversationId;
37
+ const sessionLoadedCount = sessionCtx?.loadedCount ?? 0;
38
+ const seedMessages = sessionCtx?.messages ?? body.messages;
39
+ const tracked = trackRequest(conversationId, { requestSignal });
40
+ if (!tracked) {
41
+ return c.json({ error: { code: "conflict", message: `A run is already active for conversation ${conversationId}.` } }, 409);
42
+ }
43
+ return c.body(new ReadableStream({
44
+ async start(controller) {
45
+ const encoder = new TextEncoder();
46
+ let closed = false;
47
+ const send = (event, data) => {
48
+ if (closed)
49
+ return;
50
+ const payload = `event: ${event}\ndata: ${typeof data === "string" ? data : JSON.stringify(data)}\n\n`;
51
+ controller.enqueue(encoder.encode(payload));
52
+ };
53
+ const close = () => {
54
+ if (closed)
55
+ return;
56
+ closed = true;
57
+ try {
58
+ controller.close();
59
+ }
60
+ catch {
61
+ // already closed
62
+ }
63
+ };
64
+ try {
65
+ const { signal } = tracked;
66
+ const agent = await buildAgent(spec, {
67
+ conversationId,
68
+ messages: seedMessages,
69
+ userId: c.get("userId"),
70
+ signal,
71
+ }, models, {
72
+ approvalConfig: deps.approval ?? false,
73
+ emitApprovalRequired: (info) => {
74
+ send("approval_required", info);
75
+ },
76
+ });
77
+ // Plow abort from agent controller (handled via trackRequest's signal).
78
+ const unsubscribe = agent.subscribe((event) => {
79
+ deps.onEvent?.(event);
80
+ emitAgentEvent(event, send);
81
+ });
82
+ try {
83
+ if (typeof body.message === "string") {
84
+ await agent.prompt(body.message);
85
+ }
86
+ else {
87
+ await agent.prompt(body.message);
88
+ }
89
+ // Persist new messages after prompt settles.
90
+ await persistNewMessages(conversationId, agent.state.messages, sessionLoadedCount);
91
+ const lastAssistant = [...agent.state.messages].reverse().find((m) => m.role === "assistant");
92
+ if (lastAssistant &&
93
+ "stopReason" in lastAssistant &&
94
+ (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted")) {
95
+ send("error", {
96
+ code: lastAssistant.stopReason,
97
+ message: lastAssistant.errorMessage ?? "Provider stream failed.",
98
+ });
99
+ return;
100
+ }
101
+ send("done", "");
102
+ }
103
+ catch (error) {
104
+ const code = error instanceof FrameworkError ? error.code : "unknown";
105
+ send("error", { code, message: error instanceof Error ? error.message : String(error) });
106
+ }
107
+ finally {
108
+ unsubscribe();
109
+ close();
110
+ }
111
+ }
112
+ catch (error) {
113
+ const code = error instanceof FrameworkError ? error.code : "unknown";
114
+ const message = error instanceof Error ? error.message : String(error);
115
+ try {
116
+ controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ code, message })}\n\n`));
117
+ }
118
+ catch {
119
+ // stream may already be closed
120
+ }
121
+ close();
122
+ }
123
+ finally {
124
+ finishRun(conversationId);
125
+ }
126
+ },
127
+ cancel() {
128
+ // Called when the client disconnects. The request signal is already
129
+ // aborted, which aborts the agent run via trackRequest.
130
+ },
131
+ }), {
132
+ headers: {
133
+ "content-type": "text/event-stream",
134
+ "cache-control": "no-cache",
135
+ connection: "keep-alive",
136
+ "x-londai-conversation": conversationId,
137
+ },
138
+ });
139
+ });
140
+ }
141
+ function emitAgentEvent(event, send) {
142
+ switch (event.type) {
143
+ case "message_update": {
144
+ const e = event.assistantMessageEvent;
145
+ if (e.type === "text_delta") {
146
+ send("delta", e.delta);
147
+ }
148
+ else if (e.type === "error") {
149
+ send("error", { code: e.reason, message: e.error.errorMessage ?? "Provider stream failed." });
150
+ }
151
+ else if (e.type === "toolcall_start") {
152
+ const call = e.partial.content.find((b) => b.type === "toolCall");
153
+ if (call && call.type === "toolCall") {
154
+ send("tool_start", { callId: call.id, name: call.name, args: call.arguments });
155
+ }
156
+ }
157
+ break;
158
+ }
159
+ case "tool_execution_start":
160
+ send("tool_execution_start", {
161
+ callId: event.toolCallId,
162
+ name: event.toolName,
163
+ args: event.args,
164
+ });
165
+ break;
166
+ case "tool_execution_update":
167
+ send("tool_update", {
168
+ callId: event.toolCallId,
169
+ name: event.toolName,
170
+ partial: event.partialResult,
171
+ });
172
+ break;
173
+ case "tool_execution_end":
174
+ send("tool_end", {
175
+ callId: event.toolCallId,
176
+ name: event.toolName,
177
+ isError: event.isError,
178
+ details: event.result?.details,
179
+ });
180
+ break;
181
+ case "agent_end":
182
+ // `done` is emitted by the route after `agent.prompt` settles.
183
+ break;
184
+ default:
185
+ // `agent_start`, `turn_start/end`, `message_start/end` are not surfaced
186
+ // on the wire (internal lifecycle only).
187
+ break;
188
+ }
189
+ }
190
+ //# sourceMappingURL=chat-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-stream.js","sourceRoot":"","sources":["../../../src/server/routes/chat-stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared helpers for chat routes.
3
+ */
4
+ import type { AgentMessage } from "@londai/core";
5
+ import type { SessionRepo, SessionSnapshot } from "@londai/session";
6
+ import type { Context } from "hono";
7
+ export interface ParsedChatBody {
8
+ message: string | AgentMessage[];
9
+ /** Pre-existing conversation history (seeds the agent). */
10
+ messages?: AgentMessage[];
11
+ conversationId: string;
12
+ conversationIdProvided: boolean;
13
+ }
14
+ export declare function parseChatBody(c: Context): Promise<ParsedChatBody>;
15
+ export declare function ensureConversationId(body: {
16
+ conversationId?: unknown;
17
+ }, c: Context): string;
18
+ /**
19
+ * Resolve a conversation for chat routes.
20
+ *
21
+ * - If the body has a `conversationId` AND a session repo is configured,
22
+ * loads existing messages + context, seeds into the agent.
23
+ * - If no conversationId but session is active, creates a new conversation.
24
+ * - If no session configured, returns undefined (stateless mode).
25
+ *
26
+ * Returns:
27
+ * - `loadedCount`: number of messages already persisted (so we only append NEW ones).
28
+ * - `messages`: the loaded messages to seed the agent with.
29
+ * - `newConversationId`: false if using an existing conversation.
30
+ */
31
+ export declare function resolveConversationContext(conversationId: string, c: Context, options?: {
32
+ explicitConversationId?: boolean;
33
+ }): Promise<{
34
+ messages: AgentMessage[];
35
+ loadedCount: number;
36
+ conversationId: string;
37
+ newConversation: boolean;
38
+ } | undefined>;
39
+ export declare function currentTenantId(c: Context): string;
40
+ export declare function assertOwnsConversation(snap: SessionSnapshot, tenantId: string): void;
41
+ export declare function openOwnedConversation(repo: SessionRepo, conversationId: string, c: Context): Promise<SessionSnapshot>;
42
+ export declare function normalizeSessionOpenError(error: unknown): Error;
43
+ /**
44
+ * Persist new messages after an agent run. Only saves messages beyond `loadedCount`.
45
+ */
46
+ export declare function persistNewMessages(conversationId: string, messages: AgentMessage[], loadedCount: number): Promise<void>;
47
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/server/routes/common.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAIpC,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,CAAC;IACjC,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,OAAO,CAAC;CAChC;AAED,wBAAsB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAwCvE;AASD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAAE,cAAc,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,CAAC,EAAE,OAAO,GAAG,MAAM,CAM3F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,0BAA0B,CAC/C,cAAc,EAAE,MAAM,EACtB,CAAC,EAAE,OAAO,EACV,OAAO,CAAC,EAAE;IAAE,sBAAsB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC5C,OAAO,CACP;IACA,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;CACxB,GACD,SAAS,CACX,CAmCA;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAElD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIpF;AAED,wBAAsB,qBAAqB,CAC1C,IAAI,EAAE,WAAW,EACjB,cAAc,EAAE,MAAM,EACtB,CAAC,EAAE,OAAO,GACR,OAAO,CAAC,eAAe,CAAC,CAQ1B;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAO/D;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACvC,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,YAAY,EAAE,EACxB,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAQf"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Shared helpers for chat routes.
3
+ */
4
+ import { randomUUID } from "node:crypto";
5
+ import { FrameworkError } from "../../errors.js";
6
+ import { getSessionRepo } from "../../registry.js";
7
+ export async function parseChatBody(c) {
8
+ let json;
9
+ try {
10
+ json = await c.req.json();
11
+ }
12
+ catch {
13
+ throw new FrameworkError("validation", "Request body must be JSON.");
14
+ }
15
+ if (typeof json !== "object" || json === null) {
16
+ throw new FrameworkError("validation", "Request body must be a JSON object.");
17
+ }
18
+ const body = json;
19
+ const message = body.message;
20
+ if (typeof message !== "string" && !Array.isArray(message)) {
21
+ throw new FrameworkError("validation", "`message` must be a string or an array of messages.");
22
+ }
23
+ if (Array.isArray(message)) {
24
+ for (const m of message) {
25
+ if (typeof m !== "object" || m === null || !("role" in m)) {
26
+ throw new FrameworkError("validation", "Each message in the array must have a `role`.");
27
+ }
28
+ }
29
+ }
30
+ const messagesField = body.messages;
31
+ if (messagesField !== undefined &&
32
+ (!Array.isArray(messagesField) ||
33
+ messagesField.some((m) => typeof m !== "object" || m === null || !("role" in m)))) {
34
+ throw new FrameworkError("validation", "`messages` must be an array of messages with a `role`.");
35
+ }
36
+ const conversationIdProvided = hasConversationId({ conversationId: body.conversationId }, c);
37
+ const conversationId = ensureConversationId({ conversationId: body.conversationId }, c);
38
+ return {
39
+ message: message,
40
+ messages: messagesField,
41
+ conversationId,
42
+ conversationIdProvided,
43
+ };
44
+ }
45
+ function hasConversationId(body, c) {
46
+ return ((typeof body.conversationId === "string" && body.conversationId.length > 0) ||
47
+ typeof c.req.header("x-londai-conversation") === "string");
48
+ }
49
+ export function ensureConversationId(body, c) {
50
+ const fromHeader = c.req.header("x-londai-conversation");
51
+ const fromBody = body.conversationId;
52
+ if (typeof fromBody === "string" && fromBody.length > 0)
53
+ return fromBody;
54
+ if (typeof fromHeader === "string" && fromHeader.length > 0)
55
+ return fromHeader;
56
+ return randomUUID();
57
+ }
58
+ /**
59
+ * Resolve a conversation for chat routes.
60
+ *
61
+ * - If the body has a `conversationId` AND a session repo is configured,
62
+ * loads existing messages + context, seeds into the agent.
63
+ * - If no conversationId but session is active, creates a new conversation.
64
+ * - If no session configured, returns undefined (stateless mode).
65
+ *
66
+ * Returns:
67
+ * - `loadedCount`: number of messages already persisted (so we only append NEW ones).
68
+ * - `messages`: the loaded messages to seed the agent with.
69
+ * - `newConversationId`: false if using an existing conversation.
70
+ */
71
+ export async function resolveConversationContext(conversationId, c, options) {
72
+ const repo = getSessionRepo();
73
+ if (!repo)
74
+ return undefined;
75
+ const tenantId = currentTenantId(c);
76
+ let existingId;
77
+ // Check if conversationId is a real existing one.
78
+ if (conversationId && !conversationId.startsWith("anon-")) {
79
+ try {
80
+ const snap = await repo.open(conversationId);
81
+ assertOwnsConversation(snap, tenantId);
82
+ existingId = conversationId;
83
+ }
84
+ catch (error) {
85
+ if (options?.explicitConversationId)
86
+ throw normalizeSessionOpenError(error);
87
+ }
88
+ }
89
+ if (!existingId) {
90
+ const conv = await repo.create(tenantId);
91
+ existingId = conv.id;
92
+ return { messages: [], loadedCount: 0, conversationId: existingId, newConversation: true };
93
+ }
94
+ // Load existing conversation.
95
+ const snap = await repo.open(existingId);
96
+ assertOwnsConversation(snap, tenantId);
97
+ return {
98
+ messages: snap.messages,
99
+ loadedCount: snap.messages.length,
100
+ conversationId: existingId,
101
+ newConversation: false,
102
+ };
103
+ }
104
+ export function currentTenantId(c) {
105
+ return c.get("userId") ?? "anonymous";
106
+ }
107
+ export function assertOwnsConversation(snap, tenantId) {
108
+ if (snap.context.tenantId !== tenantId) {
109
+ throw new FrameworkError("forbidden", "Conversation does not belong to the current user.");
110
+ }
111
+ }
112
+ export async function openOwnedConversation(repo, conversationId, c) {
113
+ try {
114
+ const snap = await repo.open(conversationId);
115
+ assertOwnsConversation(snap, currentTenantId(c));
116
+ return snap;
117
+ }
118
+ catch (error) {
119
+ throw normalizeSessionOpenError(error);
120
+ }
121
+ }
122
+ export function normalizeSessionOpenError(error) {
123
+ const err = error;
124
+ if (err instanceof FrameworkError)
125
+ return err;
126
+ if (err.code === "not_found" || err.message?.includes("not found")) {
127
+ return new FrameworkError("not_found", "Conversation not found.", error);
128
+ }
129
+ return err;
130
+ }
131
+ /**
132
+ * Persist new messages after an agent run. Only saves messages beyond `loadedCount`.
133
+ */
134
+ export async function persistNewMessages(conversationId, messages, loadedCount) {
135
+ const repo = getSessionRepo();
136
+ if (!repo)
137
+ return;
138
+ const newMessages = messages.slice(loadedCount);
139
+ if (newMessages.length === 0)
140
+ return;
141
+ await repo.appendMessages(conversationId, newMessages);
142
+ }
143
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../../../src/server/routes/common.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `/session/*` REST routes.
3
+ *
4
+ * Mounted only when `config.session` is defined. Provides CRUD for
5
+ * conversations and a `/session/:id/resume` endpoint that reuses the
6
+ * chat-stream handler factory.
7
+ *
8
+ * - `POST /session` — create new conversation
9
+ * - `GET /session/:id` — get conversation messages + context
10
+ * - `GET /session` — list conversations (filter by tenant/userId)
11
+ * - `DELETE /session/:id` — delete conversation
12
+ * - `POST /session/:id/resume` — resume (reuse chat-stream handler)
13
+ */
14
+ import type { AgentSpec, ApprovalConfig, LondaiRunOptions } from "../../types.ts";
15
+ import type { HonoContext } from "../app.ts";
16
+ export interface SessionRoutesDeps {
17
+ spec: AgentSpec;
18
+ approval?: ApprovalConfig | false;
19
+ onEvent?: LondaiRunOptions["onEvent"];
20
+ }
21
+ export declare function registerSessionRoutes(app: HonoContext, deps: SessionRoutesDeps): void;
22
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/server/routes/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGlF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAG7C,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,CAAC,EAAE,cAAc,GAAG,KAAK,CAAC;IAClC,OAAO,CAAC,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC;CACtC;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CA6LrF"}
@@ -0,0 +1,228 @@
1
+ /**
2
+ * `/session/*` REST routes.
3
+ *
4
+ * Mounted only when `config.session` is defined. Provides CRUD for
5
+ * conversations and a `/session/:id/resume` endpoint that reuses the
6
+ * chat-stream handler factory.
7
+ *
8
+ * - `POST /session` — create new conversation
9
+ * - `GET /session/:id` — get conversation messages + context
10
+ * - `GET /session` — list conversations (filter by tenant/userId)
11
+ * - `DELETE /session/:id` — delete conversation
12
+ * - `POST /session/:id/resume` — resume (reuse chat-stream handler)
13
+ */
14
+ import { errorBody, errorToStatus, FrameworkError } from "../../errors.js";
15
+ import { getSessionRepo, requireModels } from "../../registry.js";
16
+ import { finishRun, trackRequest } from "../abort.js";
17
+ import { buildAgent } from "../agent-builder.js";
18
+ import { currentTenantId, openOwnedConversation, persistNewMessages } from "./common.js";
19
+ export function registerSessionRoutes(app, deps) {
20
+ const { spec } = deps;
21
+ // POST /session — create new conversation
22
+ app.post("/session", async (c) => {
23
+ const repo = getSessionRepo();
24
+ if (!repo) {
25
+ return c.json({ error: { code: "config", message: "Session not configured." } }, 500);
26
+ }
27
+ const conv = await repo.create(currentTenantId(c));
28
+ return c.json({ id: conv.id, conversationId: conv.id, createdAt: conv.createdAt }, 201);
29
+ });
30
+ // GET /session — list conversations
31
+ app.get("/session", async (c) => {
32
+ const repo = getSessionRepo();
33
+ if (!repo) {
34
+ return c.json({ error: { code: "config", message: "Session not configured." } }, 500);
35
+ }
36
+ const entries = await repo.list(currentTenantId(c));
37
+ return c.json({ conversations: entries });
38
+ });
39
+ // GET /session/:id — get conversation messages + context
40
+ app.get("/session/:id", async (c) => {
41
+ const repo = getSessionRepo();
42
+ if (!repo) {
43
+ return c.json({ error: { code: "config", message: "Session not configured." } }, 500);
44
+ }
45
+ const id = c.req.param("id");
46
+ try {
47
+ const snap = await openOwnedConversation(repo, id, c);
48
+ return c.json({
49
+ conversationId: id,
50
+ messages: snap.messages,
51
+ context: snap.context,
52
+ });
53
+ }
54
+ catch (error) {
55
+ return c.json(errorBody(error), errorToStatus(error));
56
+ }
57
+ });
58
+ // DELETE /session/:id — delete conversation
59
+ app.delete("/session/:id", async (c) => {
60
+ const repo = getSessionRepo();
61
+ if (!repo) {
62
+ return c.json({ error: { code: "config", message: "Session not configured." } }, 500);
63
+ }
64
+ const id = c.req.param("id");
65
+ try {
66
+ await openOwnedConversation(repo, id, c);
67
+ await repo.delete(id);
68
+ return c.body(null, 204);
69
+ }
70
+ catch (error) {
71
+ return c.json(errorBody(error), errorToStatus(error));
72
+ }
73
+ });
74
+ // POST /session/:id/resume — resume a conversation (SSE stream)
75
+ app.post("/session/:id/resume", async (c) => {
76
+ const repo = getSessionRepo();
77
+ if (!repo) {
78
+ return c.json({ error: { code: "config", message: "Session not configured." } }, 500);
79
+ }
80
+ const conversationId = c.req.param("id");
81
+ // Parse body for the message.
82
+ let body;
83
+ try {
84
+ body = await c.req.json();
85
+ }
86
+ catch {
87
+ return c.json({ error: { code: "validation", message: "Request body must be JSON." } }, 422);
88
+ }
89
+ if (typeof body.message !== "string") {
90
+ return c.json({ error: { code: "validation", message: "`message` must be a string." } }, 422);
91
+ }
92
+ // Load conversation.
93
+ let snap;
94
+ try {
95
+ snap = await openOwnedConversation(repo, conversationId, c);
96
+ }
97
+ catch (error) {
98
+ return c.json(errorBody(error), errorToStatus(error));
99
+ }
100
+ const loadedCount = snap.messages.length;
101
+ const seedMessages = snap.messages;
102
+ const models = requireModels();
103
+ const tracked = trackRequest(conversationId, { requestSignal: c.req.raw.signal });
104
+ if (!tracked) {
105
+ return c.json({ error: { code: "conflict", message: `A run is already active for conversation ${conversationId}.` } }, 409);
106
+ }
107
+ return c.body(new ReadableStream({
108
+ async start(controller) {
109
+ const encoder = new TextEncoder();
110
+ let closed = false;
111
+ const send = (event, data) => {
112
+ if (closed)
113
+ return;
114
+ const payload = `event: ${event}\ndata: ${typeof data === "string" ? data : JSON.stringify(data)}\n\n`;
115
+ controller.enqueue(encoder.encode(payload));
116
+ };
117
+ const close = () => {
118
+ if (closed)
119
+ return;
120
+ closed = true;
121
+ try {
122
+ controller.close();
123
+ }
124
+ catch {
125
+ //
126
+ }
127
+ };
128
+ try {
129
+ const { signal } = tracked;
130
+ const agent = await buildAgent(spec, {
131
+ conversationId,
132
+ messages: seedMessages,
133
+ userId: c.get("userId"),
134
+ signal,
135
+ }, models, {
136
+ approvalConfig: deps.approval ?? false,
137
+ emitApprovalRequired: (info) => {
138
+ send("approval_required", info);
139
+ },
140
+ });
141
+ const unsubscribe = agent.subscribe((event) => {
142
+ deps.onEvent?.(event);
143
+ emitAgentEventSession(event, send);
144
+ });
145
+ try {
146
+ await agent.prompt(body.message);
147
+ await persistNewMessages(conversationId, agent.state.messages, loadedCount);
148
+ const lastAssistant = [...agent.state.messages].reverse().find((m) => m.role === "assistant");
149
+ if (lastAssistant &&
150
+ "stopReason" in lastAssistant &&
151
+ (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted")) {
152
+ send("error", {
153
+ code: lastAssistant.stopReason,
154
+ message: lastAssistant.errorMessage ?? "Provider stream failed.",
155
+ });
156
+ return;
157
+ }
158
+ send("done", "");
159
+ }
160
+ catch (error) {
161
+ const code = error instanceof FrameworkError ? error.code : "unknown";
162
+ send("error", { code, message: error instanceof Error ? error.message : String(error) });
163
+ }
164
+ finally {
165
+ unsubscribe();
166
+ close();
167
+ }
168
+ }
169
+ catch (error) {
170
+ const code = error instanceof FrameworkError ? error.code : "unknown";
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ try {
173
+ controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ code, message })}\n\n`));
174
+ }
175
+ catch {
176
+ //
177
+ }
178
+ close();
179
+ }
180
+ finally {
181
+ finishRun(conversationId);
182
+ }
183
+ },
184
+ }), {
185
+ headers: {
186
+ "content-type": "text/event-stream",
187
+ "cache-control": "no-cache",
188
+ connection: "keep-alive",
189
+ "x-londai-conversation": conversationId,
190
+ },
191
+ });
192
+ });
193
+ }
194
+ function emitAgentEventSession(event, send) {
195
+ switch (event.type) {
196
+ case "message_update": {
197
+ const e = event.assistantMessageEvent;
198
+ if (e.type === "text_delta") {
199
+ send("delta", e.delta);
200
+ }
201
+ else if (e.type === "error") {
202
+ send("error", { code: e.reason, message: e.error.errorMessage ?? "Provider stream failed." });
203
+ }
204
+ else if (e.type === "toolcall_start") {
205
+ const call = e.partial.content.find((b) => b.type === "toolCall");
206
+ if (call && call.type === "toolCall") {
207
+ send("tool_start", { callId: call.id, name: call.name, args: call.arguments });
208
+ }
209
+ }
210
+ break;
211
+ }
212
+ case "tool_execution_start":
213
+ send("tool_execution_start", { callId: event.toolCallId, name: event.toolName, args: event.args });
214
+ break;
215
+ case "tool_execution_update":
216
+ send("tool_update", { callId: event.toolCallId, name: event.toolName, partial: event.partialResult });
217
+ break;
218
+ case "tool_execution_end":
219
+ send("tool_end", {
220
+ callId: event.toolCallId,
221
+ name: event.toolName,
222
+ isError: event.isError,
223
+ details: event.result?.details,
224
+ });
225
+ break;
226
+ }
227
+ }
228
+ //# sourceMappingURL=session.js.map