@junejs/server 1.0.0-dev.17 → 1.0.0-dev.20

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.
@@ -19,6 +19,7 @@ import {
19
19
  AgentSession,
20
20
  ResumeAuthorizationError,
21
21
  withSystem,
22
+ type ChannelPolicy,
22
23
  type EventSink,
23
24
  type TurnEvent,
24
25
  type Model,
@@ -35,6 +36,7 @@ import type { Resources } from "@junejs/core/resources";
35
36
  import {
36
37
  channelDispatch,
37
38
  DeliverUnsupportedError,
39
+ actionToTool,
38
40
  resolveChannel,
39
41
  type AgentDefinition,
40
42
  type Channel,
@@ -42,6 +44,7 @@ import {
42
44
  type ChannelFactory,
43
45
  type ResumeDeliveryTarget,
44
46
  } from "@junejs/core/agent-config";
47
+ import { connectAll, type Connection } from "@junejs/core/connections";
45
48
  import { ensureScope, runInScope } from "@junejs/db";
46
49
  import { isolateLocal } from "./isolate-local";
47
50
  import { assertCoreRuntimeVersion } from "./core-version";
@@ -198,9 +201,12 @@ export type DoAgentDef = {
198
201
  model: Model;
199
202
  tools: Tool[];
200
203
  instructions?: string;
201
- // Per-channel-source system overlays (see AgentDefinition.channelInstructions) — the
202
- // shared agent branches by real inbound source, no userText marker.
203
- channelInstructions?: Record<string, string>;
204
+ // Per-channel-source turn policies (see AgentDefinition.channelInstructions):
205
+ // an overlay string, or a ChannelPolicy carrying overlay/overlayMode/denyTools
206
+ // (#149 — derived from agent-level instructions.<source>.md + surfaces config
207
+ // by assembleDurable). The shared agent branches by real inbound source, no
208
+ // userText marker.
209
+ channelInstructions?: Record<string, string | ChannelPolicy>;
204
210
  // Channels whose CAPABILITY tools should be available to this session's turns. Their
205
211
  // tools are built HERE, in the DO isolate, from `env` — because a channel's tool `run`
206
212
  // is a closure (over the bot token, etc.) that can't cross the worker→DO RPC. This is
@@ -213,6 +219,12 @@ export type DoAgentDef = {
213
219
  // constructor cannot await, so a provider is resolved lazily at the first
214
220
  // turn (once per DO instance) with this def's `env`.
215
221
  resources?: Resources | ((env: unknown) => Promise<Resources> | Resources);
222
+ // Outbound connection DEFINITIONS (external MCP/OpenAPI servers). Wiring them
223
+ // is network I/O (connectAll), which a DO constructor cannot await — resolved
224
+ // lazily at the first turn, their tools merged into the agent's tool set
225
+ // before any session is constructed. The native equivalent happens at
226
+ // assembly (assembleAgent → connectAll).
227
+ connections?: Connection[];
216
228
  services?: unknown;
217
229
  // Called on every turn.failed for this session — the app's seam for routing turn
218
230
  // failures to its own telemetry (Sentry, a ledger, …). Providing it REPLACES the
@@ -256,13 +268,15 @@ export class AgentDurableObject {
256
268
  private readonly model: Model;
257
269
  private readonly tools: Tool[];
258
270
  private readonly channels: Channel[];
259
- private readonly channelInstructions?: Record<string, string>;
271
+ private readonly channelInstructions?: Record<string, string | ChannelPolicy>;
260
272
  // A bag is used as-is; a provider (bindWorkerResources' shape) is resolved
261
273
  // lazily at the first turn with the def's env — opening a D1 handle is async
262
274
  // and this constructor cannot await. Resolved once, shared across turns (env
263
275
  // is stable per isolate, like bindWorkerResources memoizes per worker isolate).
264
276
  private readonly resourcesInput: DoAgentDef["resources"];
265
277
  private resourcesOpened: Promise<Resources> | undefined;
278
+ private readonly connectionsInput: Connection[] | undefined;
279
+ private connectionsOpened: Promise<void> | undefined;
266
280
  private readonly doEnv: unknown;
267
281
  private readonly services: unknown;
268
282
  private resolveResources(): Promise<Resources> {
@@ -270,11 +284,40 @@ export class AgentDurableObject {
270
284
  typeof this.resourcesInput === "function" ? this.resourcesInput(this.doEnv) : (this.resourcesInput ?? {}),
271
285
  ));
272
286
  }
287
+ // Wire outbound connections once (network I/O the constructor can't await):
288
+ // their tools merge into this.tools BEFORE any session is constructed — the
289
+ // ready() callers hoist this ahead of resolveSession. A failed connection is
290
+ // reported and skipped (connectAll never throws), matching native assembly.
291
+ private resolveConnections(): Promise<void> {
292
+ return (this.connectionsOpened ??= (async () => {
293
+ if (!this.connectionsInput?.length) return;
294
+ const { actions, report } = await connectAll(this.connectionsInput);
295
+ for (const r of report) {
296
+ if (r.error) console.error(`[june] agent "${this.name}": connection "${r.name}" (${r.url}) failed: ${r.error}`);
297
+ }
298
+ const seen = new Set(this.tools.map((t) => t.spec.name));
299
+ for (const a of actions) {
300
+ const tool = actionToTool(a);
301
+ if (seen.has(tool.spec.name)) {
302
+ throw new Error(`AgentDurableObject(${this.name}): duplicate tool name "${tool.spec.name}" from a connection — rename so dispatch is unambiguous.`);
303
+ }
304
+ seen.add(tool.spec.name);
305
+ this.tools.push(tool);
306
+ }
307
+ })());
308
+ }
309
+ // Everything a turn needs opened BEFORE its synchronous scope-and-subscribe
310
+ // section: resources (possibly an async provider) and outbound connections.
311
+ private async ready(): Promise<Resources> {
312
+ const [resources] = await Promise.all([this.resolveResources(), this.resolveConnections()]);
313
+ return resources;
314
+ }
273
315
  constructor(state: DurableObjectState, def: DoAgentDef) {
274
316
  assertCoreRuntimeVersion(`AgentDurableObject(${def.name ?? "agent"})`); // #94: fail power-on, not mid-turn
275
317
  const store = new DoSessionStore(state.storage);
276
318
  const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
277
319
  this.resourcesInput = def.resources;
320
+ this.connectionsInput = def.connections;
278
321
  this.doEnv = def.env;
279
322
  this.services = def.services;
280
323
  const name = def.name ?? "agent";
@@ -335,6 +378,18 @@ export class AgentDurableObject {
335
378
  this.tools = tools;
336
379
  this.channels = channels;
337
380
  this.channelInstructions = def.channelInstructions;
381
+ // Orphan-policy check (#149): assembly can't validate surface keys against
382
+ // factory channels (they resolve only here, with env) — so warn now, once,
383
+ // when a policy's source matches no resolved channel. A policy that never
384
+ // fires is the old silent failure; a warning is its floor.
385
+ if (def.channelInstructions) {
386
+ const names = new Set(channels.map((c) => c.name));
387
+ for (const source of Object.keys(def.channelInstructions)) {
388
+ if (!names.has(source)) {
389
+ console.warn(`[june] agent "${name}": surface policy "${source}" matches no channel wired into this DO (${[...names].join(", ") || "none"}) — it will never fire.`);
390
+ }
391
+ }
392
+ }
338
393
  }
339
394
  // Resolve THE session for this DO: explicit key (from a routed request) → persisted
340
395
  // key (a prior life learned it) → "self". A key that contradicts the persisted or
@@ -382,7 +437,7 @@ export class AgentDurableObject {
382
437
  // pipeline does; without it runInScope is a pass-through and ambient reads throw.
383
438
  async turn(input: { turnId?: string; userText: string; event?: InboundEvent; trigger?: ProactiveTrigger; session?: string }): Promise<string> {
384
439
  await ensureScope();
385
- const resources = await this.resolveResources();
440
+ const resources = await this.ready();
386
441
  const session = this.resolveSession(input.session);
387
442
  return runInScope({ resources, services: this.services }, () => session.turn(input));
388
443
  }
@@ -392,7 +447,7 @@ export class AgentDurableObject {
392
447
  // the default turn-failure log / onTurnError (#76).
393
448
  async start(input: { turnId?: string; userText: string; event?: InboundEvent; trigger?: ProactiveTrigger; session?: string }): Promise<{ turnId: string }> {
394
449
  await ensureScope();
395
- const resources = await this.resolveResources();
450
+ const resources = await this.ready();
396
451
  const session = this.resolveSession(input.session);
397
452
  return runInScope({ resources, services: this.services }, () => session.start(input));
398
453
  }
@@ -450,7 +505,7 @@ export class AgentDurableObject {
450
505
  // db/services (ALS propagates to the .then continuation registered here); subscribing
451
506
  // happens synchronously right after, before any event can emit. Resources resolve
452
507
  // BEFORE this block — the section from start() to the subscription stays synchronous.
453
- const resources = await this.resolveResources();
508
+ const resources = await this.ready();
454
509
  let session: AgentSession;
455
510
  let started: { turnId: string };
456
511
  try {
@@ -535,7 +590,7 @@ export class AgentDurableObject {
535
590
  }
536
591
  // Same discipline as /turn: resolve resources before the synchronous
537
592
  // resume-then-subscribe section.
538
- const resources = await this.resolveResources();
593
+ const resources = await this.ready();
539
594
  let session: AgentSession;
540
595
  try {
541
596
  session = this.resolveSession(key);