wayang-ai 0.6.2 → 0.6.3

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.
Files changed (2) hide show
  1. package/dist/cli.js +106 -63
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1104,14 +1104,17 @@ function summarizeWorkerMessage(argsJson, width, result, maxLines = 3) {
1104
1104
  const message = args ? str(args.message) ?? "" : argsJson;
1105
1105
  const { lines, hidden } = previewLines(message, maxLines, width);
1106
1106
  const failed = result !== void 0 && (result.isError === true || /^\[ERROR\]/.test(result.text));
1107
+ let delivery;
1108
+ if (result !== void 0 && !failed) {
1109
+ delivery = /\bqueued\b/i.test(result.text) ? "queued" : /\bdelivered\b/i.test(result.text) ? "delivered" : void 0;
1110
+ }
1107
1111
  return {
1108
1112
  ...workerId && { workerId },
1109
1113
  lines,
1110
1114
  hiddenLines: hidden,
1111
1115
  outcome: result === void 0 ? "pending" : failed ? "failed" : "ok",
1112
1116
  failure: failed ? failureText(result.text, width) : "",
1113
- ...result !== void 0 && !failed && /\bqueued\b/.test(result.text) && { delivery: "queued" },
1114
- ...result !== void 0 && !failed && /\bdelivered\b/.test(result.text) && { delivery: "delivered" }
1117
+ ...delivery && { delivery }
1115
1118
  };
1116
1119
  }
1117
1120
  var TASK_ID_RE = /^Task (\S+) (?:added|assigned)/;
