wayang-ai 0.6.1 → 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 +143 -94
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -819,37 +819,6 @@ function formatWorkerTag(ctx) {
819
819
 
820
820
  // src/tui/components/chat-area/user-row.tsx
821
821
  import { Box, Text } from "ink";
822
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
823
- function UserRow({ item, width }) {
824
- const dim = !item.read;
825
- const text = item.content;
826
- const padded = text.length < width ? text.padEnd(width) : text;
827
- return /* @__PURE__ */ jsxs(Box, { children: [
828
- /* @__PURE__ */ jsxs(
829
- Text,
830
- {
831
- backgroundColor: theme.user.bgHex,
832
- color: theme.user.prefixColor,
833
- bold: true,
834
- children: [
835
- theme.user.prefix,
836
- " "
837
- ]
838
- }
839
- ),
840
- /* @__PURE__ */ jsx2(
841
- Text,
842
- {
843
- backgroundColor: theme.user.bgHex,
844
- color: dim ? theme.baseToken.color.textSubtle : theme.baseToken.color.textNormal,
845
- children: padded
846
- }
847
- )
848
- ] });
849
- }
850
-
851
- // src/tui/components/chat-area/assistant-step-row.tsx
852
- import { Box as Box2, Text as Text2 } from "ink";
853
822
 
854
823
  // src/tui/utils/text-width.ts
855
824
  function charWidth(cp) {
@@ -894,6 +863,43 @@ function shortId(id) {
894
863
  return id.slice(-4);
895
864
  }
896
865
 
866
+ // src/tui/components/chat-area/user-row.tsx
867
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
868
+ var PREFIX = `${theme.user.prefix} `;
869
+ var PREFIX_WIDTH = stringWidth(PREFIX);
870
+ function fillRow(line, width) {
871
+ const fill = width - stringWidth(line);
872
+ return fill > 0 ? line + " ".repeat(fill) : line;
873
+ }
874
+ function UserRow({ item, width }) {
875
+ const dim = !item.read;
876
+ const rowWidth = Math.max(0, width);
877
+ const textWidth = Math.max(0, rowWidth - PREFIX_WIDTH);
878
+ const body = item.content.split("\n").map((line) => stringWidth(line) <= textWidth ? fillRow(line, textWidth) : line).join("\n");
879
+ return /* @__PURE__ */ jsxs(Box, { width: rowWidth > 0 ? rowWidth : void 0, children: [
880
+ /* @__PURE__ */ jsx2(
881
+ Text,
882
+ {
883
+ backgroundColor: theme.user.bgHex,
884
+ color: theme.user.prefixColor,
885
+ bold: true,
886
+ children: PREFIX
887
+ }
888
+ ),
889
+ /* @__PURE__ */ jsx2(
890
+ Text,
891
+ {
892
+ backgroundColor: theme.user.bgHex,
893
+ color: dim ? theme.baseToken.color.textSubtle : theme.baseToken.color.textNormal,
894
+ children: body
895
+ }
896
+ )
897
+ ] });
898
+ }
899
+
900
+ // src/tui/components/chat-area/assistant-step-row.tsx
901
+ import { Box as Box2, Text as Text2 } from "ink";
902
+
897
903
  // src/tui/utils/permission-summary.ts
898
904
  function shortenPath(p, maxSegments = 3) {
899
905
  const segs = p.split("/").filter((s) => s.length > 0);
@@ -1098,14 +1104,17 @@ function summarizeWorkerMessage(argsJson, width, result, maxLines = 3) {
1098
1104
  const message = args ? str(args.message) ?? "" : argsJson;
1099
1105
  const { lines, hidden } = previewLines(message, maxLines, width);
1100
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
+ }
1101
1111
  return {
1102
1112
  ...workerId && { workerId },
1103
1113
  lines,
1104
1114
  hiddenLines: hidden,
1105
1115
  outcome: result === void 0 ? "pending" : failed ? "failed" : "ok",
1106
1116
  failure: failed ? failureText(result.text, width) : "",
1107
- ...result !== void 0 && !failed && /\bqueued\b/.test(result.text) && { delivery: "queued" },
1108
- ...result !== void 0 && !failed && /\bdelivered\b/.test(result.text) && { delivery: "delivered" }
1117
+ ...delivery && { delivery }
1109
1118
  };
1110
1119
  }
1111
1120
  var TASK_ID_RE = /^Task (\S+) (?:added|assigned)/;
@@ -2442,7 +2451,7 @@ function AppContent() {
2442
2451
  // package.json
2443
2452
  var package_default = {
2444
2453
  name: "wayang-ai",
2445
- version: "0.6.1",
2454
+ version: "0.6.3",
2446
2455
  description: "Multi-agent orchestration platform",
2447
2456
  type: "module",
2448
2457
  license: "MIT",
@@ -4531,18 +4540,39 @@ function controllerMessageContent(content) {
4531
4540
  return `[Controller message] ${content}`;
4532
4541
  }
4533
4542
  function createInboxInjection(deps) {
4543
+ let baseLength = null;
4534
4544
  const injected = [];
4535
4545
  return {
4536
4546
  prepareStep({ messages }) {
4537
4547
  const fresh = deps.drainInbox();
4538
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);
4539
4551
  for (const m of fresh) {
4540
- 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
+ });
4541
4558
  deps.onInject(m.content);
4542
4559
  }
4543
- 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 };
4544
4574
  },
4545
- injectedMessages: () => [...injected]
4575
+ injectedMessages: () => injected.map((i) => i.message)
4546
4576
  };
4547
4577
  }
