wayang-ai 0.9.3 → 0.9.4
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/cli.js +907 -90
- package/dist/mcp-bridge-entry.js +379 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1457,7 +1457,7 @@ function validateWorkerEntry(id, worker) {
|
|
|
1457
1457
|
return `${at}.emoji must be a string (got ${JSON.stringify(worker.emoji)})`;
|
|
1458
1458
|
}
|
|
1459
1459
|
if (type === "claude-code") {
|
|
1460
|
-
for (const key of ["command", "args", "env", "protocol", "trustUpdatesAfterCancel"]) {
|
|
1460
|
+
for (const key of ["command", "args", "env", "protocol", "trustUpdatesAfterCancel", "mcp"]) {
|
|
1461
1461
|
if (worker[key] !== void 0) {
|
|
1462
1462
|
return `${at}.${key} is not supported for type "claude-code" (claude-code uses cliPath)`;
|
|
1463
1463
|
}
|
|
@@ -1473,6 +1473,22 @@ function validateWorkerEntry(id, worker) {
|
|
|
1473
1473
|
if (worker.trustUpdatesAfterCancel !== void 0 && typeof worker.trustUpdatesAfterCancel !== "boolean") {
|
|
1474
1474
|
return `${at}.trustUpdatesAfterCancel must be a boolean (got ${JSON.stringify(worker.trustUpdatesAfterCancel)})`;
|
|
1475
1475
|
}
|
|
1476
|
+
const mcp = worker.mcp;
|
|
1477
|
+
if (mcp !== void 0) {
|
|
1478
|
+
if (typeof mcp !== "object" || mcp === null || Array.isArray(mcp)) {
|
|
1479
|
+
return `${at}.mcp must be an object (got ${JSON.stringify(mcp)})`;
|
|
1480
|
+
}
|
|
1481
|
+
const record = mcp;
|
|
1482
|
+
for (const key of Object.keys(record)) {
|
|
1483
|
+
if (key !== "enabled" && key !== "fallbackMs") return `${at}.mcp.${key} is not a known option (allowed: enabled, fallbackMs)`;
|
|
1484
|
+
}
|
|
1485
|
+
if (record.enabled !== void 0 && typeof record.enabled !== "boolean") {
|
|
1486
|
+
return `${at}.mcp.enabled must be a boolean (got ${JSON.stringify(record.enabled)})`;
|
|
1487
|
+
}
|
|
1488
|
+
if (record.fallbackMs !== void 0 && !(typeof record.fallbackMs === "number" && Number.isFinite(record.fallbackMs) && record.fallbackMs > 0)) {
|
|
1489
|
+
return `${at}.mcp.fallbackMs must be a positive number (got ${JSON.stringify(record.fallbackMs)})`;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1476
1492
|
if (worker.args !== void 0 && (!Array.isArray(worker.args) || worker.args.some((a) => typeof a !== "string"))) {
|
|
1477
1493
|
return `${at}.args must be an array of strings (got ${JSON.stringify(worker.args)})`;
|
|
1478
1494
|
}
|
|
@@ -5694,7 +5710,7 @@ function AppContent() {
|
|
|
5694
5710
|
// package.json
|
|
5695
5711
|
var package_default = {
|
|
5696
5712
|
name: "wayang-ai",
|
|
5697
|
-
version: "0.9.
|
|
5713
|
+
version: "0.9.4",
|
|
5698
5714
|
description: "Multi-agent orchestration platform",
|
|
5699
5715
|
type: "module",
|
|
5700
5716
|
license: "MIT",
|
|
@@ -8051,7 +8067,8 @@ function buildWorkerSystemPrompt(ctx, skills, workspaceDir2) {
|
|
|
8051
8067
|
var RULES = [
|
|
8052
8068
|
"<Rules>",
|
|
8053
8069
|
"Output plain text only. No Markdown formatting.",
|
|
8054
|
-
"Report meaningful progress as you work (entering a new phase, completing a sub-step, recovering from an error).",
|
|
8070
|
+
"Report meaningful progress as you work (entering a new phase, completing a sub-step, recovering from an error). When an update_progress tool is available it is the primary channel \u2014 call it with a concise message; plain assistant text is only a degraded fallback.",
|
|
8071
|
+
"If no update_progress is reported for 2 minutes, the system reports your latest complete reply automatically (capped at 20000 characters), so keep reporting progress yourself to stay in control of what the controller sees.",
|
|
8055
8072
|
"Do NOT output a final summary right before completing \u2014 the system already captures the result separately.",
|
|
8056
8073
|
"Match the language of the task description.",
|
|
8057
8074
|
"Keep scratch files and intermediate artifacts in `.agents/tmp/` inside the task workspace \u2014 never `/tmp`, the home directory, or the repository root.",
|
|
@@ -12389,7 +12406,7 @@ var AcpClient = class {
|
|
|
12389
12406
|
() => new AcpError("agent-error", `authenticate(${methodId}): no response within ${this.timeouts.handshakeMs}ms`)
|
|
12390
12407
|
);
|
|
12391
12408
|
}
|
|
12392
|
-
/** mcpServers is required by the v1 schema;
|
|
12409
|
+
/** mcpServers is required by the v1 schema; the worker layer passes its injection (or [] on degrade). Unknown response fields are ignored. */
|
|
12393
12410
|
async sessionNew(input2) {
|
|
12394
12411
|
const raw = await this.rpcRequest(
|
|
12395
12412
|
"session/new",
|
|
@@ -12658,10 +12675,7 @@ function mapSessionUpdate(update, acc) {
|
|
|
12658
12675
|
const { content, messageId } = update;
|
|
12659
12676
|
const text = content && content.type === "text" ? content.text : "";
|
|
12660
12677
|
if (!text) return [{ kind: "ignored", variant, reason: "empty-chunk" }];
|
|
12661
|
-
return [
|
|
12662
|
-
{ kind: "agent-text", text, messageId: messageId ?? null },
|
|
12663
|
-
{ kind: "progress", text }
|
|
12664
|
-
];
|
|
12678
|
+
return [{ kind: "agent-text", text, messageId: messageId ?? null }];
|
|
12665
12679
|
}
|
|
12666
12680
|
if (variant === "agent_thought_chunk") {
|
|
12667
12681
|
return [{ kind: "ignored", variant, reason: "thought" }];
|
|
@@ -12709,6 +12723,48 @@ function mapSessionUpdate(update, acc) {
|
|
|
12709
12723
|
return [{ kind: "ignored", variant, reason: "unknown-variant" }];
|
|
12710
12724
|
}
|
|
12711
12725
|
|
|
12726
|
+
// src/services/agents/acp/mcp-schemas.ts
|
|
12727
|
+
var MCP_SERVER_NAME = "wayang";
|
|
12728
|
+
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
12729
|
+
var MAX_PROGRESS_CHARS = 2e4;
|
|
12730
|
+
var MAX_QUESTION_CHARS = 500;
|
|
12731
|
+
var MAX_CONTEXT_CHARS = 1e3;
|
|
12732
|
+
var TRUNCATION_MARKER = "\n\u2026[truncated]";
|
|
12733
|
+
var PROGRESS_FALLBACK_MS = 6e4;
|
|
12734
|
+
var MCP_PROGRESS_TIMEOUT_MS = 5e3;
|
|
12735
|
+
var MCP_ASK_TIMEOUT_MS = 31e4;
|
|
12736
|
+
var MCP_TOOLS = [
|
|
12737
|
+
{
|
|
12738
|
+
name: "update_progress",
|
|
12739
|
+
description: "Report progress for the current task. This is the PRIMARY progress channel: call it when entering a new phase, finishing a sub-deliverable, recovering from an error. Do not rely on session/update text \u2014 it is only a degraded fallback.",
|
|
12740
|
+
inputSchema: {
|
|
12741
|
+
type: "object",
|
|
12742
|
+
properties: {
|
|
12743
|
+
message: { type: "string", maxLength: MAX_PROGRESS_CHARS, description: "Progress description" }
|
|
12744
|
+
},
|
|
12745
|
+
required: ["message"],
|
|
12746
|
+
additionalProperties: false
|
|
12747
|
+
}
|
|
12748
|
+
},
|
|
12749
|
+
{
|
|
12750
|
+
name: "ask_controller",
|
|
12751
|
+
description: "Ask the controller a question and WAIT for the answer. Use ONLY when blocked: execution cannot continue as instructed (contradiction, missing resource, unexpected state), or continuing would risk significant wasted work. Do NOT use it for decisions you can make yourself \u2014 decide those autonomously and report via update_progress.",
|
|
12752
|
+
inputSchema: {
|
|
12753
|
+
type: "object",
|
|
12754
|
+
properties: {
|
|
12755
|
+
question: { type: "string", maxLength: MAX_QUESTION_CHARS, description: "The specific question you need answered to proceed. Be concise and self-contained." },
|
|
12756
|
+
context: { type: "string", maxLength: MAX_CONTEXT_CHARS, description: "What you have tried and where you are stuck, so the controller can answer precisely." }
|
|
12757
|
+
},
|
|
12758
|
+
required: ["question"],
|
|
12759
|
+
additionalProperties: false
|
|
12760
|
+
}
|
|
12761
|
+
}
|
|
12762
|
+
];
|
|
12763
|
+
var MCP_TOOL_NAMES = MCP_TOOLS.map((tool2) => tool2.name);
|
|
12764
|
+
function capText(text, max = MAX_PROGRESS_CHARS) {
|
|
12765
|
+
return text.length > max ? `${text.slice(0, max)}${TRUNCATION_MARKER}` : text;
|
|
12766
|
+
}
|
|
12767
|
+
|
|
12712
12768
|
// src/services/agents/acp/permission-bridge.ts
|
|
12713
12769
|
var ACP_PERMISSION_TIMEOUT_MS = 3e5;
|
|
12714
12770
|
function pathFromSnapshot(snapshot) {
|
|
@@ -12733,7 +12789,37 @@ function commandFromSnapshot(snapshot) {
|
|
|
12733
12789
|
if (Array.isArray(cmd)) return cmd.every((part) => typeof part === "string") ? cmd.join(" ") : void 0;
|
|
12734
12790
|
return void 0;
|
|
12735
12791
|
}
|
|
12792
|
+
var SAFE_WAYANG_MCP_TOOLS = /* @__PURE__ */ new Set([
|
|
12793
|
+
"update_progress",
|
|
12794
|
+
"ask_controller"
|
|
12795
|
+
]);
|
|
12796
|
+
function mcpToolIdentity(name) {
|
|
12797
|
+
const lower = name.trim().toLowerCase();
|
|
12798
|
+
if (lower.startsWith("mcp__")) {
|
|
12799
|
+
const rest = lower.slice("mcp__".length);
|
|
12800
|
+
const sep2 = rest.lastIndexOf("__");
|
|
12801
|
+
if (sep2 <= 0 || sep2 + 2 >= rest.length) return void 0;
|
|
12802
|
+
return { server: rest.slice(0, sep2), tool: rest.slice(sep2 + 2) };
|
|
12803
|
+
}
|
|
12804
|
+
const qualified = /^([a-z0-9_]+)[.-]([a-z0-9_]+)$/.exec(lower);
|
|
12805
|
+
return qualified ? { server: qualified[1], tool: qualified[2] } : void 0;
|
|
12806
|
+
}
|
|
12807
|
+
function safeOwnMcpTool(snapshot) {
|
|
12808
|
+
const raw = snapshot.rawInput;
|
|
12809
|
+
const declared = raw && typeof raw === "object" && !Array.isArray(raw) ? raw.toolName : void 0;
|
|
12810
|
+
for (const candidate of [snapshot.name, declared]) {
|
|
12811
|
+
if (typeof candidate !== "string" || candidate.length === 0) continue;
|
|
12812
|
+
const identity = mcpToolIdentity(candidate);
|
|
12813
|
+
if (!identity || identity.server !== MCP_SERVER_NAME) continue;
|
|
12814
|
+
const tool2 = identity.tool;
|
|
12815
|
+
if (SAFE_WAYANG_MCP_TOOLS.has(tool2)) return tool2;
|
|
12816
|
+
}
|
|
12817
|
+
return void 0;
|
|
12818
|
+
}
|
|
12736
12819
|
function classifyAcpToolCall(snapshot) {
|
|
12820
|
+
if (safeOwnMcpTool(snapshot) !== void 0) {
|
|
12821
|
+
return { type: "direct-allow", reason: "wayang MCP controller-facing tool" };
|
|
12822
|
+
}
|
|
12737
12823
|
const kind = snapshot.kind;
|
|
12738
12824
|
if (kind === "fetch" || kind === "think" || kind === "switch_mode") {
|
|
12739
12825
|
return { type: "direct-allow", reason: `non-path kind "${kind}"` };
|
|
@@ -13033,6 +13119,14 @@ var AcpTurnWriter = class {
|
|
|
13033
13119
|
lastText() {
|
|
13034
13120
|
return this.lastFlushedText;
|
|
13035
13121
|
}
|
|
13122
|
+
/**
|
|
13123
|
+
* Read-only view of the OPEN text segment: every `agent-text` chunk the run
|
|
13124
|
+
* has aggregated since the last flush. This is the single text source for
|
|
13125
|
+
* progress reporting (the degraded path must never keep a second copy).
|
|
13126
|
+
*/
|
|
13127
|
+
openText() {
|
|
13128
|
+
return this.openTextParts.join("");
|
|
13129
|
+
}
|
|
13036
13130
|
/**
|
|
13037
13131
|
* Buffer a tool call. A same-id record (running → terminal evolution of one
|
|
13038
13132
|
* call) replaces the previous one inside its owning step instead of piling
|
|
@@ -13418,9 +13512,10 @@ var BaseAcpWorker = class {
|
|
|
13418
13512
|
_grants = new ApprovalMemory();
|
|
13419
13513
|
_approvalMemory = "task";
|
|
13420
13514
|
_currentTurnEnv = null;
|
|
13421
|
-
// --- Progress
|
|
13422
|
-
|
|
13423
|
-
|
|
13515
|
+
// --- Progress (update_progress is primary; the idle tracker is the net) ---
|
|
13516
|
+
_onProgress = null;
|
|
13517
|
+
/** Engine-backed inquiry channel, exposed to the agent as ask_controller. */
|
|
13518
|
+
_askController = null;
|
|
13424
13519
|
// --- ACP runtime (per run) ---
|
|
13425
13520
|
_accumulator = createAcpToolAccumulator();
|
|
13426
13521
|
_turnWriter;
|
|
@@ -13558,6 +13653,14 @@ var BaseAcpWorker = class {
|
|
|
13558
13653
|
this._grants = deps.grants ?? new ApprovalMemory();
|
|
13559
13654
|
this._approvalMemory = deps.approvalMemory ?? "task";
|
|
13560
13655
|
}
|
|
13656
|
+
/**
|
|
13657
|
+
* Wire the engine-backed ask_controller implementation. The per-run Wayang
|
|
13658
|
+
* MCP bridge serves it over a Unix socket (member presence is what the
|
|
13659
|
+
* engine's `wireWorker` probe detects, as with ClaudeCodeWorker).
|
|
13660
|
+
*/
|
|
13661
|
+
setInquirySupport(ask) {
|
|
13662
|
+
this._askController = ask;
|
|
13663
|
+
}
|
|
13561
13664
|
getState() {
|
|
13562
13665
|
return this.state;
|
|
13563
13666
|
}
|
|
@@ -13620,13 +13723,6 @@ var BaseAcpWorker = class {
|
|
|
13620
13723
|
const prev = this.state.get("runtimeState") ?? {};
|
|
13621
13724
|
this.state.set("runtimeState", { ...prev, acp: { ...prev.acp ?? {}, ...fields } });
|
|
13622
13725
|
}
|
|
13623
|
-
clearPendingProgress() {
|
|
13624
|
-
if (this._progressTimer) {
|
|
13625
|
-
clearTimeout(this._progressTimer);
|
|
13626
|
-
this._progressTimer = null;
|
|
13627
|
-
}
|
|
13628
|
-
this._pendingProgress = null;
|
|
13629
|
-
}
|
|
13630
13726
|
};
|
|
13631
13727
|
|
|
13632
13728
|
// src/services/agents/acp/acp-worker-run.ts
|
|
@@ -13670,13 +13766,678 @@ function makeTerminalResult(category, verdictReason, denials, summary, stopReaso
|
|
|
13670
13766
|
return { status: "failed", error };
|
|
13671
13767
|
}
|
|
13672
13768
|
|
|
13769
|
+
// src/services/agents/acp/mcp-transport.ts
|
|
13770
|
+
import { createServer } from "net";
|
|
13771
|
+
import { chmodSync, existsSync as existsSync11, mkdirSync as mkdirSync8, unlinkSync as unlinkSync4 } from "fs";
|
|
13772
|
+
import { dirname as dirname9, join as join18 } from "path";
|
|
13773
|
+
import { fileURLToPath } from "url";
|
|
13774
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
13775
|
+
|
|
13776
|
+
// src/services/agents/acp/mcp-protocol.ts
|
|
13777
|
+
function mcpResult(id, result2) {
|
|
13778
|
+
return { jsonrpc: "2.0", id, result: result2 };
|
|
13779
|
+
}
|
|
13780
|
+
function mcpError(id, code, message) {
|
|
13781
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
13782
|
+
}
|
|
13783
|
+
function mcpToolResultPayload(call) {
|
|
13784
|
+
return {
|
|
13785
|
+
content: [{ type: "text", text: call.text }],
|
|
13786
|
+
...call.isError ? { isError: true } : {}
|
|
13787
|
+
};
|
|
13788
|
+
}
|
|
13789
|
+
function createMcpDispatcher(invoker, serverInfo) {
|
|
13790
|
+
return async (frame) => {
|
|
13791
|
+
const method = typeof frame.method === "string" ? frame.method : "";
|
|
13792
|
+
if (frame.id === void 0 || frame.id === null || method.startsWith("notifications/")) return null;
|
|
13793
|
+
const id = frame.id;
|
|
13794
|
+
switch (method) {
|
|
13795
|
+
case "initialize":
|
|
13796
|
+
return mcpResult(id, {
|
|
13797
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
13798
|
+
capabilities: { tools: {} },
|
|
13799
|
+
serverInfo
|
|
13800
|
+
});
|
|
13801
|
+
case "ping":
|
|
13802
|
+
return mcpResult(id, {});
|
|
13803
|
+
case "tools/list":
|
|
13804
|
+
return mcpResult(id, { tools: MCP_TOOLS });
|
|
13805
|
+
case "tools/call": {
|
|
13806
|
+
const params = frame.params ?? {};
|
|
13807
|
+
const name = typeof params.name === "string" ? params.name : "";
|
|
13808
|
+
if (!MCP_TOOL_NAMES.includes(name)) {
|
|
13809
|
+
return mcpError(id, -32602, `unknown tool "${name}" (available: ${MCP_TOOL_NAMES.join(", ")})`);
|
|
13810
|
+
}
|
|
13811
|
+
const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
|
|
13812
|
+
try {
|
|
13813
|
+
const call = await invoker(name, args);
|
|
13814
|
+
return mcpResult(id, mcpToolResultPayload(call));
|
|
13815
|
+
} catch (err) {
|
|
13816
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13817
|
+
return mcpResult(id, mcpToolResultPayload({ text: `[MCP ERROR] ${message}`, isError: true }));
|
|
13818
|
+
}
|
|
13819
|
+
}
|
|
13820
|
+
default:
|
|
13821
|
+
return mcpError(id, -32601, `method not found: ${method || "(missing)"}`);
|
|
13822
|
+
}
|
|
13823
|
+
};
|
|
13824
|
+
}
|
|
13825
|
+
var DEFAULT_MCP_SERVER_INFO = { name: MCP_SERVER_NAME, version: "wayang" };
|
|
13826
|
+
|
|
13827
|
+
// src/services/agents/acp/mcp-transport.ts
|
|
13828
|
+
var MCP_ENV_SOCKET = "WAYANG_MCP_SOCK";
|
|
13829
|
+
var MCP_ENV_TOKEN = "WAYANG_MCP_TOKEN";
|
|
13830
|
+
var MCP_ENV_WORKER_ID = "WAYANG_MCP_WORKER_ID";
|
|
13831
|
+
var MCP_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
13832
|
+
var MCP_MAX_CONNECTIONS = 4;
|
|
13833
|
+
function generateMcpToken() {
|
|
13834
|
+
return randomBytes(32).toString("hex");
|
|
13835
|
+
}
|
|
13836
|
+
function mcpSocketPathFor(sessionDir, workerId, suffix) {
|
|
13837
|
+
return join18(sessionDir, ".mcp", `acp-mcp-${workerId}-${suffix}.sock`);
|
|
13838
|
+
}
|
|
13839
|
+
function resolveMcpBridgeEntryPath(importMetaUrl) {
|
|
13840
|
+
const dir = dirname9(fileURLToPath(importMetaUrl));
|
|
13841
|
+
const candidates = [
|
|
13842
|
+
join18(dir, "mcp-bridge-entry.js"),
|
|
13843
|
+
join18(dir, "services/agents/acp/mcp-bridge-entry.js"),
|
|
13844
|
+
join18(dir, "mcp-bridge-entry.ts")
|
|
13845
|
+
];
|
|
13846
|
+
return candidates.find((candidate) => existsSync11(candidate)) ?? candidates[0];
|
|
13847
|
+
}
|
|
13848
|
+
function safeEqualToken(expected, actual) {
|
|
13849
|
+
if (typeof actual !== "string") return false;
|
|
13850
|
+
const a = Buffer.from(expected, "utf8");
|
|
13851
|
+
const b = Buffer.from(actual, "utf8");
|
|
13852
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
13853
|
+
}
|
|
13854
|
+
function stopBridgeProcesses(pids, logger, workerId) {
|
|
13855
|
+
const alive = [...pids].filter((pid) => pid > 0 && pid !== process.pid);
|
|
13856
|
+
if (alive.length === 0) return;
|
|
13857
|
+
for (const pid of alive) {
|
|
13858
|
+
try {
|
|
13859
|
+
process.kill(pid, "SIGTERM");
|
|
13860
|
+
} catch {
|
|
13861
|
+
}
|
|
13862
|
+
}
|
|
13863
|
+
const ladder = setTimeout(() => {
|
|
13864
|
+
for (const pid of alive) {
|
|
13865
|
+
try {
|
|
13866
|
+
process.kill(pid, 0);
|
|
13867
|
+
process.kill(pid, "SIGKILL");
|
|
13868
|
+
} catch {
|
|
13869
|
+
}
|
|
13870
|
+
}
|
|
13871
|
+
}, 2e3);
|
|
13872
|
+
ladder.unref?.();
|
|
13873
|
+
logger.debug({ workerId, count: alive.length, reason: "bridge-cleanup" }, "ACP MCP bridge processes signalled");
|
|
13874
|
+
}
|
|
13875
|
+
async function startMcpHost(opts) {
|
|
13876
|
+
const dir = dirname9(opts.socketPath);
|
|
13877
|
+
mkdirSync8(dir, { recursive: true, mode: 448 });
|
|
13878
|
+
chmodSync(dir, 448);
|
|
13879
|
+
try {
|
|
13880
|
+
unlinkSync4(opts.socketPath);
|
|
13881
|
+
} catch (err) {
|
|
13882
|
+
if (err.code !== "ENOENT") throw err;
|
|
13883
|
+
}
|
|
13884
|
+
let closed = false;
|
|
13885
|
+
let ready = false;
|
|
13886
|
+
let resolveReady = () => void 0;
|
|
13887
|
+
const readyPromise = new Promise((resolve15) => {
|
|
13888
|
+
resolveReady = resolve15;
|
|
13889
|
+
});
|
|
13890
|
+
const connections = /* @__PURE__ */ new Set();
|
|
13891
|
+
const bridgePids = /* @__PURE__ */ new Set();
|
|
13892
|
+
const handshakeTimer = setTimeout(() => {
|
|
13893
|
+
if (!closed && !ready) {
|
|
13894
|
+
opts.logger.warn({ workerId: opts.workerId, reason: "mcp-handshake-timeout" }, "ACP MCP bridge did not connect");
|
|
13895
|
+
resolveReady(false);
|
|
13896
|
+
}
|
|
13897
|
+
}, opts.handshakeTimeoutMs ?? MCP_HANDSHAKE_TIMEOUT_MS);
|
|
13898
|
+
handshakeTimer.unref?.();
|
|
13899
|
+
const dispatcher = createMcpDispatcher(
|
|
13900
|
+
(name, args) => opts.invoker.invoke(name, args),
|
|
13901
|
+
opts.serverInfo ?? DEFAULT_MCP_SERVER_INFO
|
|
13902
|
+
);
|
|
13903
|
+
const handleFrame = async (frame, socket) => {
|
|
13904
|
+
if (closed || typeof frame.method !== "string") return;
|
|
13905
|
+
try {
|
|
13906
|
+
const response = await dispatcher(frame);
|
|
13907
|
+
if (response && !socket.destroyed) {
|
|
13908
|
+
socket.write(`${JSON.stringify(response)}
|
|
13909
|
+
`);
|
|
13910
|
+
}
|
|
13911
|
+
} catch (err) {
|
|
13912
|
+
opts.logger.debug({ workerId: opts.workerId, err, reason: "mcp-dispatch" }, "ACP MCP request failed");
|
|
13913
|
+
}
|
|
13914
|
+
};
|
|
13915
|
+
const accept = (socket) => {
|
|
13916
|
+
if (closed || connections.size >= MCP_MAX_CONNECTIONS) {
|
|
13917
|
+
socket.destroy();
|
|
13918
|
+
return;
|
|
13919
|
+
}
|
|
13920
|
+
connections.add(socket);
|
|
13921
|
+
socket.unref?.();
|
|
13922
|
+
const codec = new LineCodec();
|
|
13923
|
+
let helloChecked = false;
|
|
13924
|
+
socket.setEncoding("utf8");
|
|
13925
|
+
socket.on("data", (chunk) => {
|
|
13926
|
+
const frames = codec.push(chunk);
|
|
13927
|
+
for (const frame of frames) {
|
|
13928
|
+
if (!helloChecked) {
|
|
13929
|
+
helloChecked = true;
|
|
13930
|
+
const params = frame?.params ?? {};
|
|
13931
|
+
const valid = frame?.method === "hello" && params.workerId === opts.workerId && safeEqualToken(opts.token, params.token);
|
|
13932
|
+
if (!valid) {
|
|
13933
|
+
opts.logger.warn({ workerId: opts.workerId, reason: "mcp-hello-rejected" }, "ACP MCP bridge hello rejected");
|
|
13934
|
+
socket.destroy();
|
|
13935
|
+
return;
|
|
13936
|
+
}
|
|
13937
|
+
const pid = Number(params.pid);
|
|
13938
|
+
if (Number.isInteger(pid) && pid > 0) bridgePids.add(pid);
|
|
13939
|
+
ready = true;
|
|
13940
|
+
clearTimeout(handshakeTimer);
|
|
13941
|
+
resolveReady(true);
|
|
13942
|
+
continue;
|
|
13943
|
+
}
|
|
13944
|
+
void handleFrame(frame, socket);
|
|
13945
|
+
}
|
|
13946
|
+
});
|
|
13947
|
+
socket.on("close", () => connections.delete(socket));
|
|
13948
|
+
socket.on("error", () => {
|
|
13949
|
+
});
|
|
13950
|
+
};
|
|
13951
|
+
const server = createServer(accept);
|
|
13952
|
+
await new Promise((resolve15, reject) => {
|
|
13953
|
+
const onError = (err) => reject(err);
|
|
13954
|
+
server.once("error", onError);
|
|
13955
|
+
server.listen(opts.socketPath, () => {
|
|
13956
|
+
server.off("error", onError);
|
|
13957
|
+
resolve15();
|
|
13958
|
+
});
|
|
13959
|
+
});
|
|
13960
|
+
chmodSync(opts.socketPath, 384);
|
|
13961
|
+
server.unref();
|
|
13962
|
+
const close = async () => {
|
|
13963
|
+
if (closed) return;
|
|
13964
|
+
closed = true;
|
|
13965
|
+
clearTimeout(handshakeTimer);
|
|
13966
|
+
if (!ready) resolveReady(false);
|
|
13967
|
+
opts.invoker.settlePending();
|
|
13968
|
+
await new Promise((resolve15) => setImmediate(resolve15));
|
|
13969
|
+
for (const socket of connections) socket.destroy();
|
|
13970
|
+
connections.clear();
|
|
13971
|
+
await new Promise((resolve15) => {
|
|
13972
|
+
if (!server.listening) return resolve15();
|
|
13973
|
+
server.close(() => resolve15());
|
|
13974
|
+
});
|
|
13975
|
+
try {
|
|
13976
|
+
unlinkSync4(opts.socketPath);
|
|
13977
|
+
} catch {
|
|
13978
|
+
}
|
|
13979
|
+
stopBridgeProcesses(bridgePids, opts.logger, opts.workerId);
|
|
13980
|
+
};
|
|
13981
|
+
return { ready: readyPromise, isReady: () => ready && !closed, close };
|
|
13982
|
+
}
|
|
13983
|
+
function buildMcpServerDescriptor(opts) {
|
|
13984
|
+
return {
|
|
13985
|
+
name: MCP_SERVER_NAME,
|
|
13986
|
+
command: opts.nodeExecutable ?? process.execPath,
|
|
13987
|
+
args: [opts.bridgeEntryPath],
|
|
13988
|
+
env: [
|
|
13989
|
+
{ name: MCP_ENV_SOCKET, value: opts.socketPath },
|
|
13990
|
+
{ name: MCP_ENV_TOKEN, value: opts.token },
|
|
13991
|
+
{ name: MCP_ENV_WORKER_ID, value: opts.workerId }
|
|
13992
|
+
]
|
|
13993
|
+
};
|
|
13994
|
+
}
|
|
13995
|
+
async function startStdioMcpTransport(opts) {
|
|
13996
|
+
const socketPath = mcpSocketPathFor(opts.sessionDir, opts.workerId, generateMcpToken().slice(0, 12));
|
|
13997
|
+
const token = generateMcpToken();
|
|
13998
|
+
const bridgeEntryPath = resolveMcpBridgeEntryPath(import.meta.url);
|
|
13999
|
+
const host = await startMcpHost({
|
|
14000
|
+
socketPath,
|
|
14001
|
+
token,
|
|
14002
|
+
workerId: opts.workerId,
|
|
14003
|
+
invoker: opts.invoker,
|
|
14004
|
+
logger: opts.logger,
|
|
14005
|
+
serverInfo: opts.serverInfo
|
|
14006
|
+
});
|
|
14007
|
+
return {
|
|
14008
|
+
descriptor: buildMcpServerDescriptor({ socketPath, token, workerId: opts.workerId, bridgeEntryPath }),
|
|
14009
|
+
ready: host.ready,
|
|
14010
|
+
isReady: host.isReady,
|
|
14011
|
+
close: host.close
|
|
14012
|
+
};
|
|
14013
|
+
}
|
|
14014
|
+
|
|
14015
|
+
// src/services/agents/acp/mcp-progress-fallback.ts
|
|
14016
|
+
function isAfter(a, b) {
|
|
14017
|
+
return a.turn > b.turn || a.turn === b.turn && a.chars > b.chars;
|
|
14018
|
+
}
|
|
14019
|
+
var McpProgressTracker = class {
|
|
14020
|
+
canFire;
|
|
14021
|
+
emit;
|
|
14022
|
+
now;
|
|
14023
|
+
idleMs;
|
|
14024
|
+
maxChars;
|
|
14025
|
+
timer = null;
|
|
14026
|
+
lastProgressAt = 0;
|
|
14027
|
+
/** The latest complete entry handed by the run layer (single slot). */
|
|
14028
|
+
reply = null;
|
|
14029
|
+
/**
|
|
14030
|
+
* Everything up to here counts as reported — by a real `update_progress`
|
|
14031
|
+
* (to the CURRENT position) or by the fallback itself (to the entry it
|
|
14032
|
+
* emitted). A candidate at/before this line is stale.
|
|
14033
|
+
*/
|
|
14034
|
+
watermark = { turn: 0, chars: 0 };
|
|
14035
|
+
constructor(opts) {
|
|
14036
|
+
this.canFire = opts.canFire;
|
|
14037
|
+
this.emit = opts.emit;
|
|
14038
|
+
this.idleMs = opts.idleMs ?? PROGRESS_FALLBACK_MS;
|
|
14039
|
+
this.maxChars = opts.maxChars ?? MAX_PROGRESS_CHARS;
|
|
14040
|
+
this.now = opts.now ?? (() => Date.now());
|
|
14041
|
+
}
|
|
14042
|
+
setIdleMs(ms) {
|
|
14043
|
+
this.idleMs = ms > 0 ? ms : PROGRESS_FALLBACK_MS;
|
|
14044
|
+
}
|
|
14045
|
+
/** Task start: fresh state and a fresh idle window. */
|
|
14046
|
+
start() {
|
|
14047
|
+
this.stop();
|
|
14048
|
+
this.reply = null;
|
|
14049
|
+
this.watermark = { turn: 0, chars: 0 };
|
|
14050
|
+
this.lastProgressAt = this.now();
|
|
14051
|
+
this.arm();
|
|
14052
|
+
}
|
|
14053
|
+
stop() {
|
|
14054
|
+
if (this.timer) {
|
|
14055
|
+
clearTimeout(this.timer);
|
|
14056
|
+
this.timer = null;
|
|
14057
|
+
}
|
|
14058
|
+
}
|
|
14059
|
+
/**
|
|
14060
|
+
* A real update_progress resets the idle window and advances the watermark
|
|
14061
|
+
* to the current position: everything produced so far counts as reported,
|
|
14062
|
+
* so a later window must not re-announce it. The MCP channel carries no
|
|
14063
|
+
* message id and takes no part in any dedupe.
|
|
14064
|
+
*/
|
|
14065
|
+
noteProgress(position) {
|
|
14066
|
+
if (isAfter(position, this.watermark)) this.watermark = position;
|
|
14067
|
+
this.lastProgressAt = this.now();
|
|
14068
|
+
this.arm();
|
|
14069
|
+
}
|
|
14070
|
+
/** New prompt / turn start: new idle window. Emits nothing by itself. */
|
|
14071
|
+
noteNewMessage() {
|
|
14072
|
+
this.lastProgressAt = this.now();
|
|
14073
|
+
this.arm();
|
|
14074
|
+
}
|
|
14075
|
+
/** Turn end resets the idle window. Emits nothing by itself. */
|
|
14076
|
+
noteTurnEnd() {
|
|
14077
|
+
this.lastProgressAt = this.now();
|
|
14078
|
+
this.arm();
|
|
14079
|
+
}
|
|
14080
|
+
/**
|
|
14081
|
+
* One complete entry flushed to the transcript (a tool call closed the open
|
|
14082
|
+
* segment, or the turn ended). The text IS the writer's own aggregation —
|
|
14083
|
+
* this tracker never joins chunks and never keeps more than the latest
|
|
14084
|
+
* entry. Sealing never emits anything.
|
|
14085
|
+
*/
|
|
14086
|
+
noteReplySealed(text, position) {
|
|
14087
|
+
this.reply = { text, position };
|
|
14088
|
+
}
|
|
14089
|
+
/**
|
|
14090
|
+
* Report the latest complete, never-reported entry NOW (turn end on the
|
|
14091
|
+
* degraded path — no need to wait out the idle window). Verbatim, capped at
|
|
14092
|
+
* maxChars; never a join, never a summary. Returns whether anything was
|
|
14093
|
+
* emitted; either way the idle window restarts.
|
|
14094
|
+
*/
|
|
14095
|
+
flush() {
|
|
14096
|
+
if (!this.canFire()) return false;
|
|
14097
|
+
const reply = this.reply;
|
|
14098
|
+
this.lastProgressAt = this.now();
|
|
14099
|
+
if (!reply || !isAfter(reply.position, this.watermark)) {
|
|
14100
|
+
this.arm();
|
|
14101
|
+
return false;
|
|
14102
|
+
}
|
|
14103
|
+
this.watermark = reply.position;
|
|
14104
|
+
this.emit(capText(reply.text, this.maxChars), "fallback");
|
|
14105
|
+
this.arm();
|
|
14106
|
+
return true;
|
|
14107
|
+
}
|
|
14108
|
+
arm() {
|
|
14109
|
+
this.stop();
|
|
14110
|
+
if (!this.canFire()) return;
|
|
14111
|
+
const remaining = Math.max(0, this.idleMs - (this.now() - this.lastProgressAt));
|
|
14112
|
+
this.timer = setTimeout(() => this.fire(), remaining);
|
|
14113
|
+
this.timer.unref?.();
|
|
14114
|
+
}
|
|
14115
|
+
fire() {
|
|
14116
|
+
this.timer = null;
|
|
14117
|
+
if (!this.canFire()) return;
|
|
14118
|
+
if (this.now() - this.lastProgressAt < this.idleMs) {
|
|
14119
|
+
this.arm();
|
|
14120
|
+
return;
|
|
14121
|
+
}
|
|
14122
|
+
this.flush();
|
|
14123
|
+
}
|
|
14124
|
+
};
|
|
14125
|
+
|
|
14126
|
+
// src/services/agents/acp/acp-progress.ts
|
|
14127
|
+
var PROGRESS_FILLER_RE = /[\s\p{P}]/gu;
|
|
14128
|
+
function hasProgressSubstance(text) {
|
|
14129
|
+
return text.replace(PROGRESS_FILLER_RE, "").length > 0;
|
|
14130
|
+
}
|
|
14131
|
+
var AcpProgress = class {
|
|
14132
|
+
tracker;
|
|
14133
|
+
deps;
|
|
14134
|
+
constructor(deps) {
|
|
14135
|
+
this.deps = deps;
|
|
14136
|
+
this.tracker = new McpProgressTracker({
|
|
14137
|
+
canFire: deps.canFire,
|
|
14138
|
+
emit: (message) => deps.onProgress(message),
|
|
14139
|
+
...deps.idleMs !== void 0 && { idleMs: deps.idleMs }
|
|
14140
|
+
});
|
|
14141
|
+
}
|
|
14142
|
+
/** Task start: fresh tracker state and a fresh idle window. */
|
|
14143
|
+
start() {
|
|
14144
|
+
this.tracker.start();
|
|
14145
|
+
}
|
|
14146
|
+
/** Run finally / abort / loop exit: never report after this. */
|
|
14147
|
+
stop() {
|
|
14148
|
+
this.tracker.stop();
|
|
14149
|
+
}
|
|
14150
|
+
/** New prompt / turn start: new idle window. */
|
|
14151
|
+
noteNewMessage() {
|
|
14152
|
+
this.tracker.noteNewMessage();
|
|
14153
|
+
}
|
|
14154
|
+
/** Turn end resets the idle window. */
|
|
14155
|
+
noteTurnEnd() {
|
|
14156
|
+
this.tracker.noteTurnEnd();
|
|
14157
|
+
}
|
|
14158
|
+
/**
|
|
14159
|
+
* Single choke point for a real `update_progress` (the MCP tool layer calls
|
|
14160
|
+
* this). Guards FIRST — a late tool call must not re-arm the idle timer
|
|
14161
|
+
* either — then advance the watermark to the current position: everything
|
|
14162
|
+
* produced so far counts as announced.
|
|
14163
|
+
*/
|
|
14164
|
+
report(message) {
|
|
14165
|
+
if (!this.deps.canFire()) return;
|
|
14166
|
+
this.tracker.noteProgress(this.deps.position());
|
|
14167
|
+
this.deps.onProgress(capText(message));
|
|
14168
|
+
}
|
|
14169
|
+
/**
|
|
14170
|
+
* One complete entry flushed to the transcript (a tool call closed the open
|
|
14171
|
+
* segment, or the turn ended and finishTurn wrote it): hand it to the
|
|
14172
|
+
* tracker as the fallback candidate. Recording never emits anything.
|
|
14173
|
+
*/
|
|
14174
|
+
recordReply() {
|
|
14175
|
+
if (!this.deps.canFire()) return;
|
|
14176
|
+
const text = this.deps.writer.lastText();
|
|
14177
|
+
if (text === null || text.length === 0) return;
|
|
14178
|
+
if (!hasProgressSubstance(text)) return;
|
|
14179
|
+
this.tracker.noteReplySealed(text, this.deps.position());
|
|
14180
|
+
}
|
|
14181
|
+
/**
|
|
14182
|
+
* Turn-end catch-up on the degraded path: report the latest complete,
|
|
14183
|
+
* never-reported entry NOW instead of waiting out the idle window.
|
|
14184
|
+
*/
|
|
14185
|
+
flush() {
|
|
14186
|
+
return this.tracker.flush();
|
|
14187
|
+
}
|
|
14188
|
+
};
|
|
14189
|
+
|
|
14190
|
+
// src/services/agents/acp/mcp-tools.ts
|
|
14191
|
+
var TIMED_OUT = /* @__PURE__ */ Symbol("wayang-mcp-tool-timeout");
|
|
14192
|
+
var ABORTED = /* @__PURE__ */ Symbol("wayang-mcp-tool-aborted");
|
|
14193
|
+
function withTimeout(work, ms, pending) {
|
|
14194
|
+
return new Promise((resolve15, reject) => {
|
|
14195
|
+
let done = false;
|
|
14196
|
+
const finish = (value) => {
|
|
14197
|
+
if (done) return;
|
|
14198
|
+
done = true;
|
|
14199
|
+
clearTimeout(timer);
|
|
14200
|
+
pending.delete(cancel);
|
|
14201
|
+
resolve15(value);
|
|
14202
|
+
};
|
|
14203
|
+
const fail = (err) => {
|
|
14204
|
+
if (done) return;
|
|
14205
|
+
done = true;
|
|
14206
|
+
clearTimeout(timer);
|
|
14207
|
+
pending.delete(cancel);
|
|
14208
|
+
reject(err);
|
|
14209
|
+
};
|
|
14210
|
+
const timer = setTimeout(() => finish(TIMED_OUT), ms);
|
|
14211
|
+
timer.unref?.();
|
|
14212
|
+
const cancel = () => finish(ABORTED);
|
|
14213
|
+
pending.add(cancel);
|
|
14214
|
+
work.then(finish, fail);
|
|
14215
|
+
});
|
|
14216
|
+
}
|
|
14217
|
+
function createWayangMcpToolInvoker(deps) {
|
|
14218
|
+
const pending = /* @__PURE__ */ new Set();
|
|
14219
|
+
const invoke = async (name, args) => {
|
|
14220
|
+
const started = Date.now();
|
|
14221
|
+
if (name === "update_progress") {
|
|
14222
|
+
if (typeof args.message !== "string" || args.message.length === 0) {
|
|
14223
|
+
return { text: '[MCP ERROR] update_progress requires a non-empty string "message"', isError: true };
|
|
14224
|
+
}
|
|
14225
|
+
const message = capText(args.message);
|
|
14226
|
+
const settled = await withTimeout(
|
|
14227
|
+
Promise.resolve().then(() => deps.onProgress(message)),
|
|
14228
|
+
deps.progressTimeoutMs ?? MCP_PROGRESS_TIMEOUT_MS,
|
|
14229
|
+
pending
|
|
14230
|
+
);
|
|
14231
|
+
if (settled === ABORTED) return { text: "[ABORTED] Worker is shutting down.", isError: true };
|
|
14232
|
+
if (settled === TIMED_OUT) {
|
|
14233
|
+
return { text: "[TIMEOUT] The progress report was not delivered within 5s.", isError: true };
|
|
14234
|
+
}
|
|
14235
|
+
deps.logger.debug(
|
|
14236
|
+
{ workerId: deps.workerId, toolName: name, charLen: message.length, durationMs: Date.now() - started, mcpAvailable: true, reason: "progress" },
|
|
14237
|
+
"ACP MCP tool served"
|
|
14238
|
+
);
|
|
14239
|
+
return { text: `Progress reported: ${message}` };
|
|
14240
|
+
}
|
|
14241
|
+
if (name === "ask_controller") {
|
|
14242
|
+
const question = capText(typeof args.question === "string" ? args.question : "", MAX_QUESTION_CHARS);
|
|
14243
|
+
if (!question) return { text: '[MCP ERROR] ask_controller requires a non-empty string "question"', isError: true };
|
|
14244
|
+
const context = args.context === void 0 ? void 0 : capText(String(args.context), MAX_CONTEXT_CHARS);
|
|
14245
|
+
const settled = await withTimeout(
|
|
14246
|
+
deps.ask(question, context),
|
|
14247
|
+
deps.askTimeoutMs ?? MCP_ASK_TIMEOUT_MS,
|
|
14248
|
+
pending
|
|
14249
|
+
);
|
|
14250
|
+
const answer = settled === TIMED_OUT ? "[TIMEOUT] No answer from the controller within 5 minutes. Use your best judgment to continue." : settled === ABORTED ? "[ABORTED] Worker is shutting down." : settled;
|
|
14251
|
+
deps.logger.debug(
|
|
14252
|
+
{ workerId: deps.workerId, toolName: name, charLen: answer.length, durationMs: Date.now() - started, mcpAvailable: true, reason: "ask" },
|
|
14253
|
+
"ACP MCP tool served"
|
|
14254
|
+
);
|
|
14255
|
+
return { text: answer, isError: settled === TIMED_OUT || settled === ABORTED };
|
|
14256
|
+
}
|
|
14257
|
+
return { text: `Unknown tool: ${name}`, isError: true };
|
|
14258
|
+
};
|
|
14259
|
+
return {
|
|
14260
|
+
invoke,
|
|
14261
|
+
settlePending: () => {
|
|
14262
|
+
for (const cancel of [...pending]) cancel();
|
|
14263
|
+
pending.clear();
|
|
14264
|
+
}
|
|
14265
|
+
};
|
|
14266
|
+
}
|
|
14267
|
+
|
|
14268
|
+
// src/services/agents/acp/acp-mcp-host.ts
|
|
14269
|
+
var AcpMcpHost = class {
|
|
14270
|
+
deps;
|
|
14271
|
+
transport = null;
|
|
14272
|
+
handshakeTimer = null;
|
|
14273
|
+
available = false;
|
|
14274
|
+
warned = false;
|
|
14275
|
+
/** Set by close(): a watchdog already on the timer queue must stay inert. */
|
|
14276
|
+
stopped = false;
|
|
14277
|
+
constructor(deps) {
|
|
14278
|
+
this.deps = deps;
|
|
14279
|
+
}
|
|
14280
|
+
/** Whether the injected server is live this run (false once degraded). */
|
|
14281
|
+
get mcpAvailable() {
|
|
14282
|
+
return this.available;
|
|
14283
|
+
}
|
|
14284
|
+
/**
|
|
14285
|
+
* Spawn the bridge transport (unless disabled) and return the mcpServers
|
|
14286
|
+
* descriptor for session/new. A failed start degrades immediately and
|
|
14287
|
+
* returns [] — session/new must still happen.
|
|
14288
|
+
*/
|
|
14289
|
+
async prepare() {
|
|
14290
|
+
if (!this.deps.enabled()) return [];
|
|
14291
|
+
const invoker = createWayangMcpToolInvoker({
|
|
14292
|
+
workerId: this.deps.workerId,
|
|
14293
|
+
logger: this.deps.logger,
|
|
14294
|
+
onProgress: (message) => this.deps.onProgress(message),
|
|
14295
|
+
ask: (question, context) => this.deps.ask(question, context)
|
|
14296
|
+
});
|
|
14297
|
+
try {
|
|
14298
|
+
const transport = await this.deps.spawnTransport({
|
|
14299
|
+
sessionDir: this.deps.sessionDir,
|
|
14300
|
+
workerId: this.deps.workerId,
|
|
14301
|
+
invoker,
|
|
14302
|
+
logger: this.deps.logger,
|
|
14303
|
+
serverInfo: { name: "wayang", version: package_default.version }
|
|
14304
|
+
});
|
|
14305
|
+
this.transport = transport;
|
|
14306
|
+
this.available = true;
|
|
14307
|
+
this.armHandshakeWatchdog(transport);
|
|
14308
|
+
this.deps.recordRuntime({ mcpAvailable: true });
|
|
14309
|
+
this.deps.logger.debug({ workerId: this.deps.workerId, reason: "mcp-injected" }, "ACP MCP server injected");
|
|
14310
|
+
return [transport.descriptor];
|
|
14311
|
+
} catch (err) {
|
|
14312
|
+
this.warnDegraded("transport-start-failed", err);
|
|
14313
|
+
return [];
|
|
14314
|
+
}
|
|
14315
|
+
}
|
|
14316
|
+
/**
|
|
14317
|
+
* Disable MCP, stop the watchdog and shut the transport (and thus the
|
|
14318
|
+
* bridge) down. The idle fallback stays armed: after degradation
|
|
14319
|
+
* session/update text is the only progress source.
|
|
14320
|
+
*/
|
|
14321
|
+
async degrade(reason, err) {
|
|
14322
|
+
if (this.stopped) return;
|
|
14323
|
+
if (this.available) {
|
|
14324
|
+
this.available = false;
|
|
14325
|
+
this.deps.onDegraded();
|
|
14326
|
+
this.deps.recordRuntime({ mcpAvailable: false });
|
|
14327
|
+
}
|
|
14328
|
+
await this.close();
|
|
14329
|
+
this.warnDegraded(reason, err);
|
|
14330
|
+
}
|
|
14331
|
+
/** Close the transport (idempotent) and stop the handshake watchdog. */
|
|
14332
|
+
async close() {
|
|
14333
|
+
this.stopped = true;
|
|
14334
|
+
this.stopWatchdog();
|
|
14335
|
+
const transport = this.transport;
|
|
14336
|
+
this.transport = null;
|
|
14337
|
+
if (!transport) return;
|
|
14338
|
+
try {
|
|
14339
|
+
await transport.close();
|
|
14340
|
+
} catch (err) {
|
|
14341
|
+
this.deps.logger.debug({ workerId: this.deps.workerId, err, reason: "mcp-transport-close" }, "ACP MCP transport close failed");
|
|
14342
|
+
}
|
|
14343
|
+
}
|
|
14344
|
+
/** Stop the handshake watchdog (run finally / abort path). */
|
|
14345
|
+
stopWatchdog() {
|
|
14346
|
+
if (this.handshakeTimer) {
|
|
14347
|
+
clearTimeout(this.handshakeTimer);
|
|
14348
|
+
this.handshakeTimer = null;
|
|
14349
|
+
}
|
|
14350
|
+
}
|
|
14351
|
+
/** A session/new that succeeded with injection but no hello within 10s degrades. */
|
|
14352
|
+
armHandshakeWatchdog(transport) {
|
|
14353
|
+
this.stopWatchdog();
|
|
14354
|
+
const timer = setTimeout(() => {
|
|
14355
|
+
this.handshakeTimer = null;
|
|
14356
|
+
if (this.stopped || !this.available || transport.isReady()) return;
|
|
14357
|
+
void this.degrade("handshake-timeout");
|
|
14358
|
+
}, MCP_HANDSHAKE_TIMEOUT_MS);
|
|
14359
|
+
timer.unref?.();
|
|
14360
|
+
this.handshakeTimer = timer;
|
|
14361
|
+
}
|
|
14362
|
+
/** Metadata-only warning: no token, path, question, answer or reply text. */
|
|
14363
|
+
warnDegraded(reason, err) {
|
|
14364
|
+
if (this.warned) return;
|
|
14365
|
+
this.warned = true;
|
|
14366
|
+
const rpcCode = err instanceof AcpError ? err.detail?.rpc?.code : void 0;
|
|
14367
|
+
this.deps.logger.warn(
|
|
14368
|
+
{ provider: this.deps.provider, workerId: this.deps.workerId, reason, ...rpcCode !== void 0 && { rpcCode }, mcpAvailable: false },
|
|
14369
|
+
"ACP MCP injection degraded \u2014 session/update progress is the fallback"
|
|
14370
|
+
);
|
|
14371
|
+
}
|
|
14372
|
+
};
|
|
14373
|
+
|
|
14374
|
+
// src/services/agents/acp/acp-session-open.ts
|
|
14375
|
+
async function trySessionNew(client, cwd, mcpServers, setAuthHit) {
|
|
14376
|
+
const authHit = new Promise((_, reject) => {
|
|
14377
|
+
setAuthHit((line) => reject(new AcpError("auth-required", `auth keyword on stderr during session/new: ${line}`, { stderrTail: [line] })));
|
|
14378
|
+
});
|
|
14379
|
+
authHit.catch(() => void 0);
|
|
14380
|
+
try {
|
|
14381
|
+
const { sessionId } = await Promise.race([
|
|
14382
|
+
client.sessionNew({ cwd, mcpServers }),
|
|
14383
|
+
authHit
|
|
14384
|
+
]);
|
|
14385
|
+
return sessionId;
|
|
14386
|
+
} finally {
|
|
14387
|
+
setAuthHit(null);
|
|
14388
|
+
}
|
|
14389
|
+
}
|
|
14390
|
+
function mcpRetryable(err, deps) {
|
|
14391
|
+
const readiness = deps.isReady({
|
|
14392
|
+
handshake: deps.handshake,
|
|
14393
|
+
phase: "session-new",
|
|
14394
|
+
error: err,
|
|
14395
|
+
stderrTail: deps.transport.stderrTail()
|
|
14396
|
+
});
|
|
14397
|
+
if (!readiness.ready) return false;
|
|
14398
|
+
if (err instanceof AcpError && ["auth-required", "cancelled", "transport-closed"].includes(err.code)) return false;
|
|
14399
|
+
return true;
|
|
14400
|
+
}
|
|
14401
|
+
function attributeSessionNewError(err, deps) {
|
|
14402
|
+
const readiness = deps.isReady({
|
|
14403
|
+
handshake: deps.handshake,
|
|
14404
|
+
phase: "session-new",
|
|
14405
|
+
error: err,
|
|
14406
|
+
stderrTail: deps.transport.stderrTail()
|
|
14407
|
+
});
|
|
14408
|
+
if (!readiness.ready) {
|
|
14409
|
+
throw new AcpError(readiness.kind ?? "agent-error", readiness.message ?? "agent not ready after session/new");
|
|
14410
|
+
}
|
|
14411
|
+
throw deps.normalizeError(err, "session-new");
|
|
14412
|
+
}
|
|
14413
|
+
async function openAcpSession(deps) {
|
|
14414
|
+
try {
|
|
14415
|
+
return await trySessionNew(deps.client, deps.cwd, deps.mcpServers, deps.setAuthHit);
|
|
14416
|
+
} catch (firstError) {
|
|
14417
|
+
if (deps.mcpServers.length > 0 && mcpRetryable(firstError, deps)) {
|
|
14418
|
+
await deps.degrade("session-new-mcp-error", firstError);
|
|
14419
|
+
try {
|
|
14420
|
+
return await trySessionNew(deps.client, deps.cwd, [], deps.setAuthHit);
|
|
14421
|
+
} catch (secondError) {
|
|
14422
|
+
attributeSessionNewError(secondError, deps);
|
|
14423
|
+
}
|
|
14424
|
+
}
|
|
14425
|
+
attributeSessionNewError(firstError, deps);
|
|
14426
|
+
}
|
|
14427
|
+
}
|
|
14428
|
+
|
|
13673
14429
|
// src/services/agents/acp/acp-worker-run.ts
|
|
13674
|
-
var PROGRESS_DEBOUNCE_MS = 500;
|
|
13675
14430
|
var AcpWorkerRun = class extends BaseAcpWorker {
|
|
13676
14431
|
/** Per-turn work counters feeding the guard's progressSig (#56). */
|
|
13677
14432
|
_turnTextChars = 0;
|
|
13678
14433
|
_turnToolCalls = 0;
|
|
13679
14434
|
_turnToolResults = 0;
|
|
14435
|
+
/** Turn counter: the turn component of the progress position. */
|
|
14436
|
+
_turnSeq = 0;
|
|
14437
|
+
/** Progress wiring (update_progress primary, idle fallback net). */
|
|
14438
|
+
_progress = null;
|
|
14439
|
+
/** Per-run MCP host lifecycle (injection / watchdog / degradation). */
|
|
14440
|
+
_mcpHost = null;
|
|
13680
14441
|
async run(task, _tools, onProgress) {
|
|
13681
14442
|
this._terminalResult = null;
|
|
13682
14443
|
this._streamEnded = false;
|
|
@@ -13692,6 +14453,18 @@ var AcpWorkerRun = class extends BaseAcpWorker {
|
|
|
13692
14453
|
this._turnGuard = resetTurnGuard(Date.now());
|
|
13693
14454
|
this._accumulator = createAcpToolAccumulator();
|
|
13694
14455
|
this._turnWriter.reset();
|
|
14456
|
+
this._onProgress = onProgress ?? null;
|
|
14457
|
+
this._turnSeq = 0;
|
|
14458
|
+
this._progress = new AcpProgress({
|
|
14459
|
+
canFire: () => this.progressGuardsOpen(),
|
|
14460
|
+
onProgress: (message) => {
|
|
14461
|
+
this._onProgress?.(message);
|
|
14462
|
+
},
|
|
14463
|
+
writer: this._turnWriter,
|
|
14464
|
+
position: () => this.progressPosition(),
|
|
14465
|
+
idleMs: this.acpUserConfig().mcp?.fallbackMs
|
|
14466
|
+
});
|
|
14467
|
+
this._progress.start();
|
|
13695
14468
|
this.state.set("runtimeState.task", { id: task.id, description: task.description });
|
|
13696
14469
|
const turnEnv = this.buildTurnEnv(task);
|
|
13697
14470
|
this._currentTurnEnv = turnEnv;
|
|
@@ -13746,7 +14519,32 @@ var AcpWorkerRun = class extends BaseAcpWorker {
|
|
|
13746
14519
|
if (!postHandshake.ready) {
|
|
13747
14520
|
throw new AcpError(postHandshake.kind ?? "agent-error", postHandshake.message ?? "agent not ready");
|
|
13748
14521
|
}
|
|
13749
|
-
|
|
14522
|
+
this._mcpHost = new AcpMcpHost({
|
|
14523
|
+
workerId: this.id,
|
|
14524
|
+
provider: this.config.type,
|
|
14525
|
+
sessionDir: this.sessionDir,
|
|
14526
|
+
logger: this.logger,
|
|
14527
|
+
enabled: () => this.acpUserConfig().mcp?.enabled !== false,
|
|
14528
|
+
spawnTransport: (opts) => this.createMcpTransport(opts),
|
|
14529
|
+
onProgress: (message) => this._progress.report(message),
|
|
14530
|
+
ask: (question, context) => this.mcpAsk(question, context),
|
|
14531
|
+
recordRuntime: (fields) => this.recordAcpRuntime(fields),
|
|
14532
|
+
onDegraded: () => this._progress?.noteNewMessage()
|
|
14533
|
+
});
|
|
14534
|
+
const mcpServers = await this._mcpHost.prepare();
|
|
14535
|
+
const sessionId = await openAcpSession({
|
|
14536
|
+
client,
|
|
14537
|
+
handshake,
|
|
14538
|
+
transport,
|
|
14539
|
+
cwd: this.workspaceDir,
|
|
14540
|
+
mcpServers,
|
|
14541
|
+
isReady: (ctx) => this.isReady(ctx),
|
|
14542
|
+
normalizeError: (err, phase) => this.normalizeError(err, phase),
|
|
14543
|
+
degrade: (reason, err) => this._mcpHost.degrade(reason, err),
|
|
14544
|
+
setAuthHit: (handler) => {
|
|
14545
|
+
this._sessionAuthHit = handler;
|
|
14546
|
+
}
|
|
14547
|
+
});
|
|
13750
14548
|
this._sessionId = sessionId;
|
|
13751
14549
|
this.recordAcpRuntime({ pid: transport.pid, command: launch.command, provider: this.config.type });
|
|
13752
14550
|
const unregisterPermission = client.onPermissionRequest(async (req) => {
|
|
@@ -13786,7 +14584,7 @@ var AcpWorkerRun = class extends BaseAcpWorker {
|
|
|
13786
14584
|
};
|
|
13787
14585
|
return this.mapPermission(req, acpCtx);
|
|
13788
14586
|
});
|
|
13789
|
-
const unregisterUpdates = client.onUpdate((update) => this.handleSessionUpdate(update
|
|
14587
|
+
const unregisterUpdates = client.onUpdate((update) => this.handleSessionUpdate(update));
|
|
13790
14588
|
try {
|
|
13791
14589
|
let promptText = `${buildThirdPartyPrompt(this.skills)}${task.description}`;
|
|
13792
14590
|
const initialQueued = this._messageQueue.splice(0, this._messageQueue.length);
|
|
@@ -13796,9 +14594,13 @@ ${joinControllerMessages(initialQueued)}`;
|
|
|
13796
14594
|
while (!this.abortController.signal.aborted) {
|
|
13797
14595
|
const cancelBaseline = client.cancelSentCount;
|
|
13798
14596
|
this.resetTurnProgress();
|
|
14597
|
+
this._turnSeq += 1;
|
|
14598
|
+
this._progress.noteNewMessage();
|
|
13799
14599
|
const result2 = await client.sessionPrompt(sessionId, [{ type: "text", text: promptText }]);
|
|
13800
14600
|
this._resultReceived = true;
|
|
13801
14601
|
this.settleTurn();
|
|
14602
|
+
this._progress.recordReply();
|
|
14603
|
+
this._progress.noteTurnEnd();
|
|
13802
14604
|
const denials = this.takeTurnDenials();
|
|
13803
14605
|
const cancelFact = client.cancelSentCount > cancelBaseline ? client.lastCancelReason : null;
|
|
13804
14606
|
const hasQueuedMessages = this._messageQueue.length > 0;
|
|
@@ -13821,6 +14623,7 @@ ${joinControllerMessages(initialQueued)}`;
|
|
|
13821
14623
|
);
|
|
13822
14624
|
break;
|
|
13823
14625
|
}
|
|
14626
|
+
if (!this._mcpHost.mcpAvailable) this._progress.flush();
|
|
13824
14627
|
const queued = this._messageQueue.splice(0, this._messageQueue.length);
|
|
13825
14628
|
if (verdict.action === "reissue") {
|
|
13826
14629
|
this._turnGuard.denialReissues += 1;
|
|
@@ -13832,6 +14635,7 @@ ${joinControllerMessages(initialQueued)}`;
|
|
|
13832
14635
|
this.state.append("conversation", applyTurnEnvelope(makeInjectedEntry(promptText, injectedKind, this.ctx.sessionId), this._currentTurnEnv ?? void 0));
|
|
13833
14636
|
}
|
|
13834
14637
|
} finally {
|
|
14638
|
+
this._progress?.stop();
|
|
13835
14639
|
unregisterUpdates();
|
|
13836
14640
|
unregisterPermission();
|
|
13837
14641
|
}
|
|
@@ -13848,6 +14652,7 @@ ${joinControllerMessages(initialQueued)}`;
|
|
|
13848
14652
|
} finally {
|
|
13849
14653
|
this._streamEnded = true;
|
|
13850
14654
|
this.clearPendingProgress();
|
|
14655
|
+
await this._mcpHost?.close();
|
|
13851
14656
|
if (this._cancelGraceTimer) {
|
|
13852
14657
|
clearTimeout(this._cancelGraceTimer);
|
|
13853
14658
|
this._cancelGraceTimer = null;
|
|
@@ -13882,6 +14687,7 @@ ${joinControllerMessages(initialQueued)}`;
|
|
|
13882
14687
|
abort() {
|
|
13883
14688
|
this.abortController.abort();
|
|
13884
14689
|
this.clearPendingProgress();
|
|
14690
|
+
void this._mcpHost?.close();
|
|
13885
14691
|
this.permissionHandler.cleanup();
|
|
13886
14692
|
const client = this._client;
|
|
13887
14693
|
if (!client || !this._sessionId || this._terminalResult) return;
|
|
@@ -13899,31 +14705,50 @@ ${joinControllerMessages(initialQueued)}`;
|
|
|
13899
14705
|
}, policy.cancelGraceMs);
|
|
13900
14706
|
}
|
|
13901
14707
|
// --- Internals ---
|
|
13902
|
-
/**
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
13906
|
-
|
|
13907
|
-
|
|
13908
|
-
|
|
13909
|
-
|
|
13910
|
-
|
|
13911
|
-
|
|
14708
|
+
/** Clear progress state: stop the idle tracker and the MCP handshake watchdog. */
|
|
14709
|
+
clearPendingProgress() {
|
|
14710
|
+
this._progress?.stop();
|
|
14711
|
+
this._mcpHost?.stopWatchdog();
|
|
14712
|
+
}
|
|
14713
|
+
/** The four guards: no progress after turn end / cancel / abort / agent exit. */
|
|
14714
|
+
progressGuardsOpen() {
|
|
14715
|
+
return this._runInFlight && !this._inputClosed && !this.abortController.signal.aborted && !this._streamEnded && this._terminalResult === null && this._cancelledAt === null;
|
|
14716
|
+
}
|
|
14717
|
+
/** Where the run currently is in its own text stream (turn + chars so far). */
|
|
14718
|
+
progressPosition() {
|
|
14719
|
+
return { turn: this._turnSeq, chars: this._turnTextChars };
|
|
14720
|
+
}
|
|
14721
|
+
/** ask_controller: same blocking engine channel as ClaudeCodeWorker, with an audit entry. */
|
|
14722
|
+
async mcpAsk(question, context) {
|
|
14723
|
+
const ask = this._askController;
|
|
14724
|
+
if (!ask || !this._mcpHost?.mcpAvailable) {
|
|
14725
|
+
return "[UNAVAILABLE] The controller inquiry channel is not available for this worker.";
|
|
14726
|
+
}
|
|
14727
|
+
const q = capText(question, MAX_QUESTION_CHARS);
|
|
14728
|
+
const c = context === void 0 ? void 0 : capText(context, MAX_CONTEXT_CHARS);
|
|
14729
|
+
let answer;
|
|
13912
14730
|
try {
|
|
13913
|
-
|
|
13914
|
-
client.sessionNew({ cwd: this.workspaceDir, mcpServers: [] }),
|
|
13915
|
-
authHit
|
|
13916
|
-
]);
|
|
13917
|
-
return sessionId;
|
|
14731
|
+
answer = await ask(q, c);
|
|
13918
14732
|
} catch (err) {
|
|
13919
|
-
|
|
13920
|
-
if (!readiness.ready) {
|
|
13921
|
-
throw new AcpError(readiness.kind ?? "agent-error", readiness.message ?? "agent not ready after session/new");
|
|
13922
|
-
}
|
|
13923
|
-
throw this.normalizeError(err, "session-new");
|
|
13924
|
-
} finally {
|
|
13925
|
-
this._sessionAuthHit = null;
|
|
14733
|
+
answer = `[UNAVAILABLE] ${err instanceof Error ? err.message : String(err)}`;
|
|
13926
14734
|
}
|
|
14735
|
+
this.recordInquiryAudit(q, answer);
|
|
14736
|
+
return answer;
|
|
14737
|
+
}
|
|
14738
|
+
recordInquiryAudit(question, answer) {
|
|
14739
|
+
const ts = nowISO();
|
|
14740
|
+
this.state.append("conversation", applyTurnEnvelope({
|
|
14741
|
+
type: "system" /* System */,
|
|
14742
|
+
uuid: generateId("s"),
|
|
14743
|
+
parentUuid: null,
|
|
14744
|
+
sessionId: this.ctx.sessionId,
|
|
14745
|
+
timestamp: ts,
|
|
14746
|
+
subtype: "inquiry" /* Inquiry */,
|
|
14747
|
+
content: `ask_controller: ${question}
|
|
14748
|
+
controller: ${answer}`,
|
|
14749
|
+
startedAt: ts,
|
|
14750
|
+
endedAt: ts
|
|
14751
|
+
}, this._currentTurnEnv ?? void 0));
|
|
13927
14752
|
}
|
|
13928
14753
|
handleStderrLine(line) {
|
|
13929
14754
|
this._stderrLines.push(line);
|
|
@@ -13934,7 +14759,7 @@ ${joinControllerMessages(initialQueued)}`;
|
|
|
13934
14759
|
}
|
|
13935
14760
|
}
|
|
13936
14761
|
/** Route one session/update through the mapper and persist/forward the result. */
|
|
13937
|
-
handleSessionUpdate(update
|
|
14762
|
+
handleSessionUpdate(update) {
|
|
13938
14763
|
if (this._cancelledAt !== null && !this.stopPolicy().trustUpdatesAfterCancel) {
|
|
13939
14764
|
this._discardedAfterCancel += 1;
|
|
13940
14765
|
this.logger.debug(
|
|
@@ -13952,17 +14777,16 @@ ${joinControllerMessages(initialQueued)}`;
|
|
|
13952
14777
|
}
|
|
13953
14778
|
for (const event of events) {
|
|
13954
14779
|
switch (event.kind) {
|
|
13955
|
-
case "agent-text":
|
|
14780
|
+
case "agent-text": {
|
|
13956
14781
|
this._turnWriter.addText(event.text);
|
|
13957
14782
|
this._turnTextChars += event.text.length;
|
|
13958
14783
|
break;
|
|
13959
|
-
|
|
13960
|
-
this.emitDebouncedProgress(event.text, onProgress);
|
|
13961
|
-
break;
|
|
14784
|
+
}
|
|
13962
14785
|
case "tool-call":
|
|
13963
14786
|
this._sawToolCall = true;
|
|
13964
14787
|
this._turnWriter.addToolCall(event.record);
|
|
13965
14788
|
this._turnToolCalls += 1;
|
|
14789
|
+
this._progress?.recordReply();
|
|
13966
14790
|
break;
|
|
13967
14791
|
case "tool-result":
|
|
13968
14792
|
this._turnWriter.addToolResult(event.record);
|
|
@@ -13992,22 +14816,6 @@ ${joinControllerMessages(initialQueued)}`;
|
|
|
13992
14816
|
}
|
|
13993
14817
|
this._turnWriter.finishTurn();
|
|
13994
14818
|
}
|
|
13995
|
-
/** Progress debounce (02 §5): 500ms, dropped once the run is over. */
|
|
13996
|
-
emitDebouncedProgress(text, onProgress) {
|
|
13997
|
-
if (!onProgress) return;
|
|
13998
|
-
this._pendingProgress = text.slice(0, 200);
|
|
13999
|
-
if (this._progressTimer) clearTimeout(this._progressTimer);
|
|
14000
|
-
this._progressTimer = setTimeout(() => {
|
|
14001
|
-
this._progressTimer = null;
|
|
14002
|
-
if (this._pendingProgress === null || this.abortController.signal.aborted || this._streamEnded || this._terminalResult !== null) {
|
|
14003
|
-
this._pendingProgress = null;
|
|
14004
|
-
return;
|
|
14005
|
-
}
|
|
14006
|
-
const pending = this._pendingProgress;
|
|
14007
|
-
this._pendingProgress = null;
|
|
14008
|
-
onProgress(pending);
|
|
14009
|
-
}, PROGRESS_DEBOUNCE_MS);
|
|
14010
|
-
}
|
|
14011
14819
|
/** Display-ready error text with the full context (frozen §2.5 acpUserMessage). */
|
|
14012
14820
|
userFacingError(err) {
|
|
14013
14821
|
return acpUserMessage(err.code, {
|
|
@@ -14052,6 +14860,15 @@ ${joinControllerMessages(initialQueued)}`;
|
|
|
14052
14860
|
createTransport(config) {
|
|
14053
14861
|
return new StdioAcpTransport(config);
|
|
14054
14862
|
}
|
|
14863
|
+
/**
|
|
14864
|
+
* MCP transport factory — the ONLY seam between the worker run and the MCP
|
|
14865
|
+
* plumbing. Swapping the stdio bridge for an ACP v2 `acp` tunnel means
|
|
14866
|
+
* replacing this one call (plus capability detection), not the tool layer.
|
|
14867
|
+
* Overridable in tests to inject a fake transport.
|
|
14868
|
+
*/
|
|
14869
|
+
createMcpTransport(opts) {
|
|
14870
|
+
return startStdioMcpTransport(opts);
|
|
14871
|
+
}
|
|
14055
14872
|
/** Capability observation snapshot for diagnostics/tests (02 §6). */
|
|
14056
14873
|
get observedCapabilities() {
|
|
14057
14874
|
return this.getCapabilities();
|
|
@@ -14236,13 +15053,13 @@ function formatApiCallError(err) {
|
|
|
14236
15053
|
}
|
|
14237
15054
|
|
|
14238
15055
|
// src/services/tools/add-task.ts
|
|
14239
|
-
import { mkdirSync as
|
|
15056
|
+
import { mkdirSync as mkdirSync9 } from "fs";
|
|
14240
15057
|
import * as path8 from "path";
|
|
14241
15058
|
import { z as z3 } from "zod";
|
|
14242
15059
|
|
|
14243
15060
|
// src/services/tools/workspace-rules.ts
|
|
14244
15061
|
import { readFileSync as readFileSync10, statSync as statSync6 } from "fs";
|
|
14245
|
-
import { join as
|
|
15062
|
+
import { join as join19 } from "path";
|
|
14246
15063
|
var RULE_FILE_NAMES2 = ["AGENTS.md", "CLAUDE.md"];
|
|
14247
15064
|
var WORKSPACE_RULES_MAX_LINES = 40;
|
|
14248
15065
|
var WORKSPACE_RULES_MAX_CHARS = 2e3;
|
|
@@ -14250,7 +15067,7 @@ function buildWorkspaceRulesSummary(input2) {
|
|
|
14250
15067
|
const workspacePath = input2.workspacePath;
|
|
14251
15068
|
if (!workspacePath) return "";
|
|
14252
15069
|
for (const name of RULE_FILE_NAMES2) {
|
|
14253
|
-
const rulesPath =
|
|
15070
|
+
const rulesPath = join19(workspacePath, name);
|
|
14254
15071
|
if (!isRegularFile(rulesPath)) continue;
|
|
14255
15072
|
const header = `Workspace rules (from ${rulesPath}):`;
|
|
14256
15073
|
const refusal = describeRefusal(rulesPath, workspacePath, input2);
|
|
@@ -14325,7 +15142,7 @@ function readTextFile(p) {
|
|
|
14325
15142
|
// src/services/tools/add-task.ts
|
|
14326
15143
|
function ensureWorkspaceDir(dirPath) {
|
|
14327
15144
|
try {
|
|
14328
|
-
|
|
15145
|
+
mkdirSync9(dirPath, { recursive: true });
|
|
14329
15146
|
return null;
|
|
14330
15147
|
} catch (err) {
|
|
14331
15148
|
return `Cannot create workspace ${dirPath}: ${err.message}. The task was NOT created.`;
|
|
@@ -15033,8 +15850,8 @@ function readFileTool(deps = {}) {
|
|
|
15033
15850
|
|
|
15034
15851
|
// src/services/tools/write-file.ts
|
|
15035
15852
|
import { z as z15 } from "zod";
|
|
15036
|
-
import { writeFileSync as writeFileSync5, mkdirSync as
|
|
15037
|
-
import { dirname as
|
|
15853
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync10, existsSync as existsSync12 } from "fs";
|
|
15854
|
+
import { dirname as dirname10, resolve as resolve9 } from "path";
|
|
15038
15855
|
function writeFileTool(deps = {}) {
|
|
15039
15856
|
const workspace = deps.cwd ?? process.cwd();
|
|
15040
15857
|
return defineTool({
|
|
@@ -15045,10 +15862,10 @@ function writeFileTool(deps = {}) {
|
|
|
15045
15862
|
}),
|
|
15046
15863
|
execute: safeExecute("write_file", async ({ path: path10, content }) => {
|
|
15047
15864
|
const resolved = resolve9(workspace, expandHome(path10).path);
|
|
15048
|
-
if (
|
|
15865
|
+
if (existsSync12(resolved)) {
|
|
15049
15866
|
return `[ERROR] write_file: File already exists: ${path10}. Use edit_file to modify existing files.`;
|
|
15050
15867
|
}
|
|
15051
|
-
|
|
15868
|
+
mkdirSync10(dirname10(resolved), { recursive: true });
|
|
15052
15869
|
writeFileSync5(resolved, content, "utf-8");
|
|
15053
15870
|
return `Written ${content.length} chars to ${resolved}`;
|
|
15054
15871
|
})
|
|
@@ -15503,12 +16320,12 @@ function askControllerTool(deps) {
|
|
|
15503
16320
|
import { resolve as resolve12 } from "path";
|
|
15504
16321
|
|
|
15505
16322
|
// src/services/task/task-pool-state.ts
|
|
15506
|
-
import { join as
|
|
16323
|
+
import { join as join20 } from "path";
|
|
15507
16324
|
var TaskPoolState = class extends BaseWayangState {
|
|
15508
16325
|
jsonFile;
|
|
15509
16326
|
logger;
|
|
15510
16327
|
constructor(ctx) {
|
|
15511
|
-
const jsonFile = new JSONFileHelper(
|
|
16328
|
+
const jsonFile = new JSONFileHelper(join20(ctx.sessionDir, "tasks.json"));
|
|
15512
16329
|
super(
|
|
15513
16330
|
{
|
|
15514
16331
|
tasks: {
|
|
@@ -16339,7 +17156,7 @@ var WorkerWiringEngine = class extends WorkerTrackingEngine {
|
|
|
16339
17156
|
// (accessScope) judges expanded paths with — no injection needed.
|
|
16340
17157
|
tavilyApiKey: this.ctx.config.tavilyApiKey,
|
|
16341
17158
|
registry: this.skills,
|
|
16342
|
-
reportProgress: (msg
|
|
17159
|
+
reportProgress: (msg) => {
|
|
16343
17160
|
const activity = this.getWorkerActivity(workerId);
|
|
16344
17161
|
this.enqueueWorkerSignal(workerId, {
|
|
16345
17162
|
source: "worker",
|
|
@@ -16871,7 +17688,7 @@ var ControllerLoop = class {
|
|
|
16871
17688
|
};
|
|
16872
17689
|
|
|
16873
17690
|
// src/services/agents/controller-state.ts
|
|
16874
|
-
import { join as
|
|
17691
|
+
import { join as join21 } from "path";
|
|
16875
17692
|
|
|
16876
17693
|
// src/services/conversation/conversation-index.ts
|
|
16877
17694
|
function emptyUsage() {
|
|
@@ -17095,8 +17912,8 @@ var ControllerAgentState = class extends BaseWayangState {
|
|
|
17095
17912
|
/** In-memory resolve function for the current inquiry — never persisted. */
|
|
17096
17913
|
inquiryResolver = null;
|
|
17097
17914
|
constructor(ctx) {
|
|
17098
|
-
const jsonFile = new JSONFileHelper(
|
|
17099
|
-
const jsonlFile = new JSONLFileHelper(
|
|
17915
|
+
const jsonFile = new JSONFileHelper(join21(ctx.sessionDir, "runtime-state.json"));
|
|
17916
|
+
const jsonlFile = new JSONLFileHelper(join21(ctx.sessionDir, "conversation.jsonl"));
|
|
17100
17917
|
const initialData = {
|
|
17101
17918
|
runtimeState: {
|
|
17102
17919
|
session: { id: "", startedAt: 0 },
|
|
@@ -18086,7 +18903,7 @@ var ControllerAgent = class _ControllerAgent extends ControllerTurnRun {
|
|
|
18086
18903
|
|
|
18087
18904
|
// src/services/skills/registry.ts
|
|
18088
18905
|
import { readFileSync as readFileSync13, readdirSync as readdirSync2, statSync as statSync7 } from "fs";
|
|
18089
|
-
import { join as
|
|
18906
|
+
import { join as join22 } from "path";
|
|
18090
18907
|
import { parse as parseYaml } from "yaml";
|
|
18091
18908
|
var SKILL_FILE = "SKILL.md";
|
|
18092
18909
|
function parseFrontmatter(raw) {
|
|
@@ -18121,7 +18938,7 @@ function listResources(skillDir) {
|
|
|
18121
18938
|
const walk = (relDir) => {
|
|
18122
18939
|
let entries;
|
|
18123
18940
|
try {
|
|
18124
|
-
entries = readdirSync2(
|
|
18941
|
+
entries = readdirSync2(join22(skillDir, relDir), { withFileTypes: true });
|
|
18125
18942
|
} catch {
|
|
18126
18943
|
return;
|
|
18127
18944
|
}
|
|
@@ -18168,7 +18985,7 @@ var SkillRegistry = class {
|
|
|
18168
18985
|
if (existing) return existing;
|
|
18169
18986
|
const desc = this.descriptors.get(name);
|
|
18170
18987
|
if (!desc) return void 0;
|
|
18171
|
-
const filePath =
|
|
18988
|
+
const filePath = join22(desc.dir, SKILL_FILE);
|
|
18172
18989
|
let raw;
|
|
18173
18990
|
try {
|
|
18174
18991
|
raw = readFileSync13(filePath, "utf-8");
|
|
@@ -18199,8 +19016,8 @@ var SkillRegistry = class {
|
|
|
18199
19016
|
for (const entry of entries) {
|
|
18200
19017
|
if (!entry.isDirectory()) continue;
|
|
18201
19018
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
18202
|
-
const skillDir =
|
|
18203
|
-
const skillFile =
|
|
19019
|
+
const skillDir = join22(dir, entry.name);
|
|
19020
|
+
const skillFile = join22(skillDir, SKILL_FILE);
|
|
18204
19021
|
if (!existsAsFile(skillFile)) continue;
|
|
18205
19022
|
const { frontmatter } = safeReadFrontmatter(skillFile);
|
|
18206
19023
|
const description = asString(frontmatter.description);
|
|
@@ -18229,12 +19046,12 @@ function safeReadFrontmatter(path10) {
|
|
|
18229
19046
|
}
|
|
18230
19047
|
|
|
18231
19048
|
// src/services/skills/skill-dir.ts
|
|
18232
|
-
import { existsSync as
|
|
18233
|
-
import { join as
|
|
19049
|
+
import { existsSync as existsSync13 } from "fs";
|
|
19050
|
+
import { join as join23, resolve as resolve13 } from "path";
|
|
18234
19051
|
function resolveSkillDirs(dataDir, workspaceDir2, configDirs) {
|
|
18235
19052
|
const candidates = [
|
|
18236
|
-
|
|
18237
|
-
|
|
19053
|
+
join23(dataDir, "skills"),
|
|
19054
|
+
join23(workspaceDir2, ".wayang", "skills"),
|
|
18238
19055
|
...(configDirs ?? []).map((d) => resolve13(d))
|
|
18239
19056
|
];
|
|
18240
19057
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -18242,14 +19059,14 @@ function resolveSkillDirs(dataDir, workspaceDir2, configDirs) {
|
|
|
18242
19059
|
for (const dir of candidates) {
|
|
18243
19060
|
if (seen.has(dir)) continue;
|
|
18244
19061
|
seen.add(dir);
|
|
18245
|
-
if (
|
|
19062
|
+
if (existsSync13(dir)) dirs.push(dir);
|
|
18246
19063
|
}
|
|
18247
19064
|
return dirs;
|
|
18248
19065
|
}
|
|
18249
19066
|
|
|
18250
19067
|
// src/services/supervisor.ts
|
|
18251
19068
|
import { homedir as homedir8 } from "os";
|
|
18252
|
-
import { join as
|
|
19069
|
+
import { join as join24 } from "path";
|
|
18253
19070
|
var Supervisor = class {
|
|
18254
19071
|
ctx;
|
|
18255
19072
|
signalQueue;
|
|
@@ -18298,7 +19115,7 @@ var Supervisor = class {
|
|
|
18298
19115
|
);
|
|
18299
19116
|
this.signalQueue = new SignalQueue(this.ctx);
|
|
18300
19117
|
this.skills = new SkillRegistry(
|
|
18301
|
-
resolveSkillDirs(options.dataDir ??
|
|
19118
|
+
resolveSkillDirs(options.dataDir ?? join24(homedir8(), ".wayang"), workspaceDir2, config.skillsDirs),
|
|
18302
19119
|
this.ctx.logger
|
|
18303
19120
|
);
|
|
18304
19121
|
this.engine = new TaskExecuteEngine(this.ctx, this.signalQueue, this.skills, {
|