@@ -2448,7 +2451,7 @@ function AppContent() {
2448
2451
  // package.json
2449
2452
  var package_default = {
2450
2453
  name: "wayang-ai",
2451
- version: "0.6.2",
2454
+ version: "0.6.3",
2452
2455
  description: "Multi-agent orchestration platform",
2453
2456
  type: "module",
2454
2457
  license: "MIT",
@@ -4537,18 +4540,39 @@ function controllerMessageContent(content) {
4537
4540
  return `[Controller message] ${content}`;
4538
4541
  }
4539
4542
  function createInboxInjection(deps) {
4543
+ let baseLength = null;
4540
4544
  const injected = [];
4541
4545
  return {
4542
4546
  prepareStep({ messages }) {
4543
4547
  const fresh = deps.drainInbox();
4544
4548
  if (fresh.length === 0 && injected.length === 0) return void 0;
4549
+ if (baseLength === null) baseLength = messages.length;
4550
+ const responseCount = Math.max(0, messages.length - baseLength);
4545
4551
  for (const m of fresh) {
4546
- injected.push({ role: "user", content: controllerMessageContent(m.content) });
4552
+ injected.push({
4553
+ message: { role: "user", content: controllerMessageContent(m.content) },
4554
+ // After the response messages that exist NOW — i.e. at the tail of
4555
+ // the conversation as this step sees it, before this step's reply.
4556
+ insertAfter: responseCount
4557
+ });
4547
4558
  deps.onInject(m.content);
4548
4559
  }
4549
- return { messages: [...messages, ...injected] };
4560
+ const merged = messages.slice(0, baseLength);
4561
+ let cursor = 0;
4562
+ for (const inj of injected) {
4563
+ while (cursor < Math.min(inj.insertAfter, responseCount)) {
4564
+ merged.push(messages[baseLength + cursor]);
4565
+ cursor++;
4566
+ }
4567
+ merged.push(inj.message);
4568
+ }
4569
+ while (cursor < responseCount) {
4570
+ merged.push(messages[baseLength + cursor]);
4571
+ cursor++;
4572
+ }
4573
+ return { messages: merged };
4550
4574
  },
4551
- injectedMessages: () => [...injected]
4575
+ injectedMessages: () => injected.map((i) => i.message)
4552
4576
  };
4553
4577
  }
4554
4578
 
@@ -4803,7 +4827,10 @@ The tool shows task metadata (status, result, error) and recent worker conversat
4803
4827
 
4804
4828
  ## Sending messages to workers (chat_worker)
4805
4829
 
4806
- Use chat_worker(workerId, message) to send guidance to a running worker. The worker will see the message next time it calls check_controller_messages.
4830
+ Use chat_worker(workerId, message) to send guidance to a running worker.
4831
+ The worker reads it at its next step (puppet) or turn boundary (third-party) \u2014 a tool call in progress is NEVER interrupted; an idle multi-stage worker reads it when its next stage starts.
4832
+ Delivery is blind: "queued" only means the message sits in the worker's inbox until that boundary. A stuck worker never reads it.
4833
+ A third-party worker that already finished its session rejects the message with an explicit [ERROR] \u2014 inspect its output instead of retrying.
4807
4834
  Use sparingly \u2014 only when you need to:
4808
4835
  - Correct a worker that is going in the wrong direction
4809
4836
  - Provide additional context the worker did not have
@@ -5272,7 +5299,11 @@ var WorkerAgent = class extends BaseAgent {
5272
5299
  getSnapshot(path7) {
5273
5300
  return this.state.get(path7);
5274
5301
  }
5275
- /** Accept a message from the controller — writes to internal inbox. */
5302
+ /**
5303
+ * Accept a message from the controller — writes to internal inbox. The
5304
+ * puppet reads it at its next LLM step, so a running (or idle) worker
5305
+ * always accepts.
5306
+ */
5276
5307
  acceptMessage(message) {
5277
5308
  this.state.append("inbox", {
5278
5309
  id: generateId("msg"),
@@ -5280,6 +5311,7 @@ var WorkerAgent = class extends BaseAgent {
5280
5311
  timestamp: Date.now()
5281
5312
  });
5282
5313
  this.logger.info({ workerId: this.id }, "Controller message accepted");
5314
+ return true;
5283
5315
  }
5284
5316
  /** Unread controller messages (non-destructive) — engine reports these when the run ends. */
5285
5317
  pendingMessages() {
@@ -5376,14 +5408,20 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5376
5408
  // --- Controller messaging via streamInput ---
5377
5409
  _messageQueue = [];
5378
5410
  _messageResolve = null;
5379
- /** Active SDK query — interrupted to deliver controller messages promptly. */
5380
- _activeQuery = null;
5381
5411
  /**
5382
- * Interrupt requested while a permission was pending — delivered right
5383
- * after the permission settles (interrupting mid-permission is unproven
5384
- * SDK territory; see docs/develop/v0.6-design.md §3).
5412
+ * True while run() is executing. Controller messages are rejected once the
5413
+ * run's input has closed (see `_inputClosed`), but idle multi-stage workers
5414
+ * (no run in flight) keep accepting — the next stage folds them into its
5415
+ * opening prompt.
5385
5416
  */
5386
- _interruptQueued = false;
5417
+ _runInFlight = false;
5418
+ /**
5419
+ * True once the prompt iterator reported done for the current run: the
5420
+ * session can no longer take input (a turn result arrived with nothing
5421
+ * queued, or the run was aborted). Further acceptMessage calls fail
5422
+ * explicitly instead of pretending to queue.
5423
+ */
5424
+ _inputClosed = false;
5387
5425
  // --- ask_controller via in-process SDK MCP server ---
5388
5426
  _askController = null;
5389
5427
  // --- Permission control via canUseTool ---
@@ -5392,31 +5430,17 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5392
5430
  /** Exposed for engine to resolve pending permission requests. */
5393
5431
  permissionHandler = {
5394
5432
  resolvePermission: (requestId, approved, reason) => {
5395
- const ok = this._pendingPermissions.resolve(
5433
+ return this._pendingPermissions.resolve(
5396
5434
  requestId,
5397
5435
  approved ? { behavior: "allow" } : { behavior: "deny", message: reason ?? "Denied by controller" }
5398
5436
  );
5399
- if (ok) this.afterPermissionSettled();
5400
- return ok;
5401
5437
  },
5402
5438
  cleanup: () => {
5403
- this._interruptQueued = false;
5404
5439
  this._pendingPermissions.cleanupAll(() => ({ behavior: "deny", message: "Worker aborted" }));
5405
5440
  },
5406
5441
  wrappedTools: {}
5407
5442
  // not used for ClaudeCodeWorker
5408
5443
  };
5409
- /**
5410
- * A permission settled (approved / denied / timed out). If a controller
5411
- * message was waiting for the permission window to close, deliver its
5412
- * interrupt now — this runs on BOTH the resolve and the timeout path.
5413
- */
5414
- afterPermissionSettled() {
5415
- if (this._interruptQueued && this._pendingPermissions.size === 0) {
5416
- this._interruptQueued = false;
5417
- this.requestInterrupt();
5418
- }
5419
- }
5420
5444
  // Signal context set during run()
5421
5445
  _signalQueue = null;
5422
5446
  _runContext = null;
@@ -5463,6 +5487,8 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5463
5487
  }
5464
5488
  async run(task, _tools, onProgress) {
5465
5489
  this._terminalResult = null;
5490
+ this._inputClosed = false;
5491
+ this._runInFlight = true;
5466
5492
  this.state.set("runtimeState.task", { id: task.id, description: task.description });
5467
5493
  const turnEnv = this.buildTurnEnv(task);
5468
5494
  this._currentTurnEnv = turnEnv;
@@ -5520,18 +5546,17 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5520
5546
  prompt: this.buildPromptIterable(task.description),
5521
5547
  options
5522
5548
  });
5523
- this._activeQuery = stream;
5524
5549
  try {
5525
5550
  for await (const message of stream) {
5526
5551
  if (this.abortController.signal.aborted) break;
5527
5552
  this.processStreamMessage(message, onProgress);
5528
- if (this._terminalResult) break;
5529
5553
  }
5530
5554
  } finally {
5531
- this._activeQuery = null;
5555
+ this._inputClosed = true;
5532
5556
  }
5533
5557
  } catch (err) {
5534
5558
  this.logger.error({ err, taskId: task.id }, "ClaudeCodeWorker query error");
5559
+ this._inputClosed = true;
5535
5560
  if (!this._terminalResult) {
5536
5561
  this._terminalResult = {
5537
5562
  status: "failed",
@@ -5558,6 +5583,7 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5558
5583
  startedAt: resultTs,
5559
5584
  endedAt: resultTs
5560
5585
  }, turnEnv));
5586
+ this._runInFlight = false;
5561
5587
  return this._terminalResult;
5562
5588
  }
5563
5589
  /** Set signal context for permission requests. Called by engine before run(). */
@@ -5586,34 +5612,36 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5586
5612
  getSnapshot(path7) {
5587
5613
  return this.state.get(path7);
5588
5614
  }
5589
- /** Accept a message from the controller — injects into running session via streamInput. */
5615
+ /**
5616
+ * Accept a message from the controller. The message is queued and handed
5617
+ * to the SDK only when it pulls the next prompt: the CLI parks pulled
5618
+ * prompts in an internal queue that it drains strictly BETWEEN turns, so
5619
+ * delivery always lands on a turn boundary — a tool call in progress is
5620
+ * never cut off (the v0.6 interrupt delivery did exactly that and killed
5621
+ * the session with an orphaned tool_use; see docs/develop/v0.6-design.md
5622
+ * §6.1 — the hazard is any unclosed tool_use, not just a pending
5623
+ * permission).
5624
+ *
5625
+ * Returns false when delivery is impossible: the run's input has closed
5626
+ * (final result received) or the worker was aborted. Idle multi-stage
5627
+ * workers (no run in flight) always accept — the next stage folds the
5628
+ * message into its opening prompt.
5629
+ */
5590
5630
  acceptMessage(message) {
5631
+ if (this.abortController.signal.aborted) return false;
5632
+ if (this._runInFlight && this._inputClosed) return false;
5591
5633
  this._messageQueue.push(message);
5592
5634
  if (this._messageResolve) {
5593
5635
  this._messageResolve();
5594
5636
  this._messageResolve = null;
5595
5637
  }
5596
- if ((this.config.interruptOnMessage ?? "immediate") === "immediate") {
5597
- this.requestInterrupt();
5598
- }
5599
5638
  this.logger.info({ workerId: this.id }, "Controller message accepted");
5639
+ return true;
5600
5640
  }
5601
5641
  /** Unread controller messages (non-destructive) — engine reports these when the run ends. */
5602
5642
  pendingMessages() {
5603
5643
  return [...this._messageQueue];
5604
5644
  }
5605
- /** Interrupt the active query unless a permission is pending (queued until it settles). */
5606
- requestInterrupt() {
5607
- if (this.abortController.signal.aborted || !this._activeQuery) return;
5608
- if (this._pendingPermissions.size > 0) {
5609
- this._interruptQueued = true;
5610
- return;
5611
- }
5612
- if (typeof this._activeQuery.interrupt !== "function") return;
5613
- this._activeQuery.interrupt().catch((err) => {
5614
- this.logger.warn({ workerId: this.id, err: String(err) }, "interrupt() failed");
5615
- });
5616
- }
5617
5645
  /**
5618
5646
  * Provide the engine-backed ask_controller implementation. Once set, the
5619
5647
  * worker exposes it to Claude Code as an in-process SDK MCP tool.
@@ -5651,9 +5679,7 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5651
5679
  ...this._turnLineage
5652
5680
  });
5653
5681
  return this._pendingPermissions.wait(requestId, _ClaudeCodeWorker.PERMISSION_TIMEOUT_MS, () => {
5654
- const value = { behavior: "deny", message: "Permission request timed out" };
5655
- this.afterPermissionSettled();
5656
- return value;
5682
+ return { behavior: "deny", message: "Permission request timed out" };
5657
5683
  });
5658
5684
  }
5659
5685
  // --- Prompt as AsyncIterable (supports follow-up messages) ---
@@ -5680,13 +5706,22 @@ ${leftoverText}` : `${buildThirdPartyPrompt(this.skills)}${initialPrompt}`;
5680
5706
  };
5681
5707
  }
5682
5708
  while (true) {
5683
- if (this.abortController.signal.aborted || this._terminalResult) {
5709
+ if (this.abortController.signal.aborted) {
5710
+ this._inputClosed = true;
5684
5711
  return { value: void 0, done: true };
5685
5712
  }
5686
- if (this._messageQueue.length > 0) break;
5687
- await new Promise((resolve11) => {
5688
- this._messageResolve = resolve11;
5689
- });
5713
+ if (this._terminalResult) {
5714
+ if (this._messageQueue.length === 0) {
5715
+ this._inputClosed = true;
5716
+ return { value: void 0, done: true };
5717
+ }
5718
+ } else if (this._messageQueue.length === 0) {
5719
+ await new Promise((resolve11) => {
5720
+ this._messageResolve = resolve11;
5721
+ });
5722
+ continue;
5723
+ }
5724
+ break;
5690
5725
  }
5691
5726
  const msg = this._messageQueue.shift();
5692
5727
  return {
@@ -6353,7 +6388,7 @@ function updateProgressTool(deps) {
6353
6388
  import { z as z23 } from "zod";
6354
6389
  function chatWorkerTool(deps) {
6355
6390
  return defineTool({
6356
- description: "Send a message to a running worker. Use this to provide additional guidance, correct the worker's approach, or ask the worker to check something. The worker reads it at its next step (puppet) or turn boundary (third-party); idle multi-stage workers read it when their next stage starts.",
6391
+ description: `Send a message to a running worker. Use this to provide additional guidance, correct the worker's approach, or ask the worker to check something. The worker reads it at its next step (puppet) or turn boundary (third-party); idle multi-stage workers read it when their next stage starts. A third-party worker NEVER acts on the message mid-tool-call: it finishes the current turn first, so "queued" can mean a delay of minutes. If the worker has already finished, this tool fails explicitly \u2014 inspect its output instead.`,
6357
6392
  parameters: z23.object({
6358
6393
  workerId: z23.string().describe("ID of the target worker"),
6359
6394
  message: z23.string().describe("Message to send to the worker")
@@ -6363,8 +6398,11 @@ function chatWorkerTool(deps) {
6363
6398
  if (status === "not_running") {
6364
6399
  return `[ERROR] Worker ${workerId} is not running (it may have just finished).`;
6365
6400
  }
6401
+ if (status === "finished") {
6402
+ return `[ERROR] Worker ${workerId} has already finished; message not delivered. Use get_task_detail to inspect its output.`;
6403
+ }
6366
6404
  if (status === "queued") {
6367
- return `Message queued for worker ${workerId} \u2014 it will be read when the current turn ends or the next stage starts.`;
6405
+ return `Message queued for worker ${workerId} \u2014 QUEUED only, not delivered yet: the worker reads it at its next step (puppet), turn boundary (third-party \u2014 the current turn, including any running tool call, finishes first) or next stage start (idle). A stuck worker will never reach that point. Verify with get_task_detail before relying on it; if it looks stuck, use cancel_task instead of waiting for this message.`;
6368
6406
  }
6369
6407
  return `Message delivered to worker ${workerId} \u2014 it will be read at the worker's next step.`;
6370
6408
  })
@@ -6885,14 +6923,16 @@ var TaskExecuteEngine = class {
6885
6923
  * Send a message to a worker. Delivery is judged by the activeWorkers
6886
6924
  * runtime state — NOT by the workers Map, which lingers one tick past
6887
6925
  * disposal (removeWorkerTracking defers) and would accept messages for a
6888
- * worker that is already gone.
6926
+ * worker that is already gone. A worker that rejects the message (its run
6927
+ * can no longer take input) is reported as 'finished' — never a fake
6928
+ * 'queued'.
6889
6929
  */
6890
6930
  sendMessageToWorker(workerId, message) {
6891
6931
  const info = this.getActiveWorkers().find((w) => w.workerId === workerId);
6892
6932
  if (!info) return "not_running";
6893
6933
  const worker = this.workers.get(workerId);
6894
6934
  if (!worker) return "not_running";
6895
- worker.acceptMessage(message);
6935
+ if (!worker.acceptMessage(message)) return "finished";
6896
6936
  if (info.status === "idle") return "queued";
6897
6937
  return worker instanceof WorkerAgent ? "delivered" : "queued";
6898
6938
  }
@@ -7556,7 +7596,7 @@ var ControllerLoop = class {
7556
7596
  this.signalQueue = signalQueue;
7557
7597
  this.controllerAgent = controllerAgent;
7558
7598
  this.heartbeatProvider = heartbeatProvider;
7559
- this.heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 3e4;
7599
+ this.heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 6e4;
7560
7600
  }
7561
7601
  ctx;
7562
7602
  signalQueue;
@@ -9245,8 +9285,11 @@ async function runWayangServer(options, io = process) {
9245
9285
  return { status };
9246
9286
  });
9247
9287
  rpc.register(METHOD.CHAT_WORKER, (params) => {
9248
- supervisor.engine.sendMessageToWorker(requiredString(params, "workerId"), requiredString(params, "message"));
9249
- return {};
9288
+ const status = supervisor.engine.sendMessageToWorker(
9289
+ requiredString(params, "workerId"),
9290
+ requiredString(params, "message")
9291
+ );
9292
+ return { status };
9250
9293
  });
9251
9294
  rpc.register(METHOD.DISPOSE_WORKER, (params) => {
9252
9295
  supervisor.engine.abortByWorkerId(requiredString(params, "workerId"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayang-ai",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Multi-agent orchestration platform",
5
5
  "type": "module",
6
6
  "license": "MIT",