@junejs/server 1.0.0-dev.19 → 1.0.0-dev.24
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/dist/adapter-DcZ-W1-7.d.ts +62 -0
- package/dist/adapter.d.ts +1 -61
- package/dist/agent-durable.js +1 -43
- package/dist/agent-native.d.ts +15 -5
- package/dist/agent-native.js +85 -16
- package/dist/app.js +2 -7
- package/dist/build.d.ts +5 -26
- package/dist/build.js +10 -276
- package/dist/config-freeze-DwI-Rmvq.d.ts +17 -0
- package/dist/config-freeze.d.ts +2 -0
- package/dist/config-freeze.js +51 -0
- package/dist/connection-auth.d.ts +39 -0
- package/dist/connection-auth.js +31 -0
- package/dist/content-freeze-CyiW2fRK.d.ts +4 -0
- package/dist/content-freeze.d.ts +2 -0
- package/dist/content-freeze.js +83 -0
- package/dist/core-version.js +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.js +7 -2
- package/dist/manifest-gcNWqWoI.d.ts +11 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +111 -0
- package/dist/route-scan-DMlwoESy.d.ts +13 -0
- package/dist/route-scan.d.ts +2 -0
- package/dist/route-scan.js +55 -0
- package/dist/turn-events.d.ts +5 -0
- package/dist/turn-events.js +46 -0
- package/dist/worker-BkFHGtxp.d.ts +61 -0
- package/dist/worker.d.ts +1 -60
- package/package.json +2 -2
- package/src/agent-discover.ts +10 -7
- package/src/agent-durable.ts +1 -39
- package/src/agent-native.ts +133 -21
- package/src/app.ts +4 -6
- package/src/build.ts +26 -440
- package/src/config-freeze.ts +104 -0
- package/src/connection-auth.ts +101 -0
- package/src/content-freeze.ts +151 -0
- package/src/core-version.ts +1 -1
- package/src/index.ts +14 -0
- package/src/manifest.ts +128 -0
- package/src/route-scan.ts +84 -0
- package/src/turn-events.ts +43 -0
package/src/agent-native.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import {
|
|
11
11
|
AgentSession,
|
|
12
12
|
withSystem,
|
|
13
|
+
type ChannelPolicy,
|
|
13
14
|
type EventSink,
|
|
14
15
|
type TurnEvent,
|
|
15
16
|
type Model,
|
|
@@ -18,13 +19,17 @@ import {
|
|
|
18
19
|
type SessionStore,
|
|
19
20
|
type Tool,
|
|
20
21
|
} from "@junejs/core/agent-runtime";
|
|
21
|
-
import { channelFetch, type AgentDefinition, type ChannelContext } from "@junejs/core/agent-config";
|
|
22
|
+
import { buildSystemPrompt, channelFetch, type AgentDefinition, type ChannelContext } from "@junejs/core/agent-config";
|
|
22
23
|
import { openLocalSqliteSync, type SyncSqlite } from "./sqlite-driver";
|
|
23
24
|
import { assertCoreRuntimeVersion } from "./core-version";
|
|
25
|
+
import { observeTurnEvents } from "./turn-events";
|
|
24
26
|
|
|
25
27
|
function initSchema(db: SyncSqlite) {
|
|
26
28
|
db.exec(`CREATE TABLE IF NOT EXISTS agent_sessions (session_id TEXT PRIMARY KEY, status TEXT)`);
|
|
27
29
|
db.exec(`CREATE TABLE IF NOT EXISTS agent_messages (seq INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, body TEXT)`);
|
|
30
|
+
// Every store read is scoped to one session; without this index each one scanned the
|
|
31
|
+
// messages of ALL sessions in the file (#168). IF NOT EXISTS: existing DBs gain it.
|
|
32
|
+
db.exec(`CREATE INDEX IF NOT EXISTS agent_messages_session ON agent_messages (session_id, seq)`);
|
|
28
33
|
// NOTE: PRIMARY KEY (session_id, id) — never id alone. The store view scopes
|
|
29
34
|
// every query by session, so a step id can't leak across sessions.
|
|
30
35
|
db.exec(`CREATE TABLE IF NOT EXISTS agent_steps (session_id TEXT, id TEXT, output TEXT, PRIMARY KEY (session_id, id))`);
|
|
@@ -47,8 +52,11 @@ class SqliteSessionStore implements SessionStore {
|
|
|
47
52
|
return (this.db.query("SELECT body FROM agent_messages WHERE session_id = ? ORDER BY seq").all(this.sid) as { body: string }[])
|
|
48
53
|
.map((r) => JSON.parse(r.body));
|
|
49
54
|
}
|
|
55
|
+
// Asked once per turn start: probe for the one row instead of parsing the transcript.
|
|
50
56
|
hasOpeningMessage(turnId: string): boolean {
|
|
51
|
-
return this.
|
|
57
|
+
return this.db
|
|
58
|
+
.query("SELECT 1 FROM agent_messages WHERE session_id = ? AND json_extract(body, '$.turnId') = ? AND json_extract(body, '$.role') IN ('user', 'trigger') LIMIT 1")
|
|
59
|
+
.get(this.sid, turnId) != null; // bun:sqlite misses with null, node:sqlite with undefined
|
|
52
60
|
}
|
|
53
61
|
getStep(id: string): unknown | undefined {
|
|
54
62
|
const r = this.db.query("SELECT output FROM agent_steps WHERE session_id = ? AND id = ?").get(this.sid, id) as { output: string } | undefined;
|
|
@@ -106,37 +114,96 @@ class InProcEventSink implements EventSink {
|
|
|
106
114
|
private subs = new Set<(e: TurnEvent) => void>();
|
|
107
115
|
emit(e: TurnEvent) { this.subs.forEach((cb) => { try { cb(e); } catch { /* a bad subscriber must not break emit */ } }); }
|
|
108
116
|
subscribe(cb: (e: TurnEvent) => void): () => void { this.subs.add(cb); return () => this.subs.delete(cb); }
|
|
117
|
+
get size(): number { return this.subs.size; }
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
// `instructions` (the agent's system prompt) is injected into the model per turn
|
|
112
121
|
// by the runtime (withSystem) — single-sourced on the def, not baked into `model`.
|
|
113
|
-
export type AgentDef = { model: Model; tools: Tool[]; instructions?: string; channelInstructions?: Record<string, string> };
|
|
122
|
+
export type AgentDef = { model: Model; tools: Tool[]; instructions?: string; channelInstructions?: Record<string, string | ChannelPolicy> };
|
|
123
|
+
|
|
124
|
+
// The runtime-side def for an assembled AgentDefinition (#173): the tools (channel
|
|
125
|
+
// capability tools and read_skill included), the system prompt (instructions + the
|
|
126
|
+
// skill index) and the per-surface policies all come from the ONE definition that
|
|
127
|
+
// mountAgent mounts, so the engine and the channels can't drift apart. Only the model
|
|
128
|
+
// is the host's choice.
|
|
129
|
+
// createNativeRuntime({ [agent.name]: toAgentDef(agent, anthropic({ … })) })
|
|
130
|
+
export function toAgentDef(agent: AgentDefinition, model: Model): AgentDef {
|
|
131
|
+
return {
|
|
132
|
+
model,
|
|
133
|
+
tools: agent.tools,
|
|
134
|
+
instructions: buildSystemPrompt(agent),
|
|
135
|
+
...(agent.channelInstructions ? { channelInstructions: agent.channelInstructions } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type NativeRuntimeOptions = {
|
|
140
|
+
// Soft cap on memoized session actors (#174), default 1000. Past it, the least recently
|
|
141
|
+
// used IDLE actors (no turn running or queued, no reset pending, no live subscriber) are
|
|
142
|
+
// dropped; their state is in SQLite, so the next session() rebuilds them transparently.
|
|
143
|
+
// Busy actors are never dropped, so the count can exceed the cap while they run.
|
|
144
|
+
maxSessions?: number;
|
|
145
|
+
};
|
|
114
146
|
|
|
115
147
|
// The native Runtime: a registry of agent definitions over one SQLite handle,
|
|
116
|
-
// handing out (and memoizing) an AgentSession actor per (agent, id).
|
|
148
|
+
// handing out (and memoizing) an AgentSession actor per (agent, id). The memo is an
|
|
149
|
+
// LRU bounded by maxSessions — with slackChannel every thread is a session, so an
|
|
150
|
+
// unbounded memo grew by one actor per thread for the life of the process.
|
|
151
|
+
// Callers must not hold an AgentSession across an await and start turns on it later:
|
|
152
|
+
// if it was evicted meanwhile, a fresh actor for the same session would run turns
|
|
153
|
+
// unserialized with it. Call session() at the point of use (mountAgent does).
|
|
117
154
|
export class NativeRuntime implements Runtime {
|
|
118
|
-
private actors = new Map<string, AgentSession>();
|
|
155
|
+
private actors = new Map<string, { session: AgentSession; sink: InProcEventSink }>();
|
|
119
156
|
private readonly agents: Record<string, AgentDef>;
|
|
120
157
|
private readonly db: SyncSqlite;
|
|
158
|
+
private readonly maxSessions: number;
|
|
121
159
|
|
|
122
|
-
constructor(agents: Record<string, AgentDef>, db: SyncSqlite) {
|
|
160
|
+
constructor(agents: Record<string, AgentDef>, db: SyncSqlite, opts: NativeRuntimeOptions = {}) {
|
|
123
161
|
assertCoreRuntimeVersion("NativeRuntime"); // #94: fail power-on, not mid-turn
|
|
124
162
|
this.agents = agents;
|
|
125
163
|
this.db = db;
|
|
164
|
+
const max = opts.maxSessions ?? 1000;
|
|
165
|
+
// At least one actor is always retained (the one being handed out), so a cap below 1
|
|
166
|
+
// means nothing, and NaN would compare false and evict every idle actor on each miss.
|
|
167
|
+
// Infinity is allowed: an explicit "no cap".
|
|
168
|
+
if (!(Number.isInteger(max) && max >= 1) && max !== Infinity) {
|
|
169
|
+
throw new RangeError(`NativeRuntime: maxSessions must be an integer >= 1 (or Infinity for no cap), got ${max}`);
|
|
170
|
+
}
|
|
171
|
+
this.maxSessions = max;
|
|
126
172
|
initSchema(db);
|
|
127
173
|
}
|
|
128
174
|
|
|
175
|
+
agentDef(name: string): AgentDef | undefined { return this.agents[name]; }
|
|
176
|
+
|
|
129
177
|
session(agent: string, id: string): AgentSession {
|
|
130
178
|
const key = `${agent}:${id}`;
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
179
|
+
const hit = this.actors.get(key);
|
|
180
|
+
if (hit) {
|
|
181
|
+
// most recently used → the tail (Map iteration order is insertion order)
|
|
182
|
+
this.actors.delete(key);
|
|
183
|
+
this.actors.set(key, hit);
|
|
184
|
+
return hit.session;
|
|
185
|
+
}
|
|
186
|
+
const def = this.agents[agent];
|
|
187
|
+
if (!def) throw new Error(`unknown agent: ${agent}`);
|
|
188
|
+
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
189
|
+
const sink = new InProcEventSink();
|
|
190
|
+
const session = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), sink, model, def.tools, this, def.channelInstructions);
|
|
191
|
+
this.evictIdle();
|
|
192
|
+
this.actors.set(key, { session, sink });
|
|
193
|
+
return session;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Number of memoized actors (observability / tests).
|
|
197
|
+
get sessionCount(): number { return this.actors.size; }
|
|
198
|
+
|
|
199
|
+
// Drop least recently used idle actors until one more fits under the cap. Runs before
|
|
200
|
+
// the new actor is inserted, so the actor being handed out is never a candidate.
|
|
201
|
+
private evictIdle() {
|
|
202
|
+
if (this.actors.size < this.maxSessions) return;
|
|
203
|
+
for (const [key, a] of this.actors) {
|
|
204
|
+
if (this.actors.size < this.maxSessions) break;
|
|
205
|
+
if (a.session.idle() && a.sink.size === 0) this.actors.delete(key);
|
|
138
206
|
}
|
|
139
|
-
return a;
|
|
140
207
|
}
|
|
141
208
|
}
|
|
142
209
|
|
|
@@ -146,14 +213,16 @@ export class NativeRuntime implements Runtime {
|
|
|
146
213
|
export async function createNativeRuntime(
|
|
147
214
|
agents: Record<string, AgentDef>,
|
|
148
215
|
path = ":memory:",
|
|
216
|
+
opts: NativeRuntimeOptions = {},
|
|
149
217
|
): Promise<NativeRuntime> {
|
|
150
|
-
return new NativeRuntime(agents, await openLocalSqliteSync(path));
|
|
218
|
+
return new NativeRuntime(agents, await openLocalSqliteSync(path), opts);
|
|
151
219
|
}
|
|
152
220
|
|
|
153
221
|
// ── memory backend — in-process, ephemeral (no DB, no disk) ───────────────────
|
|
154
222
|
// The lightest "no Durable Object, no persistence" option: great for dev, tests,
|
|
155
223
|
// and stateless previews. Same engine + seams; state is a Map that dies with the
|
|
156
|
-
// process
|
|
224
|
+
// process — and is never evicted (the actor IS the state), so it grows with every
|
|
225
|
+
// session: not for a long-running host with unbounded sessions. (For durability pick `native` on a long-running host or the Durable
|
|
157
226
|
// Object target on the edge.)
|
|
158
227
|
class MemorySessionStore implements SessionStore {
|
|
159
228
|
private msgs: Msg[] = [];
|
|
@@ -191,6 +260,7 @@ export class MemoryRuntime implements Runtime {
|
|
|
191
260
|
assertCoreRuntimeVersion("MemoryRuntime"); // #94: fail power-on, not mid-turn
|
|
192
261
|
this.agents = agents;
|
|
193
262
|
}
|
|
263
|
+
agentDef(name: string): AgentDef | undefined { return this.agents[name]; }
|
|
194
264
|
session(agent: string, id: string): AgentSession {
|
|
195
265
|
const key = `${agent}:${id}`;
|
|
196
266
|
let a = this.actors.get(key);
|
|
@@ -199,7 +269,9 @@ export class MemoryRuntime implements Runtime {
|
|
|
199
269
|
if (!def) throw new Error(`unknown agent: ${agent}`);
|
|
200
270
|
const store = new MemorySessionStore();
|
|
201
271
|
this.stores.set(key, store);
|
|
202
|
-
|
|
272
|
+
// Same def handling as NativeRuntime: switching backend must not change behavior.
|
|
273
|
+
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
274
|
+
a = new AgentSession(agent, id, store, new InProcEventSink(), model, def.tools, this, def.channelInstructions);
|
|
203
275
|
this.actors.set(key, a);
|
|
204
276
|
}
|
|
205
277
|
return a;
|
|
@@ -217,16 +289,39 @@ export type AgentBackend = "native" | "memory" | "durable";
|
|
|
217
289
|
// choice is explicit and a mis-selection fails loudly.
|
|
218
290
|
export async function createAgentRuntime(
|
|
219
291
|
agents: Record<string, AgentDef>,
|
|
220
|
-
opts: { backend?: AgentBackend; path?: string } = {},
|
|
292
|
+
opts: { backend?: AgentBackend; path?: string; maxSessions?: number } = {},
|
|
221
293
|
): Promise<Runtime> {
|
|
222
294
|
const backend = opts.backend ?? "native";
|
|
223
295
|
if (backend === "memory") return new MemoryRuntime(agents);
|
|
224
|
-
if (backend === "native") return createNativeRuntime(agents, opts.path);
|
|
296
|
+
if (backend === "native") return createNativeRuntime(agents, opts.path, { maxSessions: opts.maxSessions });
|
|
225
297
|
throw new Error("backend 'durable' is the Cloudflare Durable Object target — construct AgentDurableObject in your worker, not via createAgentRuntime");
|
|
226
298
|
}
|
|
227
299
|
|
|
228
|
-
//
|
|
229
|
-
//
|
|
300
|
+
// #173: the engine runs the runtime's AgentDef, the channels see the AgentDefinition —
|
|
301
|
+
// declared separately by hand, they drift silently (a tool the model can't call, a
|
|
302
|
+
// channel capability tool the engine never got). Say so once at mount. Only the
|
|
303
|
+
// in-process runtimes expose their def; tools compare by name.
|
|
304
|
+
function warnOnDrift(agent: AgentDefinition, runtime: Runtime) {
|
|
305
|
+
const def = (runtime as { agentDef?: (name: string) => AgentDef | undefined }).agentDef?.(agent.name);
|
|
306
|
+
if (!def) return;
|
|
307
|
+
const names = (tools: Tool[]) => tools.map((t) => t.spec.name).sort();
|
|
308
|
+
const want = names(agent.tools), have = names(def.tools);
|
|
309
|
+
if (want.join("\0") !== have.join("\0")) {
|
|
310
|
+
const missing = want.filter((n) => !have.includes(n)), extra = have.filter((n) => !want.includes(n));
|
|
311
|
+
console.warn(
|
|
312
|
+
`[june] mountAgent("${agent.name}"): the runtime's tools differ from the agent definition's` +
|
|
313
|
+
(missing.length ? ` — missing from the runtime: ${missing.join(", ")}` : "") +
|
|
314
|
+
(extra.length ? ` — only in the runtime: ${extra.join(", ")}` : "") +
|
|
315
|
+
`. Build the runtime entry with toAgentDef(agent, model) so both come from one definition.`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Mount a discovered agent on a runtime. Builds the ChannelContext the channels drive
|
|
321
|
+
// turns through — run, runDetached, runStream, resumeStream and resetSession, each over
|
|
322
|
+
// the runtime's in-process session. Not provided: runDelivered/resumeDelivered, which
|
|
323
|
+
// exist to escape the edge waitUntil ceiling (a native host has none; channels fall back
|
|
324
|
+
// to runStream/resumeStream without them). Exposes:
|
|
230
325
|
// • surface(req) — the composable agent surface for June's router: a framework
|
|
231
326
|
// chat endpoint at `chatPath` (POST {message, session?} → a turn) PLUS the
|
|
232
327
|
// discovered channels; returns null when the request isn't an agent route.
|
|
@@ -244,6 +339,7 @@ export function mountAgent(
|
|
|
244
339
|
} {
|
|
245
340
|
const chatPath = opts.chatPath ?? "/message";
|
|
246
341
|
const channelsOn = opts.channels ?? true;
|
|
342
|
+
warnOnDrift(agent, runtime);
|
|
247
343
|
const ctx: ChannelContext = {
|
|
248
344
|
agent,
|
|
249
345
|
services: opts.services, // same DI bag reachable from channel hooks (parity with durableChannelSurface)
|
|
@@ -254,6 +350,22 @@ export function mountAgent(
|
|
|
254
350
|
// written against ctx.runDetached behaves identically on both targets.
|
|
255
351
|
runDetached: async (message, o) =>
|
|
256
352
|
runtime.session(agent.name, o?.session ?? "default").start({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger, replace: o?.replace }),
|
|
353
|
+
// LIVE (#169): the turn's event stream, so a channel renders as the turn runs —
|
|
354
|
+
// slackChannel({ stream: true }) edits in place here exactly as on the edge, instead of
|
|
355
|
+
// silently degrading to one post at the end. Lazy like the DO's (the turn starts on the
|
|
356
|
+
// first pull); start()/resume() and the subscription run with nothing awaited between,
|
|
357
|
+
// so no event can emit unobserved.
|
|
358
|
+
runStream: async function* (message, o) {
|
|
359
|
+
const session = runtime.session(agent.name, o?.session ?? "default");
|
|
360
|
+
const { turnId } = session.start({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger, replace: o?.replace });
|
|
361
|
+
yield* observeTurnEvents(session, turnId);
|
|
362
|
+
},
|
|
363
|
+
// HITL: answer a parked turn and stream its continuation (the approval-button path).
|
|
364
|
+
resumeStream: async function* (o) {
|
|
365
|
+
const session = runtime.session(agent.name, o.session ?? "default");
|
|
366
|
+
const { turnId } = session.resume(o.turnId, o.inputId, o.input, { by: o.by });
|
|
367
|
+
yield* observeTurnEvents(session, turnId);
|
|
368
|
+
},
|
|
257
369
|
// SESSION RESET (#129): same seam as durableChannelSurface — the in-process runtimes'
|
|
258
370
|
// stores implement archival, so a channel written against ctx.resetSession behaves
|
|
259
371
|
// identically on both targets.
|
package/src/app.ts
CHANGED
|
@@ -28,8 +28,7 @@ import { runWithTrace, type RequestTrace } from "@junejs/core/instrumentation";
|
|
|
28
28
|
import { findMiddlewareFile, isResourceFile, listRoutes, matchRouteTree, resolveNotFound, routeFiles, type SegmentMatch } from "./router";
|
|
29
29
|
import { createPipeline, type ExtraHandler, type LayoutComponent, type MiddlewareHandler, type Pipeline, type Resolved, type ResourceHandler } from "./pipeline";
|
|
30
30
|
import { discoverAgent } from "./agent-discover";
|
|
31
|
-
import { createAgentRuntime, mountAgent } from "./agent-native";
|
|
32
|
-
import { buildSystemPrompt } from "@junejs/core/agent-config";
|
|
31
|
+
import { createAgentRuntime, mountAgent, toAgentDef } from "./agent-native";
|
|
33
32
|
import { anthropic } from "@junejs/core/agent-models";
|
|
34
33
|
import { resolveBoundary } from "./segment";
|
|
35
34
|
import { memoizeResources } from "./resources";
|
|
@@ -237,10 +236,9 @@ export function createApp({ appDir: appDirInput, config = {} }: CreateAppOptions
|
|
|
237
236
|
// instructions live on the def (single source); the runtime injects them
|
|
238
237
|
// into the model per turn via withSystem — no need to bake into anthropic().
|
|
239
238
|
const model = anthropic({ model: def.model });
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
);
|
|
239
|
+
// toAgentDef: tools, prompt AND the per-surface policies (#149) from the one
|
|
240
|
+
// definition — channelInstructions was previously dropped on this path (#173).
|
|
241
|
+
const rt = await createAgentRuntime({ [def.name]: toAgentDef(def, model) }, { backend });
|
|
244
242
|
const mounted = mountAgent(def, rt, { chatPath: agent.runtime.chat.path, channels: agent.runtime.channels });
|
|
245
243
|
agentSurface = (req) => mounted.surface(req);
|
|
246
244
|
await mounted.startAll();
|