@plurnk/plurnk-agui 1.7.0 → 1.9.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/dist/Module.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // {§agui-daemon-client} The in-process transport module is what the daemon's boot plug-point
2
- // activates: registerModule(aguiModule(opts)) hands this the CoreSeam handle; it opens
2
+ // activates: registerModule(aguiModule(opts)) hands this the ApplicationPort handle; it opens
3
3
  // the AG-UI+ HTTP/SSE listener and owns the client interface from there.
4
4
  //
5
5
  // This is the single external client interface:
@@ -15,11 +15,11 @@ var _a;
15
15
  import { createServer } from "node:http";
16
16
  import Portal from "./Portal.js";
17
17
  import { stateSnapshot, parseAction, actionResult } from "./AguiPlus.js";
18
- import { PlurnkParser, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
19
18
  import { EventType } from "./types.js";
20
19
  import { aguiRouteTemplate, observed } from "./observe.js";
21
20
  import { RunAgentInputSchema } from "@ag-ui/core";
22
- import { Problems, Validator } from "@plurnk/plurnk-contracts";
21
+ import { Problems, PlurnkParser, UNKNOWN_POSITION, Validator, } from "@plurnk/plurnk-contracts";
22
+ import { AGUI_BUILTIN_ACTIONS, AGUI_NOTIFICATIONS, } from "./AguiSurface.js";
23
23
  import { resolveModuleOptions } from "./config.js";
24
24
  const actionFailure = (code, detail, status = 400, extensions = {}) => ({
25
25
  ok: false,
@@ -95,50 +95,62 @@ class Module {
95
95
  #http;
96
96
  #threads = new Map(); // threadId → envelope
97
97
  #threadWorkers = new Map(); // threadId → conversation workerId
98
- static #CONTROL_ACTIONS = Object.freeze([
99
- "ping", "discover", "providers.list", "workspace.list", "workspace.create", "workspace.attach",
100
- ]);
101
- // The control plane vs the world. An AG-UI Run lives in a world (a conversation, or an action
102
- // that reads/writes a workspace's log); a control-plane action (list/create/attach/discover/
103
- // auth) does NOT — so it must not bind or forge a workspace. Only these kinds bind a workspace.
104
- static #WORLD_SCOPED = Object.freeze(new Set([
105
- "workspace.workers", "log.read", "loop.inject", "loop.cancel", "workspace.prompts", "workspace.rename",
106
- "workspace.constrain", "workspace.unconstrain", "workspace.constraints", "entry.read",
107
- "workspace.derivation", "op.exec", "op.parse", "workspace.members", "op.look", "run.fork",
108
- ]));
109
- static #BUILTIN_ACTIONS = Object.freeze(new Set([
110
- ...this.#CONTROL_ACTIONS,
111
- ...this.#WORLD_SCOPED,
112
- ]));
113
- static #NOTIFICATIONS = Object.freeze([
114
- "log/entry", "loop/terminated", "loop/proposal", "loop/interaction", "notice/event",
115
- "stream/event", "stream/concluded", "workspace/branch-batch",
116
- ]);
98
+ #actions = new Map();
117
99
  constructor(seam, opts) {
118
100
  this.#seam = seam;
119
101
  this.#opts = resolveModuleOptions(opts);
120
- this.#moduleActions();
102
+ this.#registerActions();
121
103
  this.#portal = new Portal(seam);
122
104
  this.#http = createServer((req, res) => { void this.#route(req, res); });
123
105
  }