4548
4578
 
@@ -4797,7 +4827,10 @@ The tool shows task metadata (status, result, error) and recent worker conversat
4797
4827
 
4798
4828
  ## Sending messages to workers (chat_worker)
4799
4829
 
4800
- 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.
4801
4834
  Use sparingly \u2014 only when you need to:
4802
4835
  - Correct a worker that is going in the wrong direction
4803
4836
  - Provide additional context the worker did not have
@@ -5266,7 +5299,11 @@ var WorkerAgent = class extends BaseAgent {
5266
5299
  getSnapshot(path7) {
5267
5300
  return this.state.get(path7);
5268
5301
  }
5269
- /** 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
+ */
5270
5307
  acceptMessage(message) {
5271
5308
  this.state.append("inbox", {
5272
5309
  id: generateId("msg"),
@@ -5274,6 +5311,7 @@ var WorkerAgent = class extends BaseAgent {
5274
5311
  timestamp: Date.now()
5275
5312
  });
5276
5313
  this.logger.info({ workerId: this.id }, "Controller message accepted");
5314
+ return true;
5277
5315
  }
5278
5316
  /** Unread controller messages (non-destructive) — engine reports these when the run ends. */
5279
5317
  pendingMessages() {
@@ -5370,14 +5408,20 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5370
5408
  // --- Controller messaging via streamInput ---
5371
5409
  _messageQueue = [];
5372
5410
  _messageResolve = null;
5373
- /** Active SDK query — interrupted to deliver controller messages promptly. */
5374
- _activeQuery = null;
5375
5411
  /**
5376
- * Interrupt requested while a permission was pending — delivered right
5377
- * after the permission settles (interrupting mid-permission is unproven
5378
- * 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.
5416
+ */
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.
5379
5423
  */
5380
- _interruptQueued = false;
5424
+ _inputClosed = false;
5381
5425
  // --- ask_controller via in-process SDK MCP server ---
5382
5426
  _askController = null;
5383
5427
  // --- Permission control via canUseTool ---
@@ -5386,31 +5430,17 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5386
5430
  /** Exposed for engine to resolve pending permission requests. */
5387
5431
  permissionHandler = {
5388
5432
  resolvePermission: (requestId, approved, reason) => {
5389
- const ok = this._pendingPermissions.resolve(
5433
+ return this._pendingPermissions.resolve(
5390
5434
  requestId,
5391
5435
  approved ? { behavior: "allow" } : { behavior: "deny", message: reason ?? "Denied by controller" }
5392
5436
  );
5393
- if (ok) this.afterPermissionSettled();
5394
- return ok;
5395
5437
  },
5396
5438
  cleanup: () => {
5397
- this._interruptQueued = false;
5398
5439
  this._pendingPermissions.cleanupAll(() => ({ behavior: "deny", message: "Worker aborted" }));
5399
5440
  },
5400
5441
  wrappedTools: {}
5401
5442
  // not used for ClaudeCodeWorker
5402
5443
  };
5403
- /**
5404
- * A permission settled (approved / denied / timed out). If a controller
5405
- * message was waiting for the permission window to close, deliver its
5406
- * interrupt now — this runs on BOTH the resolve and the timeout path.
5407
- */
5408
- afterPermissionSettled() {
5409
- if (this._interruptQueued && this._pendingPermissions.size === 0) {
5410
- this._interruptQueued = false;
5411
- this.requestInterrupt();
5412
- }
5413
- }
5414
5444
  // Signal context set during run()
5415
5445
  _signalQueue = null;
5416
5446
  _runContext = null;
@@ -5457,6 +5487,8 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5457
5487
  }
5458
5488
  async run(task, _tools, onProgress) {
5459
5489
  this._terminalResult = null;
5490
+ this._inputClosed = false;
5491
+ this._runInFlight = true;
5460
5492
  this.state.set("runtimeState.task", { id: task.id, description: task.description });
5461
5493
  const turnEnv = this.buildTurnEnv(task);
5462
5494
  this._currentTurnEnv = turnEnv;
@@ -5514,18 +5546,17 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5514
5546
  prompt: this.buildPromptIterable(task.description),
5515
5547
  options
5516
5548
  });
5517
- this._activeQuery = stream;
5518
5549
  try {
5519
5550
  for await (const message of stream) {
5520
5551
  if (this.abortController.signal.aborted) break;
5521
5552
  this.processStreamMessage(message, onProgress);
5522
- if (this._terminalResult) break;
5523
5553
  }
5524
5554
  } finally {
5525
- this._activeQuery = null;
5555
+ this._inputClosed = true;
5526
5556
  }
5527
5557
  } catch (err) {
5528
5558
  this.logger.error({ err, taskId: task.id }, "ClaudeCodeWorker query error");
5559
+ this._inputClosed = true;
5529
5560
  if (!this._terminalResult) {
5530
5561
  this._terminalResult = {
5531
5562
  status: "failed",
@@ -5552,6 +5583,7 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5552
5583
  startedAt: resultTs,
5553
5584
  endedAt: resultTs
5554
5585
  }, turnEnv));
5586
+ this._runInFlight = false;
5555
5587
  return this._terminalResult;
5556
5588
  }
