@junejs/server 1.0.0-dev.20 → 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/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/core-version.js +1 -1
- package/dist/turn-events.d.ts +5 -0
- package/dist/turn-events.js +46 -0
- package/package.json +2 -2
- package/src/agent-discover.ts +2 -2
- package/src/agent-durable.ts +1 -39
- package/src/agent-native.ts +133 -21
- package/src/app.ts +4 -6
- package/src/core-version.ts +1 -1
- package/src/turn-events.ts +43 -0
package/dist/agent-durable.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { observeTurnEvents } from "./turn-events.js";
|
|
1
2
|
import { isolateLocal } from "./isolate-local.js";
|
|
2
3
|
import { assertCoreRuntimeVersion } from "./core-version.js";
|
|
3
4
|
import { DeliverUnsupportedError, actionToTool, channelDispatch, resolveChannel } from "@junejs/core/agent-config";
|
|
@@ -373,49 +374,6 @@ function sseTurnStream(session, turnId) {
|
|
|
373
374
|
}
|
|
374
375
|
});
|
|
375
376
|
}
|
|
376
|
-
function observeTurnEvents(session, turnId) {
|
|
377
|
-
const queue = [];
|
|
378
|
-
let terminal = false;
|
|
379
|
-
let notify;
|
|
380
|
-
const unsub = session.observe((e) => {
|
|
381
|
-
queue.push(e);
|
|
382
|
-
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") terminal = true;
|
|
383
|
-
notify?.();
|
|
384
|
-
}, { turnId });
|
|
385
|
-
return { [Symbol.asyncIterator]() {
|
|
386
|
-
return {
|
|
387
|
-
async next() {
|
|
388
|
-
for (;;) {
|
|
389
|
-
const e = queue.shift();
|
|
390
|
-
if (e) return {
|
|
391
|
-
value: e,
|
|
392
|
-
done: false
|
|
393
|
-
};
|
|
394
|
-
if (terminal) {
|
|
395
|
-
unsub();
|
|
396
|
-
return {
|
|
397
|
-
value: void 0,
|
|
398
|
-
done: true
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
await new Promise((resolve) => {
|
|
402
|
-
notify = resolve;
|
|
403
|
-
});
|
|
404
|
-
notify = void 0;
|
|
405
|
-
}
|
|
406
|
-
},
|
|
407
|
-
async return() {
|
|
408
|
-
unsub();
|
|
409
|
-
terminal = true;
|
|
410
|
-
queue.length = 0;
|
|
411
|
-
return {
|
|
412
|
-
value: void 0,
|
|
413
|
-
done: true
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
} };
|
|
418
|
-
}
|
|
419
377
|
async function sseTurnFinalText(res) {
|
|
420
378
|
const ct = res.headers.get("content-type") ?? "";
|
|
421
379
|
if (!res.body || !ct.includes("text/event-stream")) {
|
package/dist/agent-native.d.ts
CHANGED
|
@@ -1,32 +1,42 @@
|
|
|
1
1
|
import { SyncSqlite } from "./sqlite-driver.js";
|
|
2
2
|
import { AgentDefinition, ChannelContext } from "@junejs/core/agent-config";
|
|
3
|
-
import { AgentSession, Model, Runtime, Tool } from "@junejs/core/agent-runtime";
|
|
3
|
+
import { AgentSession, ChannelPolicy, Model, Runtime, Tool } from "@junejs/core/agent-runtime";
|
|
4
4
|
//#region src/agent-native.d.ts
|
|
5
5
|
type AgentDef = {
|
|
6
6
|
model: Model;
|
|
7
7
|
tools: Tool[];
|
|
8
8
|
instructions?: string;
|
|
9
|
-
channelInstructions?: Record<string, string>;
|
|
9
|
+
channelInstructions?: Record<string, string | ChannelPolicy>;
|
|
10
|
+
};
|
|
11
|
+
declare function toAgentDef(agent: AgentDefinition, model: Model): AgentDef;
|
|
12
|
+
type NativeRuntimeOptions = {
|
|
13
|
+
maxSessions?: number;
|
|
10
14
|
};
|
|
11
15
|
declare class NativeRuntime implements Runtime {
|
|
12
16
|
private actors;
|
|
13
17
|
private readonly agents;
|
|
14
18
|
private readonly db;
|
|
15
|
-
|
|
19
|
+
private readonly maxSessions;
|
|
20
|
+
constructor(agents: Record<string, AgentDef>, db: SyncSqlite, opts?: NativeRuntimeOptions);
|
|
21
|
+
agentDef(name: string): AgentDef | undefined;
|
|
16
22
|
session(agent: string, id: string): AgentSession;
|
|
23
|
+
get sessionCount(): number;
|
|
24
|
+
private evictIdle;
|
|
17
25
|
}
|
|
18
|
-
declare function createNativeRuntime(agents: Record<string, AgentDef>, path?: string): Promise<NativeRuntime>;
|
|
26
|
+
declare function createNativeRuntime(agents: Record<string, AgentDef>, path?: string, opts?: NativeRuntimeOptions): Promise<NativeRuntime>;
|
|
19
27
|
declare class MemoryRuntime implements Runtime {
|
|
20
28
|
private actors;
|
|
21
29
|
private stores;
|
|
22
30
|
private readonly agents;
|
|
23
31
|
constructor(agents: Record<string, AgentDef>);
|
|
32
|
+
agentDef(name: string): AgentDef | undefined;
|
|
24
33
|
session(agent: string, id: string): AgentSession;
|
|
25
34
|
}
|
|
26
35
|
type AgentBackend = "native" | "memory" | "durable";
|
|
27
36
|
declare function createAgentRuntime(agents: Record<string, AgentDef>, opts?: {
|
|
28
37
|
backend?: AgentBackend;
|
|
29
38
|
path?: string;
|
|
39
|
+
maxSessions?: number;
|
|
30
40
|
}): Promise<Runtime>;
|
|
31
41
|
declare function mountAgent(agent: AgentDefinition, runtime: Runtime, opts?: {
|
|
32
42
|
chatPath?: string;
|
|
@@ -39,4 +49,4 @@ declare function mountAgent(agent: AgentDefinition, runtime: Runtime, opts?: {
|
|
|
39
49
|
ctx: ChannelContext;
|
|
40
50
|
};
|
|
41
51
|
//#endregion
|
|
42
|
-
export { AgentBackend, AgentDef, MemoryRuntime, NativeRuntime, createAgentRuntime, createNativeRuntime, mountAgent };
|
|
52
|
+
export { AgentBackend, AgentDef, MemoryRuntime, NativeRuntime, NativeRuntimeOptions, createAgentRuntime, createNativeRuntime, mountAgent, toAgentDef };
|
package/dist/agent-native.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { observeTurnEvents } from "./turn-events.js";
|
|
1
2
|
import { assertCoreRuntimeVersion } from "./core-version.js";
|
|
2
3
|
import { openLocalSqliteSync } from "./sqlite-driver.js";
|
|
3
|
-
import { channelFetch } from "@junejs/core/agent-config";
|
|
4
|
+
import { buildSystemPrompt, channelFetch } from "@junejs/core/agent-config";
|
|
4
5
|
import { AgentSession, withSystem } from "@junejs/core/agent-runtime";
|
|
5
6
|
//#region src/agent-native.ts
|
|
6
7
|
function initSchema(db) {
|
|
7
8
|
db.exec(`CREATE TABLE IF NOT EXISTS agent_sessions (session_id TEXT PRIMARY KEY, status TEXT)`);
|
|
8
9
|
db.exec(`CREATE TABLE IF NOT EXISTS agent_messages (seq INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, body TEXT)`);
|
|
10
|
+
db.exec(`CREATE INDEX IF NOT EXISTS agent_messages_session ON agent_messages (session_id, seq)`);
|
|
9
11
|
db.exec(`CREATE TABLE IF NOT EXISTS agent_steps (session_id TEXT, id TEXT, output TEXT, PRIMARY KEY (session_id, id))`);
|
|
10
12
|
}
|
|
11
13
|
var SqliteSessionStore = class {
|
|
@@ -22,7 +24,7 @@ var SqliteSessionStore = class {
|
|
|
22
24
|
return this.db.query("SELECT body FROM agent_messages WHERE session_id = ? ORDER BY seq").all(this.sid).map((r) => JSON.parse(r.body));
|
|
23
25
|
}
|
|
24
26
|
hasOpeningMessage(turnId) {
|
|
25
|
-
return this.
|
|
27
|
+
return this.db.query("SELECT 1 FROM agent_messages WHERE session_id = ? AND json_extract(body, '$.turnId') = ? AND json_extract(body, '$.role') IN ('user', 'trigger') LIMIT 1").get(this.sid, turnId) != null;
|
|
26
28
|
}
|
|
27
29
|
getStep(id) {
|
|
28
30
|
const r = this.db.query("SELECT output FROM agent_steps WHERE session_id = ? AND id = ?").get(this.sid, id);
|
|
@@ -83,32 +85,68 @@ var InProcEventSink = class {
|
|
|
83
85
|
this.subs.add(cb);
|
|
84
86
|
return () => this.subs.delete(cb);
|
|
85
87
|
}
|
|
88
|
+
get size() {
|
|
89
|
+
return this.subs.size;
|
|
90
|
+
}
|
|
86
91
|
};
|
|
92
|
+
function toAgentDef(agent, model) {
|
|
93
|
+
return {
|
|
94
|
+
model,
|
|
95
|
+
tools: agent.tools,
|
|
96
|
+
instructions: buildSystemPrompt(agent),
|
|
97
|
+
...agent.channelInstructions ? { channelInstructions: agent.channelInstructions } : {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
87
100
|
var NativeRuntime = class {
|
|
88
101
|
actors = /* @__PURE__ */ new Map();
|
|
89
102
|
agents;
|
|
90
103
|
db;
|
|
91
|
-
|
|
104
|
+
maxSessions;
|
|
105
|
+
constructor(agents, db, opts = {}) {
|
|
92
106
|
assertCoreRuntimeVersion("NativeRuntime");
|
|
93
107
|
this.agents = agents;
|
|
94
108
|
this.db = db;
|
|
109
|
+
const max = opts.maxSessions ?? 1e3;
|
|
110
|
+
if (!(Number.isInteger(max) && max >= 1) && max !== Infinity) throw new RangeError(`NativeRuntime: maxSessions must be an integer >= 1 (or Infinity for no cap), got ${max}`);
|
|
111
|
+
this.maxSessions = max;
|
|
95
112
|
initSchema(db);
|
|
96
113
|
}
|
|
114
|
+
agentDef(name) {
|
|
115
|
+
return this.agents[name];
|
|
116
|
+
}
|
|
97
117
|
session(agent, id) {
|
|
98
118
|
const key = `${agent}:${id}`;
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
const hit = this.actors.get(key);
|
|
120
|
+
if (hit) {
|
|
121
|
+
this.actors.delete(key);
|
|
122
|
+
this.actors.set(key, hit);
|
|
123
|
+
return hit.session;
|
|
124
|
+
}
|
|
125
|
+
const def = this.agents[agent];
|
|
126
|
+
if (!def) throw new Error(`unknown agent: ${agent}`);
|
|
127
|
+
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
128
|
+
const sink = new InProcEventSink();
|
|
129
|
+
const session = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), sink, model, def.tools, this, def.channelInstructions);
|
|
130
|
+
this.evictIdle();
|
|
131
|
+
this.actors.set(key, {
|
|
132
|
+
session,
|
|
133
|
+
sink
|
|
134
|
+
});
|
|
135
|
+
return session;
|
|
136
|
+
}
|
|
137
|
+
get sessionCount() {
|
|
138
|
+
return this.actors.size;
|
|
139
|
+
}
|
|
140
|
+
evictIdle() {
|
|
141
|
+
if (this.actors.size < this.maxSessions) return;
|
|
142
|
+
for (const [key, a] of this.actors) {
|
|
143
|
+
if (this.actors.size < this.maxSessions) break;
|
|
144
|
+
if (a.session.idle() && a.sink.size === 0) this.actors.delete(key);
|
|
106
145
|
}
|
|
107
|
-
return a;
|
|
108
146
|
}
|
|
109
147
|
};
|
|
110
|
-
async function createNativeRuntime(agents, path = ":memory:") {
|
|
111
|
-
return new NativeRuntime(agents, await openLocalSqliteSync(path));
|
|
148
|
+
async function createNativeRuntime(agents, path = ":memory:", opts = {}) {
|
|
149
|
+
return new NativeRuntime(agents, await openLocalSqliteSync(path), opts);
|
|
112
150
|
}
|
|
113
151
|
var MemorySessionStore = class {
|
|
114
152
|
msgs = [];
|
|
@@ -165,6 +203,9 @@ var MemoryRuntime = class {
|
|
|
165
203
|
assertCoreRuntimeVersion("MemoryRuntime");
|
|
166
204
|
this.agents = agents;
|
|
167
205
|
}
|
|
206
|
+
agentDef(name) {
|
|
207
|
+
return this.agents[name];
|
|
208
|
+
}
|
|
168
209
|
session(agent, id) {
|
|
169
210
|
const key = `${agent}:${id}`;
|
|
170
211
|
let a = this.actors.get(key);
|
|
@@ -173,7 +214,8 @@ var MemoryRuntime = class {
|
|
|
173
214
|
if (!def) throw new Error(`unknown agent: ${agent}`);
|
|
174
215
|
const store = new MemorySessionStore();
|
|
175
216
|
this.stores.set(key, store);
|
|
176
|
-
|
|
217
|
+
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
218
|
+
a = new AgentSession(agent, id, store, new InProcEventSink(), model, def.tools, this, def.channelInstructions);
|
|
177
219
|
this.actors.set(key, a);
|
|
178
220
|
}
|
|
179
221
|
return a;
|
|
@@ -182,12 +224,23 @@ var MemoryRuntime = class {
|
|
|
182
224
|
async function createAgentRuntime(agents, opts = {}) {
|
|
183
225
|
const backend = opts.backend ?? "native";
|
|
184
226
|
if (backend === "memory") return new MemoryRuntime(agents);
|
|
185
|
-
if (backend === "native") return createNativeRuntime(agents, opts.path);
|
|
227
|
+
if (backend === "native") return createNativeRuntime(agents, opts.path, { maxSessions: opts.maxSessions });
|
|
186
228
|
throw new Error("backend 'durable' is the Cloudflare Durable Object target — construct AgentDurableObject in your worker, not via createAgentRuntime");
|
|
187
229
|
}
|
|
230
|
+
function warnOnDrift(agent, runtime) {
|
|
231
|
+
const def = runtime.agentDef?.(agent.name);
|
|
232
|
+
if (!def) return;
|
|
233
|
+
const names = (tools) => tools.map((t) => t.spec.name).sort();
|
|
234
|
+
const want = names(agent.tools), have = names(def.tools);
|
|
235
|
+
if (want.join("\0") !== have.join("\0")) {
|
|
236
|
+
const missing = want.filter((n) => !have.includes(n)), extra = have.filter((n) => !want.includes(n));
|
|
237
|
+
console.warn(`[june] mountAgent("${agent.name}"): the runtime's tools differ from the agent definition's` + (missing.length ? ` — missing from the runtime: ${missing.join(", ")}` : "") + (extra.length ? ` — only in the runtime: ${extra.join(", ")}` : "") + `. Build the runtime entry with toAgentDef(agent, model) so both come from one definition.`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
188
240
|
function mountAgent(agent, runtime, opts = {}) {
|
|
189
241
|
const chatPath = opts.chatPath ?? "/message";
|
|
190
242
|
const channelsOn = opts.channels ?? true;
|
|
243
|
+
warnOnDrift(agent, runtime);
|
|
191
244
|
const ctx = {
|
|
192
245
|
agent,
|
|
193
246
|
services: opts.services,
|
|
@@ -205,6 +258,22 @@ function mountAgent(agent, runtime, opts = {}) {
|
|
|
205
258
|
trigger: o?.trigger,
|
|
206
259
|
replace: o?.replace
|
|
207
260
|
}),
|
|
261
|
+
runStream: async function* (message, o) {
|
|
262
|
+
const session = runtime.session(agent.name, o?.session ?? "default");
|
|
263
|
+
const { turnId } = session.start({
|
|
264
|
+
turnId: o?.turnId,
|
|
265
|
+
userText: message,
|
|
266
|
+
event: o?.event,
|
|
267
|
+
trigger: o?.trigger,
|
|
268
|
+
replace: o?.replace
|
|
269
|
+
});
|
|
270
|
+
yield* observeTurnEvents(session, turnId);
|
|
271
|
+
},
|
|
272
|
+
resumeStream: async function* (o) {
|
|
273
|
+
const session = runtime.session(agent.name, o.session ?? "default");
|
|
274
|
+
const { turnId } = session.resume(o.turnId, o.inputId, o.input, { by: o.by });
|
|
275
|
+
yield* observeTurnEvents(session, turnId);
|
|
276
|
+
},
|
|
208
277
|
resetSession: (o) => runtime.session(agent.name, o?.session ?? "default").reset()
|
|
209
278
|
};
|
|
210
279
|
const channels = channelFetch(agent, ctx);
|
|
@@ -226,4 +295,4 @@ function mountAgent(agent, runtime, opts = {}) {
|
|
|
226
295
|
};
|
|
227
296
|
}
|
|
228
297
|
//#endregion
|
|
229
|
-
export { MemoryRuntime, NativeRuntime, createAgentRuntime, createNativeRuntime, mountAgent };
|
|
298
|
+
export { MemoryRuntime, NativeRuntime, createAgentRuntime, createNativeRuntime, mountAgent, toAgentDef };
|
package/dist/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { discoverAgent } from "./agent-discover.js";
|
|
2
|
-
import { createAgentRuntime, mountAgent } from "./agent-native.js";
|
|
2
|
+
import { createAgentRuntime, mountAgent, toAgentDef } from "./agent-native.js";
|
|
3
3
|
import { findMiddlewareFile, isResourceFile, listRoutes, matchRouteTree, resolveNotFound, routeFiles } from "./router.js";
|
|
4
4
|
import { createPipeline } from "./pipeline.js";
|
|
5
5
|
import { resolveBoundary } from "./segment.js";
|
|
@@ -13,7 +13,6 @@ import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
|
13
13
|
import { readFile } from "node:fs/promises";
|
|
14
14
|
import { dirname, join, resolve } from "node:path";
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
|
-
import { buildSystemPrompt } from "@junejs/core/agent-config";
|
|
17
16
|
import { routeFromModule } from "@junejs/core/route";
|
|
18
17
|
import { resolveAgent, resolveClientRouter, resolveSpeculationRules } from "@junejs/core/config";
|
|
19
18
|
import { runWithTrace } from "@junejs/core/instrumentation";
|
|
@@ -125,11 +124,7 @@ function createApp({ appDir: appDirInput, config = {} }) {
|
|
|
125
124
|
const def = await discoverAgent(agentDir);
|
|
126
125
|
const backend = agent.runtime.backend === "durable" ? "native" : agent.runtime.backend;
|
|
127
126
|
const model = anthropic({ model: def.model });
|
|
128
|
-
const mounted = mountAgent(def, await createAgentRuntime({ [def.name]: {
|
|
129
|
-
model,
|
|
130
|
-
tools: def.tools,
|
|
131
|
-
instructions: buildSystemPrompt(def)
|
|
132
|
-
} }, { backend }), {
|
|
127
|
+
const mounted = mountAgent(def, await createAgentRuntime({ [def.name]: toAgentDef(def, model) }, { backend }), {
|
|
133
128
|
chatPath: agent.runtime.chat.path,
|
|
134
129
|
channels: agent.runtime.channels
|
|
135
130
|
});
|
package/dist/core-version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as coreRuntime from "@junejs/core/agent-runtime";
|
|
2
2
|
//#region src/core-version.ts
|
|
3
|
-
const EXPECTED_CORE_RUNTIME_API =
|
|
3
|
+
const EXPECTED_CORE_RUNTIME_API = 2;
|
|
4
4
|
function assertCoreRuntimeVersion(surface) {
|
|
5
5
|
checkCoreRuntimeVersion(surface, coreRuntime.RUNTIME_API_VERSION);
|
|
6
6
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/turn-events.ts
|
|
2
|
+
function observeTurnEvents(session, turnId) {
|
|
3
|
+
const queue = [];
|
|
4
|
+
let terminal = false;
|
|
5
|
+
let notify;
|
|
6
|
+
const unsub = session.observe((e) => {
|
|
7
|
+
queue.push(e);
|
|
8
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") terminal = true;
|
|
9
|
+
notify?.();
|
|
10
|
+
}, { turnId });
|
|
11
|
+
return { [Symbol.asyncIterator]() {
|
|
12
|
+
return {
|
|
13
|
+
async next() {
|
|
14
|
+
for (;;) {
|
|
15
|
+
const e = queue.shift();
|
|
16
|
+
if (e) return {
|
|
17
|
+
value: e,
|
|
18
|
+
done: false
|
|
19
|
+
};
|
|
20
|
+
if (terminal) {
|
|
21
|
+
unsub();
|
|
22
|
+
return {
|
|
23
|
+
value: void 0,
|
|
24
|
+
done: true
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
await new Promise((resolve) => {
|
|
28
|
+
notify = resolve;
|
|
29
|
+
});
|
|
30
|
+
notify = void 0;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async return() {
|
|
34
|
+
unsub();
|
|
35
|
+
terminal = true;
|
|
36
|
+
queue.length = 0;
|
|
37
|
+
return {
|
|
38
|
+
value: void 0,
|
|
39
|
+
done: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
} };
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { observeTurnEvents };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.24",
|
|
4
4
|
"description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"rolldown": "^1.1.0"
|
|
139
139
|
},
|
|
140
140
|
"peerDependencies": {
|
|
141
|
-
"@junejs/core": ">=0.2.0-dev.
|
|
141
|
+
"@junejs/core": ">=0.2.0-dev.44",
|
|
142
142
|
"react": "^19.2.0",
|
|
143
143
|
"react-dom": "^19.2.0",
|
|
144
144
|
"react-server-dom-webpack": "19.2.7",
|
package/src/agent-discover.ts
CHANGED
|
@@ -120,8 +120,8 @@ export async function discoverAgentModule(dir: string): Promise<AgentModule> {
|
|
|
120
120
|
|
|
121
121
|
// Discover an agent from its directory, returning the assembled AgentDefinition
|
|
122
122
|
// (tools already adapted from defineActions; read_skill added if any skills
|
|
123
|
-
// exist). Mount it on a runtime with createNativeRuntime({ [agent.name]:
|
|
124
|
-
//
|
|
123
|
+
// exist). Mount it on a runtime with createNativeRuntime({ [agent.name]:
|
|
124
|
+
// toAgentDef(agent, model) }).
|
|
125
125
|
export async function discoverAgent(dir: string): Promise<AgentDefinition> {
|
|
126
126
|
return assembleAgent(await discoverAgentModule(dir), process.env);
|
|
127
127
|
}
|
package/src/agent-durable.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type TurnFailurePhase,
|
|
34
34
|
} from "@junejs/core/agent-runtime";
|
|
35
35
|
import type { Resources } from "@junejs/core/resources";
|
|
36
|
+
import { observeTurnEvents } from "./turn-events";
|
|
36
37
|
import {
|
|
37
38
|
channelDispatch,
|
|
38
39
|
DeliverUnsupportedError,
|
|
@@ -692,45 +693,6 @@ function sseTurnStream(session: AgentSession, turnId: string): ReadableStream<Ui
|
|
|
692
693
|
});
|
|
693
694
|
}
|
|
694
695
|
|
|
695
|
-
// The IN-PROCESS sibling of sseTurnStream: one turn's TurnEvents as an AsyncIterable, for a
|
|
696
|
-
// consumer living in the SAME isolate as the session (a delivered render). Subscribes eagerly
|
|
697
|
-
// at call time — not at first iteration — so the sseTurnStream timing guarantee carries over
|
|
698
|
-
// (call this synchronously after start(), before any event can emit); events landing before
|
|
699
|
-
// the consumer catches up are buffered. Ends after the turn's terminal event (completed,
|
|
700
|
-
// failed, or input.requested — a park ends this stream; a later /resume is a new one), and an
|
|
701
|
-
// early consumer exit (for-await break/return) unsubscribes rather than buffering forever.
|
|
702
|
-
function observeTurnEvents(session: AgentSession, turnId: string): AsyncIterable<TurnEvent> {
|
|
703
|
-
const queue: TurnEvent[] = [];
|
|
704
|
-
let terminal = false;
|
|
705
|
-
let notify: (() => void) | undefined;
|
|
706
|
-
const unsub = session.observe((e) => {
|
|
707
|
-
queue.push(e);
|
|
708
|
-
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") terminal = true;
|
|
709
|
-
notify?.();
|
|
710
|
-
}, { turnId });
|
|
711
|
-
return {
|
|
712
|
-
[Symbol.asyncIterator]() {
|
|
713
|
-
return {
|
|
714
|
-
async next(): Promise<IteratorResult<TurnEvent>> {
|
|
715
|
-
for (;;) {
|
|
716
|
-
const e = queue.shift();
|
|
717
|
-
if (e) return { value: e, done: false };
|
|
718
|
-
if (terminal) { unsub(); return { value: undefined, done: true }; }
|
|
719
|
-
await new Promise<void>((resolve) => { notify = resolve; });
|
|
720
|
-
notify = undefined;
|
|
721
|
-
}
|
|
722
|
-
},
|
|
723
|
-
async return(): Promise<IteratorResult<TurnEvent>> {
|
|
724
|
-
unsub();
|
|
725
|
-
terminal = true;
|
|
726
|
-
queue.length = 0;
|
|
727
|
-
return { value: undefined, done: true };
|
|
728
|
-
},
|
|
729
|
-
};
|
|
730
|
-
},
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
|
|
734
696
|
// Consume an SSE turn stream to its terminal state: the final text, or throw on failure.
|
|
735
697
|
// The non-streaming path (channels, a JSON chat response) uses this. Guards a non-SSE /
|
|
736
698
|
// body-less upstream response (a misroute or an error) into a clear error, not a TypeError.
|
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();
|
package/src/core-version.ts
CHANGED
|
@@ -15,7 +15,7 @@ import * as coreRuntime from "@junejs/core/agent-runtime";
|
|
|
15
15
|
|
|
16
16
|
// The contract number this server tree is built against. Bump in lockstep with
|
|
17
17
|
// core's RUNTIME_API_VERSION whenever the server↔core runtime contract changes.
|
|
18
|
-
const EXPECTED_CORE_RUNTIME_API =
|
|
18
|
+
const EXPECTED_CORE_RUNTIME_API = 2;
|
|
19
19
|
|
|
20
20
|
export function assertCoreRuntimeVersion(surface: string): void {
|
|
21
21
|
checkCoreRuntimeVersion(surface, (coreRuntime as { RUNTIME_API_VERSION?: number }).RUNTIME_API_VERSION);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// turn-events.ts — one turn's live TurnEvents as an AsyncIterable, for a consumer in the
|
|
2
|
+
// SAME process as the session: the Durable Object's delivered renders (agent-durable.ts)
|
|
3
|
+
// and the native host's ChannelContext.runStream/resumeStream (agent-native.ts).
|
|
4
|
+
|
|
5
|
+
import type { AgentSession, TurnEvent } from "@junejs/core/agent-runtime";
|
|
6
|
+
|
|
7
|
+
// Subscribes eagerly at call time — not at first iteration — so the sseTurnStream timing
|
|
8
|
+
// guarantee carries over (call this synchronously after start()/resume(), before any event
|
|
9
|
+
// can emit); events landing before the consumer catches up are buffered. Ends after the
|
|
10
|
+
// turn's terminal event (completed, failed, cancelled, or input.requested — a park ends
|
|
11
|
+
// this stream; a later resume is a new one), and an early consumer exit (for-await
|
|
12
|
+
// break/return) unsubscribes rather than buffering forever.
|
|
13
|
+
export function observeTurnEvents(session: AgentSession, turnId: string): AsyncIterable<TurnEvent> {
|
|
14
|
+
const queue: TurnEvent[] = [];
|
|
15
|
+
let terminal = false;
|
|
16
|
+
let notify: (() => void) | undefined;
|
|
17
|
+
const unsub = session.observe((e) => {
|
|
18
|
+
queue.push(e);
|
|
19
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") terminal = true;
|
|
20
|
+
notify?.();
|
|
21
|
+
}, { turnId });
|
|
22
|
+
return {
|
|
23
|
+
[Symbol.asyncIterator]() {
|
|
24
|
+
return {
|
|
25
|
+
async next(): Promise<IteratorResult<TurnEvent>> {
|
|
26
|
+
for (;;) {
|
|
27
|
+
const e = queue.shift();
|
|
28
|
+
if (e) return { value: e, done: false };
|
|
29
|
+
if (terminal) { unsub(); return { value: undefined, done: true }; }
|
|
30
|
+
await new Promise<void>((resolve) => { notify = resolve; });
|
|
31
|
+
notify = undefined;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async return(): Promise<IteratorResult<TurnEvent>> {
|
|
35
|
+
unsub();
|
|
36
|
+
terminal = true;
|
|
37
|
+
queue.length = 0;
|
|
38
|
+
return { value: undefined, done: true };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|