124
- #moduleActions() {
125
- const actions = new Map();
106
+ #registerActions() {
107
+ for (const [name, contract] of Object.entries(AGUI_BUILTIN_ACTIONS)) {
108
+ this.#actions.set(name, {
109
+ ...contract,
110
+ execute: (params, env, conversationWorkerId) => this.#executeBuiltin(name, params, env, conversationWorkerId),
111
+ });
112
+ }
126
113
  for (const descriptor of this.#seam.listModuleActions()) {
127
- const { name, scope } = descriptor;
128
- if (_a.#BUILTIN_ACTIONS.has(name)) {
129
- throw new Error(`module action '${name}' collides with AG-UI built-in action`);
130
- }
131
- if (scope !== "worldless" && scope !== "workspace") {
114
+ const { name, scope, inputSchema, outputSchema } = descriptor;
115
+ if (this.#actions.has(name))
116
+ throw new Error(`AG-UI action '${name}' is registered more than once`);
117
+ if (scope !== "worldless" && scope !== "workspace" && scope !== "worker") {
132
118
  throw new Error(`module action '${name}' has invalid scope '${String(scope)}'`);
133
119
  }
134
- if (actions.has(name))
135
- throw new Error(`module action '${name}' is registered more than once`);
136
- actions.set(name, descriptor);
120
+ this.#actions.set(name, {
121
+ scope,
122
+ inputSchema,
123
+ outputSchema,
124
+ execute: async (params, env, conversationWorkerId) => {
125
+ if (scope === "worldless") {
126
+ return {
127
+ ok: true,
128
+ result: await this.#seam.invokeModuleAction(name, params, { scope }),
129
+ };
130
+ }
131
+ const { workspaceId } = _a.#requireWorkspace(name, env);
132
+ if (scope === "workspace") {
133
+ return {
134
+ ok: true,
135
+ result: await this.#seam.invokeModuleAction(name, params, { scope, workspaceId }),
136
+ };
137
+ }
138
+ const workerId = conversationWorkerId ?? await this.#seam.ensureModelWorker(workspaceId);
139
+ return {
140
+ ok: true,
141
+ result: await this.#seam.invokeModuleAction(name, params, { scope, workspaceId, workerId }),
142
+ };
143
+ },
144
+ });
137
145
  }
138
- return actions;
139
146
  }
140
- #isWorkspaceAction(kind) {
141
- return _a.#WORLD_SCOPED.has(kind) || this.#moduleActions().get(kind)?.scope === "workspace";
147
+ #requiresWorkspace(kind) {
148
+ return this.#actions.get(kind)?.scope !== "worldless";
149
+ }
150
+ static #requireWorkspace(kind, env) {
151
+ if (env === null)
152
+ throw new Error(`action '${kind}' operates within a workspace, but none is bound`);
153
+ return env;
142
154
  }