5557
5589
  /** Set signal context for permission requests. Called by engine before run(). */
@@ -5580,34 +5612,36 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5580
5612
  getSnapshot(path7) {
5581
5613
  return this.state.get(path7);
5582
5614
  }
5583
- /** 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
+ */
5584
5630
  acceptMessage(message) {
5631
+ if (this.abortController.signal.aborted) return false;
5632
+ if (this._runInFlight && this._inputClosed) return false;
5585
5633
  this._messageQueue.push(message);
5586
5634
  if (this._messageResolve) {
5587
5635
  this._messageResolve();
5588
5636
  this._messageResolve = null;
5589
5637
  }
5590
- if ((this.config.interruptOnMessage ?? "immediate") === "immediate") {
5591
- this.requestInterrupt();
5592
- }
5593
5638
  this.logger.info({ workerId: this.id }, "Controller message accepted");
5639
+ return true;
5594
5640
  }
5595
5641
  /** Unread controller messages (non-destructive) — engine reports these when the run ends. */
5596
5642
  pendingMessages() {
5597
5643
  return [...this._messageQueue];
5598
5644
  }
5599
- /** Interrupt the active query unless a permission is pending (queued until it settles). */
5600
- requestInterrupt() {
5601
- if (this.abortController.signal.aborted || !this._activeQuery) return;
5602
- if (this._pendingPermissions.size > 0) {
5603
- this._interruptQueued = true;
5604
- return;
5605
- }
5606
- if (typeof this._activeQuery.interrupt !== "function") return;
5607
- this._activeQuery.interrupt().catch((err) => {
5608
- this.logger.warn({ workerId: this.id, err: String(err) }, "interrupt() failed");
5609
- });
5610
- }
5611
5645
  /**
5612
5646
  * Provide the engine-backed ask_controller implementation. Once set, the
5613
5647
  * worker exposes it to Claude Code as an in-process SDK MCP tool.
@@ -5645,9 +5679,7 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5645
5679
  ...this._turnLineage
5646
5680
  });
5647
5681
  return this._pendingPermissions.wait(requestId, _ClaudeCodeWorker.PERMISSION_TIMEOUT_MS, () => {
5648
- const value = { behavior: "deny", message: "Permission request timed out" };
5649
- this.afterPermissionSettled();
5650
- return value;
5682
+ return { behavior: "deny", message: "Permission request timed out" };
5651
5683
  });
5652
5684
  }
5653
5685
  // --- Prompt as AsyncIterable (supports follow-up messages) ---
@@ -5674,13 +5706,22 @@ ${leftoverText}` : `${buildThirdPartyPrompt(this.skills)}${initialPrompt}`;
5674
5706
  };
5675
5707
  }
5676
5708
  while (true) {
5677
- if (this.abortController.signal.aborted || this._terminalResult) {
5709
+ if (this.abortController.signal.aborted) {
5710
+ this._inputClosed = true;
5678
5711
  return { value: void 0, done: true };
5679
5712
  }
5680
- if (this._messageQueue.length > 0) break;
5681
- await new Promise((resolve11) => {
5682
- this._messageResolve = resolve11;
5683
- });
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;
5684
5725
  }
5685
5726
  const msg = this._messageQueue.shift();
5686
5727
  return {
@@ -6347,7 +6388,7 @@ function updateProgressTool(deps) {
6347
6388
  import { z as z23 } from "zod";
6348
6389
  function chatWorkerTool(deps) {
6349
6390
  return defineTool({
6350
- 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.`,
6351
6392
  parameters: z23.object({
6352
6393
  workerId: z23.string().describe("ID of the target worker"),
6353
6394
  message: z23.string().describe("Message to send to the worker")
@@ -6357,8 +6398,11 @@ function chatWorkerTool(deps) {
6357
6398
  if (status === "not_running") {
6358
6399
  return `[ERROR] Worker ${workerId} is not running (it may have just finished).`;
6359
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
+ }
6360
6404
  if (status === "queued") {
6361
- 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.`;
6362
6406
  }
6363
6407
  return `Message delivered to worker ${workerId} \u2014 it will be read at the worker's next step.`;
6364
6408
  })
@@ -6879,14 +6923,16 @@ var TaskExecuteEngine = class {
6879
6923
  * Send a message to a worker. Delivery is judged by the activeWorkers
6880
6924
  * runtime state — NOT by the workers Map, which lingers one tick past
6881
6925
  * disposal (removeWorkerTracking defers) and would accept messages for a
6882
- * 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'.
6883
6929
  */
