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.
- package/LICENSE +21 -0
- package/dist/bin-create.d.ts +3 -0
- package/dist/bin-create.d.ts.map +1 -0
- package/dist/bin-create.js +3 -0
- package/dist/bin-create.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +3 -0
- package/dist/bin.js.map +1 -0
- package/dist/config-loader.d.ts +12 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +22 -0
- package/dist/config-loader.js.map +1 -0
- package/dist/define-agent.d.ts +34 -0
- package/dist/define-agent.d.ts.map +1 -0
- package/dist/define-agent.js +38 -0
- package/dist/define-agent.js.map +1 -0
- package/dist/define-config.d.ts +27 -0
- package/dist/define-config.d.ts.map +1 -0
- package/dist/define-config.js +88 -0
- package/dist/define-config.js.map +1 -0
- package/dist/define-skill.d.ts +10 -0
- package/dist/define-skill.d.ts.map +1 -0
- package/dist/define-skill.js +45 -0
- package/dist/define-skill.js.map +1 -0
- package/dist/define-tool.d.ts +27 -0
- package/dist/define-tool.d.ts.map +1 -0
- package/dist/define-tool.js +44 -0
- package/dist/define-tool.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +88 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +69 -0
- package/dist/logger.js.map +1 -0
- package/dist/londai.d.ts +15 -0
- package/dist/londai.d.ts.map +1 -0
- package/dist/londai.js +74 -0
- package/dist/londai.js.map +1 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +130 -0
- package/dist/registry.js.map +1 -0
- package/dist/server/abort.d.ts +23 -0
- package/dist/server/abort.d.ts.map +1 -0
- package/dist/server/abort.js +35 -0
- package/dist/server/abort.js.map +1 -0
- package/dist/server/agent-builder.d.ts +49 -0
- package/dist/server/agent-builder.d.ts.map +1 -0
- package/dist/server/agent-builder.js +107 -0
- package/dist/server/agent-builder.js.map +1 -0
- package/dist/server/app.d.ts +21 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +34 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/middleware/auth.d.ts +23 -0
- package/dist/server/middleware/auth.d.ts.map +1 -0
- package/dist/server/middleware/auth.js +102 -0
- package/dist/server/middleware/auth.js.map +1 -0
- package/dist/server/middleware/error.d.ts +10 -0
- package/dist/server/middleware/error.d.ts.map +1 -0
- package/dist/server/middleware/error.js +17 -0
- package/dist/server/middleware/error.js.map +1 -0
- package/dist/server/middleware/rate-limit.d.ts +18 -0
- package/dist/server/middleware/rate-limit.d.ts.map +1 -0
- package/dist/server/middleware/rate-limit.js +55 -0
- package/dist/server/middleware/rate-limit.js.map +1 -0
- package/dist/server/routes/approve.d.ts +11 -0
- package/dist/server/routes/approve.d.ts.map +1 -0
- package/dist/server/routes/approve.js +38 -0
- package/dist/server/routes/approve.js.map +1 -0
- package/dist/server/routes/chat-completion.d.ts +14 -0
- package/dist/server/routes/chat-completion.d.ts.map +1 -0
- package/dist/server/routes/chat-completion.js +70 -0
- package/dist/server/routes/chat-completion.js.map +1 -0
- package/dist/server/routes/chat-stream.d.ts +29 -0
- package/dist/server/routes/chat-stream.d.ts.map +1 -0
- package/dist/server/routes/chat-stream.js +190 -0
- package/dist/server/routes/chat-stream.js.map +1 -0
- package/dist/server/routes/common.d.ts +47 -0
- package/dist/server/routes/common.d.ts.map +1 -0
- package/dist/server/routes/common.js +143 -0
- package/dist/server/routes/common.js.map +1 -0
- package/dist/server/routes/session.d.ts +22 -0
- package/dist/server/routes/session.d.ts.map +1 -0
- package/dist/server/routes/session.js +228 -0
- package/dist/server/routes/session.js.map +1 -0
- package/dist/skills-loader.d.ts +46 -0
- package/dist/skills-loader.d.ts.map +1 -0
- package/dist/skills-loader.js +113 -0
- package/dist/skills-loader.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- 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
|