143
155
  static init(opts) {
144
156
  return {
@@ -267,15 +279,20 @@ class Module {
267
279
  return { env, reattached };
268
280
  }
269
281
  // Resolve the thread's conversation worker within its world. Cached per threadId;
270
- // worker names are immutable so the binding can't rot.
271
- async #conversationWorker(threadId, env) {
282
+ // worker names are immutable so the binding can't rot. {§worker-settings} — the
283
+ // client's per-run declaration (forwardedProps.plurnk.requestUserInput) rides the
284
+ // resolution, so a client can change its mind between loops.
285
+ async #conversationWorker(threadId, env, settings) {
272
286
  const cached = this.#threadWorkers.get(threadId);
273
- if (cached !== undefined)
287
+ if (cached !== undefined) {
288
+ if (settings !== undefined)
289
+ await this.#seam.setWorkerSettings({ workspaceId: env.workspaceId, workerId: cached, settings });
274
290
  return cached;
291
+ }
275
292
  const workerId = threadId === env.workspaceName
276
- ? await this.#seam.ensureModelWorker(env.workspaceId)
293
+ ? await this.#seam.ensureModelWorker(env.workspaceId, settings)
277
294
  : (await this.#seam.listWorkers(env.workspaceId)).find((r) => r.name === threadId)?.id
278
- ?? (await this.#seam.createConversationWorker({ workspaceId: env.workspaceId, name: threadId })).workerId;
295
+ ?? (await this.#seam.createConversationWorker({ workspaceId: env.workspaceId, name: threadId, ...(settings === undefined ? {} : { settings }) })).workerId;
279
296
  this.#threadWorkers.set(threadId, workerId);
280
297
  return workerId;
281
298
  }
@@ -301,7 +318,7 @@ class Module {
301
318
  // unknown kind, which is no worker at all) answers without binding — or forging — a
302
319
  // workspace. Only world-scoped actions and conversations reach #envelope below.
303
320
  const action = parseAction(input.forwardedProps);
304
- if (action !== null && !this.#isWorkspaceAction(action.kind)) {
321
+ if (action !== null && !this.#requiresWorkspace(action.kind)) {
305
322
  return await this.#controlRun(action, input, res);
306
323
  }
307
324
  let prompt = null;
@@ -322,7 +339,9 @@ class Module {
322
339
  // world. threadId == workspace name binds the model worker (the default conversation);
323
340
  // a distinct threadId names its own worker: found by name, else minted via
324
341
  // createConversationWorker. The name is the identity at BOTH levels.
325
- const workerId = await this.#conversationWorker(input.threadId, env);
342
+ const plurnk = forwarded?.plurnk;
343
+ const requestUserInput = plurnk?.requestUserInput;
344
+ const workerId = await this.#conversationWorker(input.threadId, env, typeof requestUserInput === "boolean" ? { requestUserInput } : undefined);
326
345
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", "connection": "keep-alive" });
327
346
  let finished = false;
328
347
  const interrupts = [];
@@ -434,20 +453,15 @@ class Module {
434
453
  ...(forwarded !== undefined && Object.hasOwn(forwarded, "openPaths")
435
454
  ? { openPaths: forwarded.openPaths }
436
455
  : {}),
437
- // {§methods-loop-run-model} The client sends alias and model on every loop;
438
- // model is the client-resolved <provider>/<model>. Forward both, and the daemon's
439
- // runLoop applies precedence (model wins) and resolves the provider per loop.
440
- ...(forwarded !== undefined && Object.hasOwn(forwarded, "alias")
441
- ? { alias: forwarded.alias }
442
- : {}),
443
- ...(forwarded !== undefined && Object.hasOwn(forwarded, "model")
444
- ? { model: forwarded.model }
456
+ // {§methods-loop-run-model} The client may still forward one explicit
457
+ // alias-or-provider/model selector. The daemon
458
+ // persists an explicit selection onto the worker and snapshots it on the
459
+ // loop; an omitted selector continues the worker's durable model.
460
+ ...(forwarded !== undefined && Object.hasOwn(forwarded, "selector")
461
+ ? { selector: forwarded.selector }
445
462
  : {}),
446
- ...(forwarded !== undefined && Object.hasOwn(forwarded, "childAlias")
447
- ? { childAlias: forwarded.childAlias }
448
- : {}),
449
- ...(forwarded !== undefined && Object.hasOwn(forwarded, "childModel")
450
- ? { childModel: forwarded.childModel }
463
+ ...(forwarded !== undefined && Object.hasOwn(forwarded, "childSelector")
464
+ ? { childSelector: forwarded.childSelector }
451
465
  : {}),
452
466
  });
453
467
  // A dropped SSE on a live AG-UI Run cancels the loop (hangup is the abort). A stream we
@@ -493,38 +507,72 @@ class Module {
493
507
  emit({ type: EventType.RUN_FINISHED, threadId: input.threadId, runId: input.runId, outcome: { type: "success" } });
494
508
  res.end();
495
509
  }
496
- // The capability manifest a client probes (`discover`) for exact action/event membership.
497
- // The built-ins come from the same inventories routing uses; extension names come from core.
510
+ // Discovery is a projection of the executable registry, not a second inventory.
498
511
  async #capabilities() {
499
- const methods = {};
500
- for (const k of [
501
- ..._a.#CONTROL_ACTIONS,
502
- ..._a.#WORLD_SCOPED,
503
- ...this.#moduleActions().keys(),
504
- ]) {
505
- if (methods[k])
506
- throw new Error(`AG-UI action '${k}' is registered more than once`);
507
- methods[k] = true;
508
- }
509
- const notifications = {};
510
- for (const n of _a.#NOTIFICATIONS)
511
- notifications[n] = true;
512
+ const actions = Object.fromEntries([...this.#actions].map(([name, action]) => [
513
+ name,
514
+ {
515
+ scope: action.scope,
516
+ inputSchema: action.inputSchema,
517
+ outputSchema: action.outputSchema,
518
+ },
519
+ ]));
512
520
  const display = Validator.assertClientDisplayCapabilities(await this.#seam.listClientDisplayCapabilities());
513
- return { methods, notifications, display };
521
+ return Validator.assertAguiDiscovery({
522
+ schemaVersion: 1,
523
+ actions,
524
+ notifications: AGUI_NOTIFICATIONS,
525
+ display,
526
+ });
514
527
  }
515
- // The action executor — the verb surface. The control plane runs worldless; everything
516
- // below the guard operates within a bound workspace. An unknown kind is an honest error,
517
- // never a silent pass-through. loop.inject rides here too (§4): the seam's unified
518
- // runLoop folds a prompt into the active drain; the steered effect streams on the SSE.
519
528
  async #action(a, env, conversationWorkerId) {
520
- const p = a.params;
521
- const moduleAction = this.#moduleActions().get(a.kind);
529
+ const action = this.#actions.get(a.kind);
530
+ if (action === undefined) {
531
+ return actionFailure("unknown-action", `Action '${a.kind}' is not registered.`, 404, {
532
+ requestedAction: a.kind,
533
+ recovery: "Use an action advertised by discover.",
534
+ });
535
+ }
536
+ const admitted = Validator.validateJsonSchemaInstance(action.inputSchema, a.params);
537
+ if (!admitted.valid) {
538
+ return actionFailure("invalid-action-parameters", `Action '${a.kind}' received parameters outside its advertised input schema.`, 400, {
539
+ issues: admitted.errors,
540
+ recovery: "Conform the action parameters to discover.actions[<name>].inputSchema.",
541
+ });
542
+ }
543
+ try {
544
+ if (action.scope !== "worldless")
545
+ _a.#requireWorkspace(a.kind, env);
546
+ const outcome = await action.execute(a.params, env, conversationWorkerId);
547
+ if (!outcome.ok)
548
+ return outcome;
549
+ const projected = Validator.validateJsonSchemaInstance(action.outputSchema, outcome.result);
550
+ if (!projected.valid) {
551
+ throw new Error(`AG-UI action '${a.kind}' produced output outside its advertised schema: ${JSON.stringify(projected.errors)}`);
552
+ }
553
+ return outcome;
554
+ }
555
+ catch (err) {
556
+ const problem = problemFromError(err);
557
+ if (problem !== null)
558
+ return { ok: false, problem };
559
+ console.error(`AG-UI action '${a.kind}' failed:`, err);
560
+ return actionFailure("action-failed", "The action failed unexpectedly.", 500);
561
+ }
562
+ }
563
+ // The built-in implementation half of the declarative registry. The registry
564
+ // owns membership, scope, admission, projection, and discovery; this dispatch
565
+ // owns only the corresponding daemon operation.
566
+ async #executeBuiltin(kind, p, env, conversationWorkerId) {
522
567
  try {
523
- // The control plane — worldless verbs (no bound workspace; #WORLD_SCOPED gates this).
524
- switch (a.kind) {
568
+ // Worldless actions never bind or forge a workspace.
569
+ switch (kind) {
525
570
  case "ping": return { ok: true, result: {} };
526
571
  case "discover": return { ok: true, result: await this.#capabilities() };
527
572
  case "providers.list": return { ok: true, result: this.#seam.listProviders() };
573
+ case "models.list": {
574
+ return { ok: true, result: this.#seam.listModels(Validator.assertModelCatalogQuery(p)) };
575
+ }
528
576
  case "workspace.list": return { ok: true, result: { workspaces: await this.#seam.listWorkspaces() } };
529
577
  case "workspace.create": {
530
578
  // The name IS the identity: an explicit name creates/attaches EXACTLY
@@ -567,35 +615,14 @@ class Module {
567
615
  return { ok: true, result: { id: att.workspaceId, name: att.workspaceName, workerId: att.workerId } };
568
616
  }
569
617
  }
570
- if (moduleAction?.scope === "worldless") {
571
- return {
572
- ok: true,
573
- result: await this.#seam.invokeModuleAction(a.kind, p, { scope: "worldless" }),
574
- };
575
- }
576
- // Below this line lives IN a world. An unknown kind is no worker at all; a
577
- // world-scoped kind with no bound workspace is a routing bug — both surface plainly.
578
- if (!_a.#WORLD_SCOPED.has(a.kind) && moduleAction?.scope !== "workspace") {
579
- return actionFailure("unknown-action", `Action '${a.kind}' is not registered.`, 404, {
580
- requestedAction: a.kind,
581
- recovery: "Use an action advertised by discover.",
582
- });
583
- }
584
- if (env === null)
585
- throw new Error(`action '${a.kind}' operates within a workspace, but none is bound`);
586
- if (moduleAction?.scope === "workspace") {
587
- return {
588
- ok: true,
589
- result: await this.#seam.invokeModuleAction(a.kind, p, { scope: "workspace", workspaceId: env.workspaceId }),
590
- };
591
- }
592
- switch (a.kind) {
593
- case "workspace.workers": return { ok: true, result: { workers: await this.#seam.listWorkers(typeof p.id === "number" ? p.id : env.workspaceId) } };
618
+ const world = _a.#requireWorkspace(kind, env);
619
+ switch (kind) {
620
+ case "workspace.workers": return { ok: true, result: { workers: await this.#seam.listWorkers(typeof p.id === "number" ? p.id : world.workspaceId) } };
594
621
  case "log.read": {
595
622
  // Default worker: the conversation; p.workerId pins another.
596
- const readWorkerId = typeof p.workerId === "number" ? p.workerId : conversationWorkerId ?? await this.#seam.ensureModelWorker(env.workspaceId);
623
+ const readWorkerId = typeof p.workerId === "number" ? p.workerId : conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId);
597
624
  const entries = await this.#seam.readLog({
598
- workspaceId: env.workspaceId,
625
+ workspaceId: world.workspaceId,
599
626
  workerId: readWorkerId,
600
627
  ...(Object.hasOwn(p, "limit") ? { limit: p.limit } : {}),
601
628
  ...(Object.hasOwn(p, "sinceId") ? { sinceId: p.sinceId } : {}),
@@ -611,38 +638,38 @@ class Module {
611
638
  if (typeof p.prompt !== "string" || p.prompt.length === 0) {
612
639
  return actionFailure("invalid-action-parameters", "loop.inject requires a non-empty prompt.", 400, { field: "prompt", recovery: "Provide the prompt to inject." });
613
640
  }
614
- const ack = await this.#seam.runLoop({ workspaceId: env.workspaceId, workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(env.workspaceId), prompt: p.prompt });
641
+ const ack = await this.#seam.runLoop({ workspaceId: world.workspaceId, workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId), prompt: p.prompt });
615
642
  return operationOutcome(ack);
616
643
  }
617
644
  // The stop button (TUI /stop + Ctrl-C, nvim :PlurnkStop): abort the model
618
645
  // worker's active drain. Mirrors the SSE-hangup abort, addressable as a verb.
619
- case "loop.cancel": return { ok: true, result: { cancelled: this.#seam.cancelDrain(conversationWorkerId ?? await this.#seam.ensureModelWorker(env.workspaceId)) } };
646
+ case "loop.cancel": return { ok: true, result: { cancelled: this.#seam.cancelDrain(conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId), typeof p.reason === "string" ? p.reason : undefined) } };
620
647
  case "workspace.prompts": return {
621
648
  ok: true,
622
649
  result: {
623
- prompts: await this.#seam.listPrompts(env.workspaceId, Object.hasOwn(p, "limit") ? p.limit : undefined),
650
+ prompts: await this.#seam.listPrompts(world.workspaceId, Object.hasOwn(p, "limit") ? p.limit : undefined),
624
651
  },
625
652
  };
626
653
  case "workspace.rename": {
627
654
  if (typeof p.name !== "string" || p.name.length === 0) {
628
655
  return actionFailure("invalid-action-parameters", "workspace.rename requires a non-empty name.", 400, { field: "name", recovery: "Provide the new workspace name." });
629
656
  }
630
- return { ok: true, result: await this.#seam.renameWorkspace(env.workspaceId, p.name) };
657
+ return { ok: true, result: await this.#seam.renameWorkspace(world.workspaceId, p.name) };
631
658
  }
632
659
  case "workspace.constrain": {
633
660
  if (typeof p.effect !== "string" || typeof p.glob !== "string") {
634
661
  return actionFailure("invalid-action-parameters", "workspace.constrain requires string effect and glob parameters.", 400, { fields: ["effect", "glob"], recovery: "Provide both constraint parameters." });
635
662
  }
636
- return { ok: true, result: await this.#seam.constrain(env.workspaceId, p.effect, p.glob) };
663
+ return { ok: true, result: await this.#seam.constrain(world.workspaceId, p.effect, p.glob) };
637
664
  }
638
665
  case "workspace.unconstrain": {
639
666
  if (typeof p.effect !== "string" || typeof p.glob !== "string") {
640
667
  return actionFailure("invalid-action-parameters", "workspace.unconstrain requires string effect and glob parameters.", 400, { fields: ["effect", "glob"], recovery: "Provide both constraint parameters." });
641
668
  }
642
- return { ok: true, result: await this.#seam.unconstrain(env.workspaceId, p.effect, p.glob) };
669
+ return { ok: true, result: await this.#seam.unconstrain(world.workspaceId, p.effect, p.glob) };
643
670
  }
644
- case "workspace.constraints": return { ok: true, result: { constraints: await this.#seam.listConstraints(env.workspaceId) } };
645
- case "workspace.derivation": return { ok: true, result: { status: this.#seam.workspaceDerivationStatus(env.workspaceId) } };
671
+ case "workspace.constraints": return { ok: true, result: { constraints: await this.#seam.listConstraints(world.workspaceId) } };
672
+ case "workspace.derivation": return { ok: true, result: { status: this.#seam.workspaceDerivationStatus(world.workspaceId) } };
646
673
  case "entry.read": {
647
674
  if (typeof p.target !== "string") {
648
675
  return actionFailure("invalid-action-parameters", "entry.read requires a string target.", 400, { field: "target", recovery: "Provide an entry URI." });
@@ -652,10 +679,10 @@ class Module {
652
679
  return actionFailure("invalid-action-parameters", "entry.read workerId must be a positive integer.", 400, { field: "workerId", recovery: "Use the workerId supplied with the entry notification." });
653
680
  }
654
681
  const result = Validator.assertEntryReadResult(await this.#seam.readEntry({
655
- workspaceId: env.workspaceId,
682
+ workspaceId: world.workspaceId,
656
683
  workerId: typeof p.workerId === "number"
657
684
  ? p.workerId
658
- : conversationWorkerId ?? await this.#seam.ensureModelWorker(env.workspaceId),
685
+ : conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
659
686
  target: p.target,
660
687
  ...(Object.hasOwn(p, "channel") ? { channel: p.channel } : {}),
661
688
  ...(Object.hasOwn(p, "offset") ? { offset: p.offset } : {}),
@@ -669,12 +696,18 @@ class Module {
669
696
  return actionFailure("invalid-action-parameters", "op.exec requires a non-empty command.", 400, { field: "command", recovery: "Provide the command to execute." });
670
697
  }
671
698
  const statement = {
672
- op: "EXEC", suffix: "", signal: null, target: null,
699
+ op: "EXEC", delimiter: "", annotation: null, signal: null, target: null,
673
700
  lineMarker: null, body: p.command, position: UNKNOWN_POSITION,
674
701
  };
675
702
  // Client ops journal as client-origin turns in the client worker (worker split:
676
- // only LOOPS live in the model worker).
677
- const [result] = await this.#seam.dispatchClientAction({ workspaceId: env.workspaceId, workerId: env.workerId, statements: [statement] });
703
+ // only LOOPS live in the model worker) and execute in the attached Worker's
704
+ // Functionality ({§actor-boundary-attached-functionality}).
705
+ const [result] = await this.#seam.dispatchClientAction({
706
+ workspaceId: world.workspaceId,
707
+ workerId: world.workerId,
708
+ functionalityWorkerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
709
+ statements: [statement],
710
+ });
678
711
  if (result === undefined)
679
712
  throw new Error("op.exec dispatch returned no operation result");
680
713
  return operationOutcome(result);
@@ -715,7 +748,12 @@ class Module {
715
748
  }
716
749
  const dispatched = statements.length === 0
717
750
  ? []
718
- : await this.#seam.dispatchClientAction({ workspaceId: env.workspaceId, workerId: env.workerId, statements });
751
+ : await this.#seam.dispatchClientAction({
752
+ workspaceId: world.workspaceId,
753
+ workerId: world.workerId,
754
+ functionalityWorkerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
755
+ statements,
756
+ });
719
757
  let index = 0;
720
758
  for (let i = 0; i < results.length; i++) {
721
759
  if (results[i] === null)
@@ -723,7 +761,7 @@ class Module {
723
761
  }
724
762
  return { ok: true, result: { results } };
725
763
  }
726
- case "workspace.members": return { ok: true, result: await this.#seam.listMembers(env.workspaceId) };
764
+ case "workspace.members": return { ok: true, result: await this.#seam.listMembers(world.workspaceId) };
727
765
  case "op.look": {
728
766
  // {§agui-op-look}
729
767
  if (typeof p.text !== "string" || p.text.length === 0) {
@@ -767,20 +805,82 @@ class Module {
767
805
  return actionFailure("invalid-action-parameters", `op.look parsed ${item.statement.op}; the single statement must be LOOK.`, 400, { field: "text", recovery: "Use LOOK as the observation operation." });
768
806
  }
769
807
  const statement = { ...item.statement, op: "READ" };
770
- return operationOutcome(await this.#seam.look({ workspaceId: env.workspaceId, workerId: env.workerId, statement }));
808
+ return operationOutcome(await this.#seam.look({
809
+ workspaceId: world.workspaceId,
810
+ workerId: world.workerId,
811
+ functionalityWorkerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
812
+ statement,
813
+ }));
771
814
  }
772
- case "run.fork": return { ok: true, result: await this.#seam.forkWorker({ workspaceId: env.workspaceId, workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(env.workspaceId), ...(typeof p.name === "string" ? { name: p.name } : {}) }) };
773
- default: return actionFailure("unknown-action", `Action '${a.kind}' is not registered.`, 404, {
774
- requestedAction: a.kind,
775
- recovery: "Use an action advertised by discover.",
776
- });
815
+ case "run.fork": return { ok: true, result: await this.#seam.forkWorker({ workspaceId: world.workspaceId, workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId), ...(typeof p.name === "string" ? { name: p.name } : {}) }) };
816
+ case "worker.model.get": {
817
+ const { model, spawnModel } = await this.#seam.readWorkerModel({
818
+ workspaceId: world.workspaceId,
819
+ workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
820
+ });
821
+ return { ok: true, result: { model, spawnModel } };
822
+ }
823
+ case "worker.model.set": {
824
+ if (typeof p.selector !== "string" || p.selector.length === 0) {
825
+ return actionFailure("invalid-action-parameters", "worker.model.set requires a selector.", 400, { recovery: "Provide a declared alias or provider/model route." });
826
+ }
827
+ return { ok: true, result: await this.#seam.setWorkerModel({
828
+ workspaceId: world.workspaceId,
829
+ workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
830
+ selector: p.selector,
831
+ }) };
832
+ }
833
+ case "worker.child.set": {
834
+ if (!Object.hasOwn(p, "selector")
835
+ || (p.selector !== null && (typeof p.selector !== "string" || p.selector.length === 0))) {
836
+ return actionFailure("invalid-action-parameters", "worker.child.set requires a selector.", 400, { recovery: "Provide a declared alias or provider/model route; null means inherit." });
837
+ }
838
+ return { ok: true, result: await this.#seam.setWorkerSpawnModel({
839
+ workspaceId: world.workspaceId,
840
+ workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
841
+ selector: p.selector,
842
+ }) };
843
+ }
844
+ case "worker.reasoning.get": {
845
+ return { ok: true, result: await this.#seam.readWorkerReasoning({
846
+ workspaceId: world.workspaceId,
847
+ workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
848
+ }) };
849
+ }
850
+ case "worker.reasoning.set": {
851
+ if (!Object.hasOwn(p, "policy")) {
852
+ return actionFailure("invalid-action-parameters", "worker.reasoning.set requires a policy.", 400, { recovery: "Provide a reasoning policy." });
853
+ }
854
+ return { ok: true, result: await this.#seam.setWorkerReasoning({
855
+ workspaceId: world.workspaceId,
856
+ workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
857
+ policy: p.policy,
858
+ }) };
859
+ }
860
+ case "worker.settings.get": {
861
+ return { ok: true, result: await this.#seam.readWorkerSettings({
862
+ workspaceId: world.workspaceId,
863
+ workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
864
+ }) };
865
+ }
866
+ case "worker.settings.set": {
867
+ if (typeof p.settings !== "object" || p.settings === null || Array.isArray(p.settings)) {
868
+ return actionFailure("invalid-action-parameters", "worker.settings.set requires a settings object.", 400, { recovery: "Provide a settings object, e.g. { requestUserInput: true }." });
869
+ }
870
+ return { ok: true, result: await this.#seam.setWorkerSettings({
871
+ workspaceId: world.workspaceId,
872
+ workerId: conversationWorkerId ?? await this.#seam.ensureModelWorker(world.workspaceId),
873
+ settings: p.settings,
874
+ }) };
875
+ }
876
+ default: throw new Error(`AG-UI built-in '${kind}' has no executor`);
777
877
  }
778
878
  }
779
879
  catch (err) {
780
880
  const problem = problemFromError(err);
781
881
  if (problem !== null)
782
882
  return { ok: false, problem };
783
- console.error(`AG-UI action '${a.kind}' failed:`, err);
883
+ console.error(`AG-UI action '${kind}' failed:`, err);
784
884
  return actionFailure("action-failed", "The action failed unexpectedly.", 500);
785
885
  }
786
886
  }