6884
6930
  sendMessageToWorker(workerId, message) {
6885
6931
  const info = this.getActiveWorkers().find((w) => w.workerId === workerId);
6886
6932
  if (!info) return "not_running";
6887
6933
  const worker = this.workers.get(workerId);
6888
6934
  if (!worker) return "not_running";
6889
- worker.acceptMessage(message);
6935
+ if (!worker.acceptMessage(message)) return "finished";
6890
6936
  if (info.status === "idle") return "queued";
6891
6937
  return worker instanceof WorkerAgent ? "delivered" : "queued";
6892
6938
  }
@@ -7550,7 +7596,7 @@ var ControllerLoop = class {
7550
7596
  this.signalQueue = signalQueue;
7551
7597
  this.controllerAgent = controllerAgent;
7552
7598
  this.heartbeatProvider = heartbeatProvider;
7553
- this.heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 3e4;
7599
+ this.heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 6e4;
7554
7600
  }
7555
7601
  ctx;
7556
7602
  signalQueue;
@@ -9239,8 +9285,11 @@ async function runWayangServer(options, io = process) {
9239
9285
  return { status };
9240
9286
  });
9241
9287
  rpc.register(METHOD.CHAT_WORKER, (params) => {
9242
- supervisor.engine.sendMessageToWorker(requiredString(params, "workerId"), requiredString(params, "message"));
9243
- return {};
9288
+ const status = supervisor.engine.sendMessageToWorker(
9289
+ requiredString(params, "workerId"),
9290
+ requiredString(params, "message")
9291
+ );
9292
+ return { status };
9244
9293
  });
9245
9294
  rpc.register(METHOD.DISPOSE_WORKER, (params) => {
9246
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.1",
3
+ "version": "0.6.3",
4
4
  "description": "Multi-agent orchestration platform",
5
5
  "type": "module",
6
6
  "license": "MIT",