agent-worker 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { I as getDefaultModel, j as FRONTIER_MODELS, k as normalizeBackendType, n as createBackend } from "../backends-CziIqKRg.mjs";
3
- import { t as AgentWorker } from "../worker-DBJ8136Q.mjs";
2
+ import { A as parseModel, I as getDefaultModel, L as isAutoProvider, P as createModelAsync, R as resolveModelFallback, a as createMockBackend, j as FRONTIER_MODELS, k as normalizeBackendType, n as createBackend } from "../backends-C7pQwuAx.mjs";
3
+ import { t as createTool } from "../create-tool-gcUuI1FD.mjs";
4
+ import { generateText, stepCountIs, tool } from "ai";
4
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
5
6
  import { dirname, isAbsolute, join, relative } from "node:path";
6
7
  import { appendFile, mkdir, open, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
8
+ import { stringify } from "yaml";
7
9
  import { z } from "zod";
8
10
  import { homedir } from "node:os";
9
- import { spawn } from "node:child_process";
11
+ import { execSync, spawn } from "node:child_process";
10
12
  import { Command, Option } from "commander";
11
13
  import { Hono } from "hono";
12
14
  import { streamSSE } from "hono/streaming";
@@ -14,6 +16,11 @@ import { randomUUID } from "node:crypto";
14
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
17
  import { nanoid } from "nanoid";
16
18
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
19
+ import { createServer } from "node:http";
20
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
21
+ import { MockLanguageModelV3, mockValues } from "ai/test";
22
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
23
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
17
24
 
18
25
  //#region rolldown:runtime
19
26
  var __defProp = Object.defineProperty;
@@ -77,40 +84,53 @@ function isDaemonRunning() {
77
84
  return null;
78
85
  }
79
86
  }
80
-
81
- //#endregion
82
- //#region src/agent/handle.ts
87
+ const DURATION_RE = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
83
88
  /**
84
- * LocalWorker In-process execution via AgentWorker.
85
- *
86
- * Wraps an AgentWorker instance. State lives in the AgentWorker's memory.
87
- * This is the default for single-machine deployments.
88
- */
89
- var LocalWorker = class {
90
- engine;
91
- constructor(config, restore) {
92
- const backend = config.backend !== "default" ? createBackend({
93
- type: config.backend,
94
- model: config.model
95
- }) : void 0;
96
- this.engine = new AgentWorker({
97
- model: config.model,
98
- system: config.system,
99
- tools: {},
100
- backend,
101
- provider: config.provider
102
- }, restore);
103
- }
104
- send(input, options) {
105
- return this.engine.send(input, options);
106
- }
107
- sendStream(input, options) {
108
- return this.engine.sendStream(input, options);
89
+ * Parse a duration string like "30s", "5m", "2h" into milliseconds.
90
+ * Returns null if not a valid duration format.
91
+ */
92
+ function parseDuration(value) {
93
+ const match = value.match(DURATION_RE);
94
+ if (!match) return null;
95
+ return parseFloat(match[1]) * {
96
+ ms: 1,
97
+ s: 1e3,
98
+ m: 60 * 1e3,
99
+ h: 3600 * 1e3,
100
+ d: 1440 * 60 * 1e3
101
+ }[match[2]];
102
+ }
103
+ /**
104
+ * Resolve a wakeup value into a typed schedule.
105
+ * - number → interval (ms)
106
+ * - "30s"/"5m"/"2h" → interval (converted to ms)
107
+ * - cron expression → cron
108
+ */
109
+ function resolveSchedule(config) {
110
+ const { wakeup, prompt } = config;
111
+ if (typeof wakeup === "number") {
112
+ if (wakeup <= 0) throw new Error("Wakeup interval must be positive");
113
+ return {
114
+ type: "interval",
115
+ ms: wakeup,
116
+ prompt
117
+ };
109
118
  }
110
- getState() {
111
- return this.engine.getState();
119
+ const ms = parseDuration(wakeup);
120
+ if (ms !== null) {
121
+ if (ms <= 0) throw new Error("Wakeup duration must be positive");
122
+ return {
123
+ type: "interval",
124
+ ms,
125
+ prompt
126
+ };
112
127
  }
113
- };
128
+ return {
129
+ type: "cron",
130
+ expr: wakeup,
131
+ prompt
132
+ };
133
+ }
114
134
 
115
135
  //#endregion
116
136
  //#region src/agent/store.ts
@@ -138,11 +158,11 @@ var MemoryStateStore = class {
138
158
  * Auto-detects runtime: Bun.serve() when available, @hono/node-server otherwise.
139
159
  */
140
160
  async function startHttpServer(app, options) {
141
- if (typeof globalThis.Bun !== "undefined") return startBun(app, options);
161
+ if ("Bun" in globalThis) return startBun(app, options);
142
162
  return startNode(app, options);
143
163
  }
144
164
  function startBun(app, options) {
145
- const server = Bun.serve({
165
+ const server = globalThis.Bun.serve({
146
166
  fetch: app.fetch,
147
167
  port: options.port,
148
168
  hostname: options.hostname
@@ -221,7 +241,7 @@ function getAgentId(extra) {
221
241
  /**
222
242
  * Format inbox messages for JSON display.
223
243
  */
224
- function formatInbox(messages) {
244
+ function formatInbox$1(messages) {
225
245
  if (messages.length === 0) return JSON.stringify({
226
246
  messages: [],
227
247
  count: 0
@@ -360,7 +380,7 @@ function registerInboxTools(server, ctx, options) {
360
380
  if (debugLog && messages.length > 0) debugLog(`[mcp:${agent}] my_inbox → ${messages.length} unread`);
361
381
  return { content: [{
362
382
  type: "text",
363
- text: formatInbox(messages)
383
+ text: formatInbox$1(messages)
364
384
  }] };
365
385
  });
366
386
  server.tool("my_inbox_ack", "Acknowledge inbox messages up to a message ID. Call after processing messages.", { until: z.string().describe("Acknowledge messages up to and including this message ID") }, async ({ until }, extra) => {
@@ -378,7 +398,7 @@ function registerInboxTools(server, ctx, options) {
378
398
  server.tool("my_status_set", "Update your status and current task. Call when starting or completing work.", {
379
399
  task: z.string().optional().describe("Current task description (what you're working on)"),
380
400
  state: z.enum(["idle", "running"]).optional().describe("Agent state (running = working, idle = available)"),
381
- metadata: z.record(z.unknown()).optional().describe("Additional metadata (e.g., PR number, file path)")
401
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Additional metadata (e.g., PR number, file path)")
382
402
  }, async (args, extra) => {
383
403
  const agent = getAgentId(extra) || "anonymous";
384
404
  logTool("my_status_set", agent, args);
@@ -555,7 +575,7 @@ function registerProposalTools(server, ctx, proposalManager) {
555
575
  }, async (params, extra) => {
556
576
  const createdBy = getAgentId(extra) || "anonymous";
557
577
  try {
558
- const proposal = proposalManager.create({
578
+ const proposal = await proposalManager.create({
559
579
  type: params.type,
560
580
  title: params.title,
561
581
  description: params.description,
@@ -596,7 +616,7 @@ function registerProposalTools(server, ctx, proposalManager) {
596
616
  reason: z.string().optional().describe("Optional reason for your vote")
597
617
  }, async ({ proposal: proposalId, choice, reason }, extra) => {
598
618
  const voter = getAgentId(extra) || "anonymous";
599
- const result = proposalManager.vote({
619
+ const result = await proposalManager.vote({
600
620
  proposalId,
601
621
  voter,
602
622
  choice,
@@ -629,7 +649,7 @@ function registerProposalTools(server, ctx, proposalManager) {
629
649
  });
630
650
  server.tool("team_proposal_status", "Check status of team proposals. Omit proposal ID to see all active proposals.", { proposal: z.string().optional().describe("Proposal ID (omit for all active)") }, async ({ proposal: proposalId }) => {
631
651
  if (proposalId) {
632
- const proposal = proposalManager.get(proposalId);
652
+ const proposal = await proposalManager.get(proposalId);
633
653
  if (!proposal) return { content: [{
634
654
  type: "text",
635
655
  text: JSON.stringify({
@@ -642,7 +662,7 @@ function registerProposalTools(server, ctx, proposalManager) {
642
662
  text: formatProposal(proposal)
643
663
  }] };
644
664
  }
645
- const activeProposals = proposalManager.list("active");
665
+ const activeProposals = await proposalManager.list("active");
646
666
  return { content: [{
647
667
  type: "text",
648
668
  text: activeProposals.length > 0 ? formatProposalList(activeProposals) : "(no active proposals)"
@@ -650,7 +670,7 @@ function registerProposalTools(server, ctx, proposalManager) {
650
670
  });
651
671
  server.tool("team_proposal_cancel", "Cancel a proposal you created.", { proposal: z.string().describe("Proposal ID to cancel") }, async ({ proposal: proposalId }, extra) => {
652
672
  const cancelledBy = getAgentId(extra) || "anonymous";
653
- const result = proposalManager.cancel(proposalId, cancelledBy);
673
+ const result = await proposalManager.cancel(proposalId, cancelledBy);
654
674
  if (!result.success) return { content: [{
655
675
  type: "text",
656
676
  text: JSON.stringify({
@@ -818,7 +838,7 @@ function createContextMCPServer(options) {
818
838
  //#endregion
819
839
  //#region src/workflow/context/types.ts
820
840
  /** Resource ID prefix */
821
- const RESOURCE_PREFIX = "res_";
841
+ const RESOURCE_PREFIX$1 = "res_";
822
842
  /** Resource URI scheme */
823
843
  const RESOURCE_SCHEME = "resource:";
824
844
  /** Message length threshold for channel messages - content longer than this should use resources or documents */
@@ -827,7 +847,7 @@ const MESSAGE_LENGTH_THRESHOLD = 1200;
827
847
  * Generate a unique resource ID
828
848
  */
829
849
  function generateResourceId() {
830
- return `${RESOURCE_PREFIX}${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
850
+ return `${RESOURCE_PREFIX$1}${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
831
851
  }
832
852
  /**
833
853
  * Create resource reference for use in markdown
@@ -876,98 +896,34 @@ function calculatePriority(msg) {
876
896
  //#endregion
877
897
  //#region src/workflow/context/provider.ts
878
898
  /**
879
- * Context Provider interface + unified implementation
880
- * Domain logic for channel, inbox, document, and resource operations.
881
- * Storage I/O is delegated to a StorageBackend.
882
- */
883
- /** Logical storage key layout */
884
- const KEYS = {
885
- channel: "channel.jsonl",
886
- inboxState: "_state/inbox.json",
887
- agentStatus: "_state/agent-status.json",
888
- documentPrefix: "documents/",
889
- resourcePrefix: "resources/"
890
- };
891
- /**
892
- * Unified ContextProvider implementation.
893
- * All domain logic lives here; storage I/O goes through StorageBackend.
899
+ * Composite ContextProvider delegates to domain-specific stores.
894
900
  *
895
- * Channel format: JSONL (one JSON object per line)
896
- * Documents: raw content strings
897
- * Resources: content-addressed blobs
898
- * Inbox state: JSON cursor file (ID-based)
901
+ * Each store owns one concern:
902
+ * - ChannelStore: append-only JSONL message log
903
+ * - InboxStore: filtered view of channel with per-agent cursors
904
+ * - DocumentStore: raw text documents
905
+ * - ResourceStore: content-addressed blobs
906
+ * - StatusStore: agent status tracking
907
+ *
908
+ * smartSend is the only cross-store orchestration (channel + resource).
899
909
  */
900
910
  var ContextProviderImpl = class {
901
- /** Cached parsed channel entries incrementally synced from storage */
902
- channelEntries = [];
903
- /** Storage byte offset up to which the cache is synced */
904
- channelOffset = 0;
905
- /** Guards concurrent syncChannel calls (share one in-flight read) */
906
- syncPromise = null;
907
- /** Run epoch: channel entry count at run start. Inbox ignores entries before this index. */
908
- runStartIndex = 0;
909
- constructor(storage, validAgents) {
910
- this.storage = storage;
911
+ constructor(channel, inbox, documents, resources, status, validAgents) {
912
+ this.channel = channel;
913
+ this.inbox = inbox;
914
+ this.documents = documents;
915
+ this.resources = resources;
916
+ this.status = status;
911
917
  this.validAgents = validAgents;
912
918
  }
913
- /** Expose storage backend (for ProposalManager, testing, etc.) */
914
- getStorage() {
915
- return this.storage;
916
- }
917
- /**
918
- * Sync cached entries from storage via incremental read.
919
- * Only parses newly appended JSONL lines since last sync.
920
- * Concurrent callers share the same in-flight read to avoid duplicate pushes.
921
- */
922
- syncChannel() {
923
- if (!this.syncPromise) this.syncPromise = this.doSyncChannel().finally(() => {
924
- this.syncPromise = null;
925
- });
926
- return this.syncPromise;
927
- }
928
- async doSyncChannel() {
929
- const result = await this.storage.readFrom(KEYS.channel, this.channelOffset);
930
- if (result.content) {
931
- this.channelEntries.push(...parseJsonl(result.content));
932
- this.channelOffset = result.offset;
933
- }
934
- return this.channelEntries;
935
- }
936
- async appendChannel(from, content, options) {
937
- const msg = {
938
- id: nanoid(),
939
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
940
- from,
941
- content,
942
- mentions: extractMentions(content, this.validAgents)
943
- };
944
- if (options?.to) msg.to = options.to;
945
- if (options?.kind) msg.kind = options.kind;
946
- if (options?.toolCall) msg.toolCall = options.toolCall;
947
- const line = JSON.stringify(msg) + "\n";
948
- await this.storage.append(KEYS.channel, line);
949
- return msg;
919
+ appendChannel(from, content, options) {
920
+ return this.channel.append(from, content, options);
950
921
  }
951
- async readChannel(options) {
952
- let entries = await this.syncChannel();
953
- if (options?.agent) {
954
- const agent = options.agent;
955
- entries = entries.filter((e) => {
956
- if (e.kind === "system" || e.kind === "debug" || e.kind === "output") return false;
957
- if (e.to) return e.to === agent || e.from === agent;
958
- return true;
959
- });
960
- }
961
- if (options?.since) entries = entries.filter((e) => e.timestamp > options.since);
962
- if (options?.limit && options.limit > 0) entries = entries.slice(-options.limit);
963
- return entries;
922
+ readChannel(options) {
923
+ return this.channel.read(options);
964
924
  }
965
- async tailChannel(cursor) {
966
- const entries = await this.syncChannel();
967
- return {
968
- entries: entries.slice(cursor),
969
- cursor: entries.length
970
- };
925
+ tailChannel(cursor) {
926
+ return this.channel.tail(cursor);
971
927
  }
972
928
  /**
973
929
  * Smart send: automatically converts long messages to resources
@@ -978,164 +934,60 @@ var ContextProviderImpl = class {
978
934
  * 3. Logs the full content in debug channel for visibility
979
935
  */
980
936
  async smartSend(from, content, options) {
981
- if (!shouldUseResource(content)) return this.appendChannel(from, content, options);
937
+ if (!shouldUseResource(content)) return this.channel.append(from, content, options);
982
938
  const resourceType = content.startsWith("```") || content.includes("\n```") ? "markdown" : "text";
983
- const resource = await this.createResource(content, from, resourceType);
984
- await this.appendChannel("system", `Created resource ${resource.id} (${content.length} chars) for @${from}:\n${content}`, { kind: "debug" });
939
+ const resource = await this.resources.create(content, from, resourceType);
940
+ await this.channel.append("system", `Created resource ${resource.id} (${content.length} chars) for @${from}:\n${content}`, { kind: "debug" });
985
941
  const mentions = extractMentions(content, this.validAgents);
986
942
  const shortMessage = `${mentions.length > 0 ? mentions.map((m) => `@${m}`).join(" ") + " " : ""}[Long content stored as resource]\n\nRead the full content: resource_read("${resource.id}")\n\nReference: ${resource.ref}`;
987
- return this.appendChannel(from, shortMessage, options);
988
- }
989
- async getInbox(agent) {
990
- const state = await this.loadInboxState();
991
- const lastAckId = state.readCursors[agent];
992
- const lastSeenId = state.seenCursors?.[agent];
993
- let entries = await this.syncChannel();
994
- if (this.runStartIndex > 0) entries = entries.slice(this.runStartIndex);
995
- if (lastAckId) {
996
- const ackIdx = entries.findIndex((e) => e.id === lastAckId);
997
- if (ackIdx >= 0) entries = entries.slice(ackIdx + 1);
998
- }
999
- let seenIdx = -1;
1000
- if (lastSeenId) seenIdx = entries.findIndex((e) => e.id === lastSeenId);
1001
- return entries.filter((e) => {
1002
- if (e.kind === "system" || e.kind === "debug" || e.kind === "output" || e.kind === "tool_call") return false;
1003
- if (e.from === agent) return false;
1004
- return e.mentions.includes(agent) || e.to === agent;
1005
- }).map((entry) => {
1006
- const entryIdx = entries.indexOf(entry);
1007
- return {
1008
- entry,
1009
- priority: calculatePriority(entry),
1010
- seen: seenIdx >= 0 && entryIdx <= seenIdx
1011
- };
1012
- });
1013
- }
1014
- async markInboxSeen(agent, untilId) {
1015
- const state = await this.loadInboxState();
1016
- if (!state.seenCursors) state.seenCursors = {};
1017
- state.seenCursors[agent] = untilId;
1018
- await this.storage.write(KEYS.inboxState, JSON.stringify(state, null, 2));
1019
- }
1020
- async ackInbox(agent, untilId) {
1021
- const state = await this.loadInboxState();
1022
- state.readCursors[agent] = untilId;
1023
- await this.storage.write(KEYS.inboxState, JSON.stringify(state, null, 2));
1024
- }
1025
- async loadInboxState() {
1026
- const raw = await this.storage.read(KEYS.inboxState);
1027
- if (!raw) return { readCursors: {} };
1028
- try {
1029
- const data = JSON.parse(raw);
1030
- return {
1031
- readCursors: data.readCursors || {},
1032
- seenCursors: data.seenCursors
1033
- };
1034
- } catch {
1035
- return { readCursors: {} };
1036
- }
943
+ return this.channel.append(from, shortMessage, options);
1037
944
  }
1038
- docKey(file) {
1039
- return KEYS.documentPrefix + (file || CONTEXT_DEFAULTS.document);
945
+ getInbox(agent) {
946
+ return this.inbox.getInbox(agent);
1040
947
  }
1041
- async readDocument(file) {
1042
- return await this.storage.read(this.docKey(file)) ?? "";
948
+ markInboxSeen(agent, untilId) {
949
+ return this.inbox.markSeen(agent, untilId);
1043
950
  }
1044
- async writeDocument(content, file) {
1045
- await this.storage.write(this.docKey(file), content);
951
+ ackInbox(agent, untilId) {
952
+ return this.inbox.ack(agent, untilId);
1046
953
  }
1047
- async appendDocument(content, file) {
1048
- await this.storage.append(this.docKey(file), content);
954
+ readDocument(file) {
955
+ return this.documents.read(file);
1049
956
  }
1050
- async listDocuments() {
1051
- return (await this.storage.list(KEYS.documentPrefix)).filter((f) => f.endsWith(".md")).sort();
957
+ writeDocument(content, file) {
958
+ return this.documents.write(content, file);
1052
959
  }
1053
- async createDocument(file, content) {
1054
- const key = this.docKey(file);
1055
- if (await this.storage.exists(key)) throw new Error(`Document already exists: ${file}`);
1056
- await this.storage.write(key, content);
960
+ appendDocument(content, file) {
961
+ return this.documents.append(content, file);
1057
962
  }
1058
- async createResource(content, _createdBy, type = "text") {
1059
- const id = generateResourceId();
1060
- const ext = type === "json" ? "json" : type === "diff" ? "diff" : "md";
1061
- const key = `${KEYS.resourcePrefix}${id}.${ext}`;
1062
- await this.storage.write(key, content);
1063
- return {
1064
- id,
1065
- ref: createResourceRef(id)
1066
- };
963
+ listDocuments() {
964
+ return this.documents.list();
1067
965
  }
1068
- async readResource(id) {
1069
- for (const ext of [
1070
- "md",
1071
- "json",
1072
- "diff",
1073
- "txt"
1074
- ]) {
1075
- const key = `${KEYS.resourcePrefix}${id}.${ext}`;
1076
- const content = await this.storage.read(key);
1077
- if (content !== null) return content;
1078
- }
1079
- return null;
966
+ createDocument(file, content) {
967
+ return this.documents.create(file, content);
1080
968
  }
1081
- async loadAgentStatus() {
1082
- const raw = await this.storage.read(KEYS.agentStatus);
1083
- if (!raw) return {};
1084
- try {
1085
- return JSON.parse(raw);
1086
- } catch {
1087
- return {};
1088
- }
969
+ createResource(content, createdBy, type) {
970
+ return this.resources.create(content, createdBy, type);
1089
971
  }
1090
- async saveAgentStatus(statuses) {
1091
- await this.storage.write(KEYS.agentStatus, JSON.stringify(statuses, null, 2));
972
+ readResource(id) {
973
+ return this.resources.read(id);
1092
974
  }
1093
- async setAgentStatus(agent, status) {
1094
- const statuses = await this.loadAgentStatus();
1095
- const existing = statuses[agent] || {
1096
- state: "idle",
1097
- lastUpdate: (/* @__PURE__ */ new Date()).toISOString()
1098
- };
1099
- statuses[agent] = {
1100
- ...existing,
1101
- ...status,
1102
- lastUpdate: (/* @__PURE__ */ new Date()).toISOString()
1103
- };
1104
- if (status.state === "running" && existing.state !== "running") statuses[agent].startedAt = (/* @__PURE__ */ new Date()).toISOString();
1105
- if (status.state === "idle") {
1106
- statuses[agent].startedAt = void 0;
1107
- statuses[agent].task = void 0;
1108
- }
1109
- await this.saveAgentStatus(statuses);
975
+ setAgentStatus(agent, status) {
976
+ return this.status.set(agent, status);
1110
977
  }
1111
- async getAgentStatus(agent) {
1112
- return (await this.loadAgentStatus())[agent] || null;
978
+ getAgentStatus(agent) {
979
+ return this.status.get(agent);
1113
980
  }
1114
- async listAgentStatus() {
1115
- return this.loadAgentStatus();
981
+ listAgentStatus() {
982
+ return this.status.list();
1116
983
  }
1117
984
  async markRunStart() {
1118
- this.runStartIndex = (await this.syncChannel()).length;
985
+ await this.inbox.markRunStart();
1119
986
  }
1120
987
  async destroy() {
1121
- await this.storage.delete(KEYS.inboxState);
988
+ await this.inbox.destroy();
1122
989
  }
1123
990
  };
1124
- /**
1125
- * Parse JSONL content into an array of objects.
1126
- * Skips empty lines and lines that fail to parse.
1127
- */
1128
- function parseJsonl(content) {
1129
- const results = [];
1130
- for (const line of content.split("\n")) {
1131
- const trimmed = line.trim();
1132
- if (!trimmed) continue;
1133
- try {
1134
- results.push(JSON.parse(trimmed));
1135
- } catch {}
1136
- }
1137
- return results;
1138
- }
1139
991
 
1140
992
  //#endregion
1141
993
  //#region src/workflow/context/storage.ts
@@ -1286,139 +1138,1676 @@ var FileStorage = class {
1286
1138
  return [];
1287
1139
  }
1288
1140
  }
1289
- async delete(key) {
1290
- try {
1291
- await unlink(this.resolve(key));
1292
- } catch {}
1293
- }
1294
- };
1141
+ async delete(key) {
1142
+ try {
1143
+ await unlink(this.resolve(key));
1144
+ } catch {}
1145
+ }
1146
+ };
1147
+
1148
+ //#endregion
1149
+ //#region src/workflow/context/stores/channel.ts
1150
+ /**
1151
+ * Channel Store
1152
+ * Append-only JSONL message log with incremental sync and visibility filtering.
1153
+ */
1154
+ const CHANNEL_KEY = "channel.jsonl";
1155
+ /**
1156
+ * JSONL-backed channel store.
1157
+ * Incrementally syncs from a StorageBackend using byte offsets.
1158
+ */
1159
+ var DefaultChannelStore = class {
1160
+ entries = [];
1161
+ offset = 0;
1162
+ syncPromise = null;
1163
+ constructor(storage, validAgents) {
1164
+ this.storage = storage;
1165
+ this.validAgents = validAgents;
1166
+ }
1167
+ sync() {
1168
+ if (!this.syncPromise) this.syncPromise = this.doSync().finally(() => {
1169
+ this.syncPromise = null;
1170
+ });
1171
+ return this.syncPromise;
1172
+ }
1173
+ async doSync() {
1174
+ const result = await this.storage.readFrom(CHANNEL_KEY, this.offset);
1175
+ if (result.content) {
1176
+ this.entries.push(...parseJsonl(result.content));
1177
+ this.offset = result.offset;
1178
+ }
1179
+ return this.entries;
1180
+ }
1181
+ length() {
1182
+ return this.entries.length;
1183
+ }
1184
+ async append(from, content, options) {
1185
+ const msg = {
1186
+ id: nanoid(),
1187
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1188
+ from,
1189
+ content,
1190
+ mentions: extractMentions(content, this.validAgents)
1191
+ };
1192
+ if (options?.to) msg.to = options.to;
1193
+ if (options?.kind) msg.kind = options.kind;
1194
+ if (options?.toolCall) msg.toolCall = options.toolCall;
1195
+ const line = JSON.stringify(msg) + "\n";
1196
+ await this.storage.append(CHANNEL_KEY, line);
1197
+ return msg;
1198
+ }
1199
+ async read(options) {
1200
+ let entries = await this.sync();
1201
+ if (options?.agent) {
1202
+ const agent = options.agent;
1203
+ entries = entries.filter((e) => {
1204
+ if (e.kind === "system" || e.kind === "debug" || e.kind === "output") return false;
1205
+ if (e.to) return e.to === agent || e.from === agent;
1206
+ return true;
1207
+ });
1208
+ }
1209
+ if (options?.since) entries = entries.filter((e) => e.timestamp > options.since);
1210
+ if (options?.limit && options.limit > 0) entries = entries.slice(-options.limit);
1211
+ return entries;
1212
+ }
1213
+ async tail(cursor) {
1214
+ const entries = await this.sync();
1215
+ return {
1216
+ entries: entries.slice(cursor),
1217
+ cursor: entries.length
1218
+ };
1219
+ }
1220
+ };
1221
+ /**
1222
+ * Parse JSONL content into an array of objects.
1223
+ * Skips empty lines and lines that fail to parse.
1224
+ */
1225
+ function parseJsonl(content) {
1226
+ const results = [];
1227
+ for (const line of content.split("\n")) {
1228
+ const trimmed = line.trim();
1229
+ if (!trimmed) continue;
1230
+ try {
1231
+ results.push(JSON.parse(trimmed));
1232
+ } catch {}
1233
+ }
1234
+ return results;
1235
+ }
1236
+
1237
+ //#endregion
1238
+ //#region src/workflow/context/stores/inbox.ts
1239
+ const INBOX_STATE_KEY = "_state/inbox.json";
1240
+ /**
1241
+ * Default inbox store backed by channel + JSON cursor file.
1242
+ * Inbox is a filtered view of the channel, not a separate log.
1243
+ */
1244
+ var DefaultInboxStore = class {
1245
+ runStartIndex = 0;
1246
+ constructor(channel, storage) {
1247
+ this.channel = channel;
1248
+ this.storage = storage;
1249
+ }
1250
+ async getInbox(agent) {
1251
+ const state = await this.loadState();
1252
+ const lastAckId = state.readCursors[agent];
1253
+ const lastSeenId = state.seenCursors?.[agent];
1254
+ let entries = await this.channel.sync();
1255
+ if (this.runStartIndex > 0) entries = entries.slice(this.runStartIndex);
1256
+ if (lastAckId) {
1257
+ const ackIdx = entries.findIndex((e) => e.id === lastAckId);
1258
+ if (ackIdx >= 0) entries = entries.slice(ackIdx + 1);
1259
+ }
1260
+ let seenIdx = -1;
1261
+ if (lastSeenId) seenIdx = entries.findIndex((e) => e.id === lastSeenId);
1262
+ return entries.filter((e) => {
1263
+ if (e.kind === "system" || e.kind === "debug" || e.kind === "output" || e.kind === "tool_call") return false;
1264
+ if (e.from === agent) return false;
1265
+ return e.mentions.includes(agent) || e.to === agent;
1266
+ }).map((entry) => {
1267
+ const entryIdx = entries.indexOf(entry);
1268
+ return {
1269
+ entry,
1270
+ priority: calculatePriority(entry),
1271
+ seen: seenIdx >= 0 && entryIdx <= seenIdx
1272
+ };
1273
+ });
1274
+ }
1275
+ async markSeen(agent, untilId) {
1276
+ const state = await this.loadState();
1277
+ if (!state.seenCursors) state.seenCursors = {};
1278
+ state.seenCursors[agent] = untilId;
1279
+ await this.storage.write(INBOX_STATE_KEY, JSON.stringify(state, null, 2));
1280
+ }
1281
+ async ack(agent, untilId) {
1282
+ const state = await this.loadState();
1283
+ state.readCursors[agent] = untilId;
1284
+ await this.storage.write(INBOX_STATE_KEY, JSON.stringify(state, null, 2));
1285
+ }
1286
+ async markRunStart() {
1287
+ this.runStartIndex = (await this.channel.sync()).length;
1288
+ }
1289
+ async destroy() {
1290
+ await this.storage.delete(INBOX_STATE_KEY);
1291
+ }
1292
+ async loadState() {
1293
+ const raw = await this.storage.read(INBOX_STATE_KEY);
1294
+ if (!raw) return { readCursors: {} };
1295
+ try {
1296
+ const data = JSON.parse(raw);
1297
+ return {
1298
+ readCursors: data.readCursors || {},
1299
+ seenCursors: data.seenCursors
1300
+ };
1301
+ } catch {
1302
+ return { readCursors: {} };
1303
+ }
1304
+ }
1305
+ };
1306
+
1307
+ //#endregion
1308
+ //#region src/workflow/context/stores/document.ts
1309
+ const DOCUMENT_PREFIX = "documents/";
1310
+ /**
1311
+ * Default document store backed by a StorageBackend.
1312
+ * Documents are stored as raw text under a key prefix.
1313
+ */
1314
+ var DefaultDocumentStore = class {
1315
+ constructor(storage) {
1316
+ this.storage = storage;
1317
+ }
1318
+ key(file) {
1319
+ return DOCUMENT_PREFIX + (file || CONTEXT_DEFAULTS.document);
1320
+ }
1321
+ async read(file) {
1322
+ return await this.storage.read(this.key(file)) ?? "";
1323
+ }
1324
+ async write(content, file) {
1325
+ await this.storage.write(this.key(file), content);
1326
+ }
1327
+ async append(content, file) {
1328
+ await this.storage.append(this.key(file), content);
1329
+ }
1330
+ async list() {
1331
+ return (await this.storage.list(DOCUMENT_PREFIX)).filter((f) => f.endsWith(".md")).sort();
1332
+ }
1333
+ async create(file, content) {
1334
+ const key = this.key(file);
1335
+ if (await this.storage.exists(key)) throw new Error(`Document already exists: ${file}`);
1336
+ await this.storage.write(key, content);
1337
+ }
1338
+ };
1339
+
1340
+ //#endregion
1341
+ //#region src/workflow/context/stores/resource.ts
1342
+ const RESOURCE_PREFIX = "resources/";
1343
+ /**
1344
+ * Default resource store backed by a StorageBackend.
1345
+ * Resources are keyed by generated ID with type-based extensions.
1346
+ */
1347
+ var DefaultResourceStore = class {
1348
+ constructor(storage) {
1349
+ this.storage = storage;
1350
+ }
1351
+ async create(content, _createdBy, type = "text") {
1352
+ const id = generateResourceId();
1353
+ const key = `${RESOURCE_PREFIX}${id}.${type === "json" ? "json" : type === "diff" ? "diff" : "md"}`;
1354
+ await this.storage.write(key, content);
1355
+ return {
1356
+ id,
1357
+ ref: createResourceRef(id)
1358
+ };
1359
+ }
1360
+ async read(id) {
1361
+ for (const ext of [
1362
+ "md",
1363
+ "json",
1364
+ "diff",
1365
+ "txt"
1366
+ ]) {
1367
+ const key = `${RESOURCE_PREFIX}${id}.${ext}`;
1368
+ const content = await this.storage.read(key);
1369
+ if (content !== null) return content;
1370
+ }
1371
+ return null;
1372
+ }
1373
+ };
1374
+
1375
+ //#endregion
1376
+ //#region src/workflow/context/stores/status.ts
1377
+ const STATUS_KEY = "_state/agent-status.json";
1378
+ /**
1379
+ * Default status store backed by a JSON file via StorageBackend.
1380
+ * All agent statuses are stored in a single JSON object.
1381
+ */
1382
+ var DefaultStatusStore = class {
1383
+ constructor(storage) {
1384
+ this.storage = storage;
1385
+ }
1386
+ async set(agent, status) {
1387
+ const statuses = await this.loadAll();
1388
+ const existing = statuses[agent] || {
1389
+ state: "idle",
1390
+ lastUpdate: (/* @__PURE__ */ new Date()).toISOString()
1391
+ };
1392
+ statuses[agent] = {
1393
+ ...existing,
1394
+ ...status,
1395
+ lastUpdate: (/* @__PURE__ */ new Date()).toISOString()
1396
+ };
1397
+ if (status.state === "running" && existing.state !== "running") statuses[agent].startedAt = (/* @__PURE__ */ new Date()).toISOString();
1398
+ if (status.state === "idle") {
1399
+ statuses[agent].startedAt = void 0;
1400
+ statuses[agent].task = void 0;
1401
+ }
1402
+ await this.save(statuses);
1403
+ }
1404
+ async get(agent) {
1405
+ return (await this.loadAll())[agent] || null;
1406
+ }
1407
+ async list() {
1408
+ return this.loadAll();
1409
+ }
1410
+ async loadAll() {
1411
+ const raw = await this.storage.read(STATUS_KEY);
1412
+ if (!raw) return {};
1413
+ try {
1414
+ return JSON.parse(raw);
1415
+ } catch {
1416
+ return {};
1417
+ }
1418
+ }
1419
+ async save(statuses) {
1420
+ await this.storage.write(STATUS_KEY, JSON.stringify(statuses, null, 2));
1421
+ }
1422
+ };
1423
+
1424
+ //#endregion
1425
+ //#region src/workflow/context/file-provider.ts
1426
+ /**
1427
+ * File Context Provider
1428
+ * Composes default stores with FileStorage backend.
1429
+ * Includes instance lock to prevent concurrent access to the same context directory.
1430
+ */
1431
+ var file_provider_exports = /* @__PURE__ */ __exportAll({
1432
+ FileContextProvider: () => FileContextProvider,
1433
+ createFileContextProvider: () => createFileContextProvider,
1434
+ getDefaultContextDir: () => getDefaultContextDir,
1435
+ resolveContextDir: () => resolveContextDir
1436
+ });
1437
+ /** Lock file name within context directory */
1438
+ const LOCK_FILE = "_state/instance.lock";
1439
+ /**
1440
+ * File-based ContextProvider.
1441
+ * Creates default stores backed by a shared FileStorage.
1442
+ *
1443
+ * Adds instance locking: only one process can hold the lock at a time.
1444
+ * Stale locks (from crashed processes) are automatically cleaned up.
1445
+ */
1446
+ var FileContextProvider = class extends ContextProviderImpl {
1447
+ lockPath;
1448
+ constructor(storage, validAgents, contextDir) {
1449
+ const channel = new DefaultChannelStore(storage, validAgents);
1450
+ const inbox = new DefaultInboxStore(channel, storage);
1451
+ const documents = new DefaultDocumentStore(storage);
1452
+ const resources = new DefaultResourceStore(storage);
1453
+ const status = new DefaultStatusStore(storage);
1454
+ super(channel, inbox, documents, resources, status, validAgents);
1455
+ this.contextDir = contextDir;
1456
+ this.lockPath = join(contextDir, LOCK_FILE);
1457
+ }
1458
+ /**
1459
+ * Acquire instance lock.
1460
+ * Throws if another live process holds the lock.
1461
+ * Automatically cleans up stale locks from dead processes.
1462
+ */
1463
+ acquireLock() {
1464
+ if (existsSync(this.lockPath)) try {
1465
+ const existing = JSON.parse(readFileSync(this.lockPath, "utf-8"));
1466
+ try {
1467
+ process.kill(existing.pid, 0);
1468
+ throw new Error(`Context directory is locked by another process (PID ${existing.pid}, started ${existing.startedAt}). If the process is no longer running, delete ${this.lockPath}`);
1469
+ } catch (e) {
1470
+ if (e instanceof Error && e.message.includes("Context directory is locked")) throw e;
1471
+ }
1472
+ } catch (e) {
1473
+ if (e instanceof Error && e.message.includes("Context directory is locked")) throw e;
1474
+ }
1475
+ const lock = {
1476
+ pid: process.pid,
1477
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1478
+ };
1479
+ const stateDir = join(this.contextDir, "_state");
1480
+ if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
1481
+ writeFileSync(this.lockPath, JSON.stringify(lock, null, 2));
1482
+ }
1483
+ /**
1484
+ * Release instance lock.
1485
+ * Safe to call even if lock is not held (no-op).
1486
+ */
1487
+ releaseLock() {
1488
+ try {
1489
+ if (existsSync(this.lockPath)) {
1490
+ if (JSON.parse(readFileSync(this.lockPath, "utf-8")).pid === process.pid) unlinkSync(this.lockPath);
1491
+ }
1492
+ } catch {}
1493
+ }
1494
+ /**
1495
+ * Override destroy to release lock and clean up transient state.
1496
+ */
1497
+ async destroy() {
1498
+ await super.destroy();
1499
+ this.releaseLock();
1500
+ }
1501
+ };
1502
+ /**
1503
+ * Resolve a context directory template to an absolute path.
1504
+ *
1505
+ * Supports:
1506
+ * - ${{ workflow.name }} — substituted with workflowName
1507
+ * - ${{ workflow.tag }} — substituted with tag
1508
+ * - ~ expansion to home directory
1509
+ * - Relative paths resolved against baseDir (or cwd if not provided)
1510
+ * - Absolute paths used as-is
1511
+ */
1512
+ function resolveContextDir(dirTemplate, opts) {
1513
+ const workflow = opts.workflow ?? opts.workflowName ?? "global";
1514
+ const workflowName = opts.workflowName ?? workflow;
1515
+ const tag = opts.tag ?? "main";
1516
+ let dir = dirTemplate.replace("${{ workflow.name }}", workflowName).replace("${{ workflow.tag }}", tag);
1517
+ if (dir.startsWith("~/")) dir = join(homedir(), dir.slice(2));
1518
+ else if (dir === "~") dir = homedir();
1519
+ else if (!isAbsolute(dir)) dir = join(opts.baseDir ?? process.cwd(), dir);
1520
+ return dir;
1521
+ }
1522
+ /**
1523
+ * Resolve context dir for a workflow:tag using default template.
1524
+ * Shorthand for the common case.
1525
+ * @param workflow Workflow name (defaults to "global")
1526
+ * @param tag Workflow instance tag (defaults to "main")
1527
+ */
1528
+ function getDefaultContextDir(workflow, tag) {
1529
+ const wf = workflow ?? "global";
1530
+ const t = tag ?? "main";
1531
+ return resolveContextDir(CONTEXT_DEFAULTS.dir, {
1532
+ workflow: wf,
1533
+ tag: t
1534
+ });
1535
+ }
1536
+ /**
1537
+ * Create a FileContextProvider with default paths.
1538
+ *
1539
+ * Directory layout:
1540
+ * contextDir/
1541
+ * ├── channel.jsonl # Channel log (JSONL)
1542
+ * ├── documents/ # Team documents
1543
+ * │ └── notes.md # Default document
1544
+ * ├── resources/ # Resource blobs
1545
+ * ├── _state/
1546
+ * │ ├── inbox.json # Inbox read cursors
1547
+ * │ ├── instance.lock # Instance lock (PID-based)
1548
+ * │ └── proposals.json # Proposal state
1549
+ * └── ...
1550
+ */
1551
+ function createFileContextProvider(contextDir, validAgents) {
1552
+ return new FileContextProvider(new FileStorage(contextDir), validAgents, contextDir);
1553
+ }
1554
+
1555
+ //#endregion
1556
+ //#region src/workflow/context/http-transport.ts
1557
+ /**
1558
+ * HTTP-based MCP Transport
1559
+ *
1560
+ * Hosts MCP server over HTTP using StreamableHTTPServerTransport.
1561
+ * CLI agents (cursor, claude, codex) connect directly via URL — no subprocess bridge needed.
1562
+ *
1563
+ * Each agent gets a unique URL: http://localhost:<port>/mcp?agent=<name>
1564
+ * The agent name is used as the MCP session ID, so tool handlers
1565
+ * receive it via extra.sessionId → getAgentId().
1566
+ */
1567
+ /**
1568
+ * Parse request body as JSON
1569
+ */
1570
+ function parseRequestBody(req) {
1571
+ return new Promise((resolve, reject) => {
1572
+ const chunks = [];
1573
+ req.on("data", (chunk) => chunks.push(chunk));
1574
+ req.on("end", () => {
1575
+ try {
1576
+ const body = Buffer.concat(chunks).toString();
1577
+ resolve(body ? JSON.parse(body) : void 0);
1578
+ } catch (err) {
1579
+ reject(err);
1580
+ }
1581
+ });
1582
+ req.on("error", reject);
1583
+ });
1584
+ }
1585
+ /**
1586
+ * Check if a JSON-RPC message is an initialize request
1587
+ */
1588
+ function isInitializeRequest(body) {
1589
+ if (Array.isArray(body)) return body.some((msg) => msg?.method === "initialize");
1590
+ return body?.method === "initialize";
1591
+ }
1592
+ /**
1593
+ * Start an HTTP MCP server
1594
+ *
1595
+ * Agents connect via: http://localhost:<port>/mcp?agent=<name>
1596
+ * The server creates a per-session StreamableHTTPServerTransport and McpServer.
1597
+ */
1598
+ async function runWithHttp(options) {
1599
+ const { createServerInstance, port = 0, onConnect, onDisconnect } = options;
1600
+ const sessions = /* @__PURE__ */ new Map();
1601
+ const httpServer = createServer(async (req, res) => {
1602
+ const reqUrl = new URL(req.url || "/", `http://localhost`);
1603
+ if (!reqUrl.pathname.startsWith("/mcp")) {
1604
+ res.writeHead(404, { "Content-Type": "application/json" });
1605
+ res.end(JSON.stringify({ error: "Not found" }));
1606
+ return;
1607
+ }
1608
+ const agentName = reqUrl.searchParams.get("agent") || "anonymous";
1609
+ const sessionId = req.headers["mcp-session-id"];
1610
+ if (sessionId && sessions.has(sessionId)) {
1611
+ const session = sessions.get(sessionId);
1612
+ if (req.method === "DELETE") {
1613
+ await session.transport.close();
1614
+ sessions.delete(sessionId);
1615
+ if (onDisconnect) onDisconnect(session.agentId, sessionId);
1616
+ res.writeHead(200);
1617
+ res.end();
1618
+ return;
1619
+ }
1620
+ const body = req.method === "POST" ? await parseRequestBody(req) : void 0;
1621
+ await session.transport.handleRequest(req, res, body);
1622
+ return;
1623
+ }
1624
+ if (req.method === "POST") {
1625
+ const body = await parseRequestBody(req);
1626
+ if (!isInitializeRequest(body)) {
1627
+ res.writeHead(400, { "Content-Type": "application/json" });
1628
+ res.end(JSON.stringify({ error: "Bad request: session required" }));
1629
+ return;
1630
+ }
1631
+ const transport = new StreamableHTTPServerTransport({
1632
+ sessionIdGenerator: () => `${agentName}-${randomUUID().slice(0, 8)}`,
1633
+ onsessioninitialized: (sid) => {
1634
+ sessions.set(sid, {
1635
+ transport,
1636
+ agentId: agentName
1637
+ });
1638
+ if (onConnect) onConnect(agentName, sid);
1639
+ }
1640
+ });
1641
+ Object.defineProperty(transport, "_agentId", {
1642
+ value: agentName,
1643
+ writable: true
1644
+ });
1645
+ await createServerInstance().connect(transport);
1646
+ await transport.handleRequest(req, res, body);
1647
+ return;
1648
+ }
1649
+ if (req.method === "GET") {
1650
+ res.writeHead(400, { "Content-Type": "application/json" });
1651
+ res.end(JSON.stringify({ error: "Session ID required for GET requests" }));
1652
+ return;
1653
+ }
1654
+ res.writeHead(405, { "Content-Type": "application/json" });
1655
+ res.end(JSON.stringify({ error: "Method not allowed" }));
1656
+ });
1657
+ const actualPort = await new Promise((resolve, reject) => {
1658
+ httpServer.on("error", reject);
1659
+ httpServer.listen(port, "127.0.0.1", () => {
1660
+ httpServer.removeListener("error", reject);
1661
+ const addr = httpServer.address();
1662
+ if (typeof addr === "object" && addr) resolve(addr.port);
1663
+ else reject(/* @__PURE__ */ new Error("Failed to get server address"));
1664
+ });
1665
+ });
1666
+ return {
1667
+ httpServer,
1668
+ url: `http://127.0.0.1:${actualPort}/mcp`,
1669
+ port: actualPort,
1670
+ sessions,
1671
+ async close() {
1672
+ for (const [sid, session] of sessions) {
1673
+ await session.transport.close();
1674
+ if (onDisconnect) onDisconnect(session.agentId, sid);
1675
+ }
1676
+ sessions.clear();
1677
+ await new Promise((resolve) => {
1678
+ httpServer.close(() => resolve());
1679
+ });
1680
+ }
1681
+ };
1682
+ }
1683
+
1684
+ //#endregion
1685
+ //#region src/workflow/loop/types.ts
1686
+ /** Default loop configuration values */
1687
+ const LOOP_DEFAULTS = {
1688
+ pollInterval: 5e3,
1689
+ retry: {
1690
+ maxAttempts: 3,
1691
+ backoffMs: 1e3,
1692
+ backoffMultiplier: 2
1693
+ },
1694
+ recentChannelLimit: 50,
1695
+ idleDebounceMs: 2e3
1696
+ };
1697
+
1698
+ //#endregion
1699
+ //#region src/workflow/loop/prompt.ts
1700
+ /**
1701
+ * Format inbox messages for display
1702
+ */
1703
+ function formatInbox(inbox) {
1704
+ if (inbox.length === 0) return "(no messages)";
1705
+ return inbox.map((m) => {
1706
+ const priority = m.priority === "high" ? " [HIGH]" : "";
1707
+ const time = m.entry.timestamp.slice(11, 19);
1708
+ const dm = m.entry.to ? " [DM]" : "";
1709
+ return `- [${time}] From @${m.entry.from}${priority}${dm}: ${m.entry.content}`;
1710
+ }).join("\n");
1711
+ }
1712
+ /**
1713
+ * Build the complete agent prompt from run context
1714
+ */
1715
+ function buildAgentPrompt(ctx) {
1716
+ const sections = [];
1717
+ sections.push("## Project");
1718
+ sections.push(`Working on: ${ctx.projectDir}`);
1719
+ sections.push("");
1720
+ sections.push(`## Inbox (${ctx.inbox.length} message${ctx.inbox.length === 1 ? "" : "s"} for you)`);
1721
+ sections.push(formatInbox(ctx.inbox));
1722
+ sections.push("");
1723
+ sections.push("## Recent Activity");
1724
+ sections.push("Use channel_read tool to view recent channel messages and conversation context if needed.");
1725
+ if (ctx.documentContent) {
1726
+ sections.push("");
1727
+ sections.push("## Shared Document");
1728
+ sections.push(ctx.documentContent);
1729
+ }
1730
+ if (ctx.retryAttempt > 1) {
1731
+ sections.push("");
1732
+ sections.push(`## Note`);
1733
+ sections.push(`This is retry attempt ${ctx.retryAttempt}. Previous attempt failed.`);
1734
+ }
1735
+ sections.push("");
1736
+ sections.push("## Instructions");
1737
+ sections.push("You are an agent in a multi-agent workflow. Communicate ONLY through the MCP tools below.");
1738
+ sections.push("Your text output is NOT seen by other agents — you MUST use channel_send to communicate.");
1739
+ sections.push("");
1740
+ sections.push("### Channel Tools");
1741
+ sections.push("- **channel_send**: Send a message to the shared channel. Use @agentname to mention/notify.");
1742
+ sections.push(" Use the \"to\" parameter for private DMs: channel_send({ message: \"...\", to: \"bob\" })");
1743
+ sections.push("- **channel_read**: Read recent channel messages (DMs and logs are auto-filtered).");
1744
+ sections.push("");
1745
+ sections.push("### Team Tools");
1746
+ sections.push("- **team_members**: List all agents you can @mention. Pass includeStatus=true to see their current state and tasks.");
1747
+ sections.push("- **team_doc_read/write/append/list/create**: Shared team documents.");
1748
+ sections.push("");
1749
+ sections.push("### Personal Tools");
1750
+ sections.push("- **my_inbox**: Check your unread messages.");
1751
+ sections.push("- **my_inbox_ack**: Acknowledge messages after processing (pass the latest message ID).");
1752
+ sections.push("- **my_status_set**: Update your status. Call when starting work (state='running', task='...') or when done (state='idle').");
1753
+ sections.push("");
1754
+ sections.push("### Proposal & Voting Tools");
1755
+ sections.push("- **team_proposal_create**: Create a proposal for team voting (types: election, decision, approval, assignment).");
1756
+ sections.push("- **team_vote**: Cast your vote on an active proposal. You can change your vote by voting again.");
1757
+ sections.push("- **team_proposal_status**: Check status of a proposal, or list all active proposals.");
1758
+ sections.push("- **team_proposal_cancel**: Cancel a proposal you created.");
1759
+ sections.push("");
1760
+ sections.push("### Resource Tools");
1761
+ sections.push("- **resource_create**: Store large content, get a reference (resource:id) for use anywhere.");
1762
+ sections.push("- **resource_read**: Read resource content by ID.");
1763
+ if (ctx.feedback) {
1764
+ sections.push("");
1765
+ sections.push("### Feedback Tool");
1766
+ sections.push("- **feedback_submit**: Report workflow improvement needs — a missing tool, an awkward step, or a capability gap.");
1767
+ sections.push(" Only use when you genuinely hit a pain point during your work.");
1768
+ }
1769
+ sections.push("");
1770
+ sections.push("### Workflow");
1771
+ sections.push("1. Read your inbox messages above");
1772
+ sections.push("2. Do your assigned work using channel_send with @mentions");
1773
+ sections.push("3. Acknowledge your inbox with my_inbox_ack");
1774
+ sections.push("4. Exit when your task is complete");
1775
+ sections.push("");
1776
+ sections.push("### IMPORTANT: When to stop");
1777
+ sections.push("- Once your assigned task is complete, acknowledge your inbox and exit. Do NOT keep chatting.");
1778
+ sections.push("- Do NOT send pleasantries (\"you're welcome\", \"glad to help\", \"thanks again\") — they trigger unnecessary cycles.");
1779
+ sections.push("- Do NOT @mention another agent in your final message unless you need them to do more work.");
1780
+ sections.push("- If you receive a thank-you or acknowledgment, just call my_inbox_ack and exit. Do not reply.");
1781
+ return sections.join("\n");
1782
+ }
1783
+
1784
+ //#endregion
1785
+ //#region src/workflow/loop/mcp-config.ts
1786
+ /**
1787
+ * Workflow MCP Config Generation & Writing
1788
+ *
1789
+ * Two responsibilities:
1790
+ * 1. Generate MCP config for workflow HTTP transport
1791
+ * 2. Write backend-specific MCP config files to workspace
1792
+ *
1793
+ * Writing lives here (not in backends) because it's workspace infrastructure,
1794
+ * not a backend concern. Backends only need their cwd set — they don't
1795
+ * need to know about MCP config file layout.
1796
+ */
1797
+ /**
1798
+ * Generate MCP config for workflow context server.
1799
+ *
1800
+ * Uses HTTP transport — CLI agents connect directly via URL:
1801
+ * { type: "http", url: "http://127.0.0.1:<port>/mcp?agent=<name>" }
1802
+ */
1803
+ function generateWorkflowMCPConfig(mcpUrl, agentName) {
1804
+ const url = `${mcpUrl}?agent=${encodeURIComponent(agentName)}`;
1805
+ return { mcpServers: { "workflow-context": {
1806
+ type: "http",
1807
+ url
1808
+ } } };
1809
+ }
1810
+ /**
1811
+ * Write MCP config to a workspace directory in the format expected by a backend.
1812
+ *
1813
+ * Each CLI backend reads MCP config from a different location:
1814
+ * - claude: {workspace}/mcp-config.json (passed via --mcp-config flag)
1815
+ * - cursor: {workspace}/.cursor/mcp.json (auto-discovered by cursor)
1816
+ * - codex: {workspace}/.codex/config.yaml (auto-discovered by codex)
1817
+ * - opencode: {workspace}/opencode.json (auto-discovered by opencode)
1818
+ * - default/mock: no config file needed (MCP handled by loop via SDK)
1819
+ */
1820
+ function writeBackendMcpConfig(backendType, workspaceDir, mcpConfig) {
1821
+ ensureDir(workspaceDir);
1822
+ switch (backendType) {
1823
+ case "claude":
1824
+ writeJsonConfig(join(workspaceDir, "mcp-config.json"), mcpConfig);
1825
+ break;
1826
+ case "cursor": {
1827
+ const cursorDir = join(workspaceDir, ".cursor");
1828
+ ensureDir(cursorDir);
1829
+ writeJsonConfig(join(cursorDir, "mcp.json"), mcpConfig);
1830
+ break;
1831
+ }
1832
+ case "codex": {
1833
+ const codexDir = join(workspaceDir, ".codex");
1834
+ ensureDir(codexDir);
1835
+ const codexConfig = { mcp_servers: mcpConfig.mcpServers };
1836
+ writeFileSync(join(codexDir, "config.yaml"), stringify(codexConfig));
1837
+ break;
1838
+ }
1839
+ case "opencode": {
1840
+ const opencodeMcp = {};
1841
+ for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
1842
+ const serverConfig = config;
1843
+ if (serverConfig.type === "http") opencodeMcp[name] = serverConfig;
1844
+ else opencodeMcp[name] = {
1845
+ type: "local",
1846
+ command: [serverConfig.command, ...serverConfig.args || []],
1847
+ enabled: true,
1848
+ ...serverConfig.env ? { environment: serverConfig.env } : {}
1849
+ };
1850
+ }
1851
+ const opencodeConfig = {
1852
+ $schema: "https://opencode.ai/config.json",
1853
+ mcp: opencodeMcp
1854
+ };
1855
+ writeJsonConfig(join(workspaceDir, "opencode.json"), opencodeConfig);
1856
+ break;
1857
+ }
1858
+ }
1859
+ }
1860
+ function ensureDir(dir) {
1861
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1862
+ }
1863
+ function writeJsonConfig(path, data) {
1864
+ writeFileSync(path, JSON.stringify(data, null, 2));
1865
+ }
1866
+
1867
+ //#endregion
1868
+ //#region src/daemon/cron.ts
1869
+ /**
1870
+ * Minimal cron expression parser.
1871
+ * Supports standard 5-field cron: minute hour day-of-month month day-of-week
1872
+ *
1873
+ * Field syntax:
1874
+ * * every value
1875
+ * N exact value
1876
+ * N-M range (inclusive)
1877
+ * N,M,O list
1878
+ * * /step every step (e.g. * /15 = 0,15,30,45) [no space — formatting only]
1879
+ * N-M/step range with step
1880
+ */
1881
+ function range(min, max) {
1882
+ const r = [];
1883
+ for (let i = min; i <= max; i++) r.push(i);
1884
+ return r;
1885
+ }
1886
+ function parseIntStrict(s, context) {
1887
+ const n = parseInt(s, 10);
1888
+ if (isNaN(n)) throw new Error(`Invalid number "${s}" in ${context}`);
1889
+ return n;
1890
+ }
1891
+ function parseCronField(field, min, max) {
1892
+ const values = /* @__PURE__ */ new Set();
1893
+ for (const part of field.split(",")) if (part === "*") for (const v of range(min, max)) values.add(v);
1894
+ else if (part.includes("/")) {
1895
+ const [rangeStr, stepStr] = part.split("/");
1896
+ const step = parseIntStrict(stepStr, `step "${part}"`);
1897
+ if (step <= 0) throw new Error(`Invalid step: ${part}`);
1898
+ let lo = min;
1899
+ let hi = max;
1900
+ if (rangeStr !== "*") if (rangeStr.includes("-")) {
1901
+ const parts = rangeStr.split("-");
1902
+ lo = parseIntStrict(parts[0], `range "${part}"`);
1903
+ hi = parseIntStrict(parts[1], `range "${part}"`);
1904
+ } else {
1905
+ lo = parseIntStrict(rangeStr, `field "${part}"`);
1906
+ hi = max;
1907
+ }
1908
+ for (let v = lo; v <= hi; v += step) values.add(v);
1909
+ } else if (part.includes("-")) {
1910
+ const parts = part.split("-");
1911
+ const lo = parseIntStrict(parts[0], `range "${part}"`);
1912
+ const hi = parseIntStrict(parts[1], `range "${part}"`);
1913
+ for (const v of range(lo, hi)) values.add(v);
1914
+ } else values.add(parseIntStrict(part, `field "${field}"`));
1915
+ return values;
1916
+ }
1917
+ /**
1918
+ * Parse a 5-field cron expression into sets of matching values.
1919
+ */
1920
+ function parseCron(expr) {
1921
+ const parts = expr.trim().split(/\s+/);
1922
+ if (parts.length !== 5) throw new Error(`Invalid cron expression (expected 5 fields): ${expr}`);
1923
+ return {
1924
+ minutes: parseCronField(parts[0], 0, 59),
1925
+ hours: parseCronField(parts[1], 0, 23),
1926
+ daysOfMonth: parseCronField(parts[2], 1, 31),
1927
+ months: parseCronField(parts[3], 1, 12),
1928
+ daysOfWeek: parseCronField(parts[4], 0, 6)
1929
+ };
1930
+ }
1931
+ /**
1932
+ * Check if a Date matches a parsed cron expression.
1933
+ */
1934
+ function matchesCron(date, fields) {
1935
+ return fields.minutes.has(date.getMinutes()) && fields.hours.has(date.getHours()) && fields.daysOfMonth.has(date.getDate()) && fields.months.has(date.getMonth() + 1) && fields.daysOfWeek.has(date.getDay());
1936
+ }
1937
+ /**
1938
+ * Calculate the next occurrence of a cron expression after `from`.
1939
+ * Searches forward minute-by-minute, up to 1 year.
1940
+ * Returns the Date of the next match.
1941
+ */
1942
+ function nextCronTime(expr, from = /* @__PURE__ */ new Date()) {
1943
+ const fields = parseCron(expr);
1944
+ const next = new Date(from);
1945
+ next.setSeconds(0, 0);
1946
+ next.setMinutes(next.getMinutes() + 1);
1947
+ const maxMinutes = 366 * 24 * 60;
1948
+ for (let i = 0; i < maxMinutes; i++) {
1949
+ if (matchesCron(next, fields)) return next;
1950
+ next.setMinutes(next.getMinutes() + 1);
1951
+ }
1952
+ throw new Error(`No matching cron time found within 1 year: ${expr}`);
1953
+ }
1954
+ /**
1955
+ * Calculate ms until the next cron occurrence.
1956
+ */
1957
+ function msUntilNextCron(expr, from = /* @__PURE__ */ new Date()) {
1958
+ return nextCronTime(expr, from).getTime() - from.getTime();
1959
+ }
1960
+
1961
+ //#endregion
1962
+ //#region src/workflow/loop/mock-runner.ts
1963
+ /**
1964
+ * Mock Agent Runner
1965
+ *
1966
+ * Orchestrates mock agent execution for workflow integration testing.
1967
+ * Uses AI SDK generateText with MockLanguageModelV3 and real MCP tool calls.
1968
+ *
1969
+ * This lives in the loop layer (not backends) because it does orchestration:
1970
+ * connecting to MCP, building prompts, managing tool loops.
1971
+ * The mock backend itself is just a simple send() adapter.
1972
+ */
1973
+ /**
1974
+ * Connect to workflow MCP server via HTTP and create AI SDK tool wrappers
1975
+ */
1976
+ async function createMCPToolBridge$1(mcpUrl, agentName) {
1977
+ const transport = new StreamableHTTPClientTransport(new URL(`${mcpUrl}?agent=${encodeURIComponent(agentName)}`));
1978
+ const client = new Client({
1979
+ name: agentName,
1980
+ version: "1.0.0"
1981
+ });
1982
+ await client.connect(transport);
1983
+ const { tools: mcpTools } = await client.listTools();
1984
+ const aiTools = {};
1985
+ for (const mcpTool of mcpTools) {
1986
+ const toolName = mcpTool.name;
1987
+ aiTools[toolName] = createTool({
1988
+ description: mcpTool.description || toolName,
1989
+ schema: mcpTool.inputSchema,
1990
+ execute: async (args) => {
1991
+ return (await client.callTool({
1992
+ name: toolName,
1993
+ arguments: args
1994
+ })).content;
1995
+ }
1996
+ });
1997
+ }
1998
+ return {
1999
+ tools: aiTools,
2000
+ close: () => client.close()
2001
+ };
2002
+ }
2003
+ /**
2004
+ * Run a mock agent with AI SDK and real MCP tools.
2005
+ *
2006
+ * Used by the loop when backend.type === 'mock'.
2007
+ * Unlike real backends that just send(), the mock runner needs to:
2008
+ * 1. Connect to MCP server for real tool execution
2009
+ * 2. Generate scripted tool calls via MockLanguageModelV3
2010
+ * 3. Execute the full tool loop to test channel/document flow
2011
+ */
2012
+ async function runMockAgent(ctx, debugLog) {
2013
+ const startTime = Date.now();
2014
+ const log = debugLog || (() => {});
2015
+ try {
2016
+ if (!ctx.mcpUrl) return {
2017
+ success: false,
2018
+ error: "Mock runner requires mcpUrl (HTTP MCP server)",
2019
+ duration: 0
2020
+ };
2021
+ const mcp = await createMCPToolBridge$1(ctx.mcpUrl, ctx.name);
2022
+ log(`MCP connected, ${Object.keys(mcp.tools).length} tools`);
2023
+ const inboxSummary = ctx.inbox.map((m) => `${m.entry.from}: ${m.entry.content.slice(0, 80).replace(/@/g, "")}`).join("; ");
2024
+ const mockModel = new MockLanguageModelV3({ doGenerate: mockValues({
2025
+ content: [{
2026
+ type: "tool-call",
2027
+ toolCallId: `call-${ctx.name}-${Date.now()}`,
2028
+ toolName: "channel_send",
2029
+ input: JSON.stringify({ message: `[${ctx.name}] Processed: ${inboxSummary.slice(0, 200)}` })
2030
+ }],
2031
+ finishReason: {
2032
+ unified: "tool-calls",
2033
+ raw: "tool_use"
2034
+ },
2035
+ usage: {
2036
+ inputTokens: {
2037
+ total: 100,
2038
+ noCache: 100,
2039
+ cacheRead: 0,
2040
+ cacheWrite: 0
2041
+ },
2042
+ outputTokens: {
2043
+ total: 50,
2044
+ text: 50,
2045
+ reasoning: 0
2046
+ }
2047
+ }
2048
+ }, {
2049
+ content: [{
2050
+ type: "text",
2051
+ text: `${ctx.name} done.`
2052
+ }],
2053
+ finishReason: {
2054
+ unified: "stop",
2055
+ raw: "end_turn"
2056
+ },
2057
+ usage: {
2058
+ inputTokens: {
2059
+ total: 50,
2060
+ noCache: 50,
2061
+ cacheRead: 0,
2062
+ cacheWrite: 0
2063
+ },
2064
+ outputTokens: {
2065
+ total: 10,
2066
+ text: 10,
2067
+ reasoning: 0
2068
+ }
2069
+ }
2070
+ }) });
2071
+ const prompt = buildAgentPrompt(ctx);
2072
+ log(`Prompt (${prompt.length} chars)`);
2073
+ const result = await generateText({
2074
+ model: mockModel,
2075
+ tools: mcp.tools,
2076
+ prompt,
2077
+ system: ctx.agent.resolvedSystemPrompt,
2078
+ stopWhen: stepCountIs(3)
2079
+ });
2080
+ const totalToolCalls = result.steps.reduce((n, s) => n + s.toolCalls.length, 0);
2081
+ await mcp.close();
2082
+ return {
2083
+ success: true,
2084
+ duration: Date.now() - startTime,
2085
+ steps: result.steps.length,
2086
+ toolCalls: totalToolCalls
2087
+ };
2088
+ } catch (error) {
2089
+ return {
2090
+ success: false,
2091
+ error: error instanceof Error ? error.message : String(error),
2092
+ duration: Date.now() - startTime
2093
+ };
2094
+ }
2095
+ }
2096
+
2097
+ //#endregion
2098
+ //#region src/workflow/loop/sdk-runner.ts
2099
+ /**
2100
+ * SDK Agent Runner
2101
+ *
2102
+ * Runs SDK agents with full tool access in workflows:
2103
+ * - MCP context tools (channel_send, document_write, etc.)
2104
+ * - Bash tool for shell commands
2105
+ *
2106
+ * Same pattern as mock-runner.ts but with real models via createModelAsync.
2107
+ * This is the standard execution path for SDK backends in workflows —
2108
+ * all agents get MCP + bash regardless of backend type.
2109
+ */
2110
+ /** Extract useful details from AI SDK errors (statusCode, url, responseBody) */
2111
+ function formatError(error) {
2112
+ if (!(error instanceof Error)) return String(error);
2113
+ const e = error;
2114
+ const parts = [error.message];
2115
+ if (e.statusCode) parts[0] = `HTTP ${e.statusCode}: ${error.message}`;
2116
+ if (e.url) parts.push(`url=${e.url}`);
2117
+ if (e.responseBody && typeof e.responseBody === "string") {
2118
+ const body = e.responseBody.length > 200 ? e.responseBody.slice(0, 200) + "…" : e.responseBody;
2119
+ parts.push(`body=${body}`);
2120
+ }
2121
+ return parts.join(" ");
2122
+ }
2123
+ /** Truncate string, flatten newlines */
2124
+ function truncate(s, max) {
2125
+ const flat = s.replace(/\s+/g, " ").trim();
2126
+ return flat.length > max ? flat.slice(0, max) + "…" : flat;
2127
+ }
2128
+ /** Format a tool call for concise single-line debug output (function call syntax) */
2129
+ function formatToolCall(tc) {
2130
+ const input = tc.input ?? tc.args ?? {};
2131
+ const pairs = Object.entries(input).map(([k, v]) => {
2132
+ return `${k}=${truncate(typeof v === "string" ? v : JSON.stringify(v), 60)}`;
2133
+ });
2134
+ return `${tc.toolName}(${pairs.join(", ")})`;
2135
+ }
2136
+ /**
2137
+ * Connect to workflow MCP server and create AI SDK tool wrappers.
2138
+ * Same bridge as mock-runner — extracted here for SDK agents.
2139
+ */
2140
+ async function createMCPToolBridge(mcpUrl, agentName) {
2141
+ const transport = new StreamableHTTPClientTransport(new URL(`${mcpUrl}?agent=${encodeURIComponent(agentName)}`));
2142
+ const client = new Client({
2143
+ name: agentName,
2144
+ version: "1.0.0"
2145
+ });
2146
+ await client.connect(transport);
2147
+ const { tools: mcpTools } = await client.listTools();
2148
+ const aiTools = {};
2149
+ for (const mcpTool of mcpTools) {
2150
+ const toolName = mcpTool.name;
2151
+ aiTools[toolName] = createTool({
2152
+ description: mcpTool.description || toolName,
2153
+ schema: mcpTool.inputSchema,
2154
+ execute: async (args) => {
2155
+ return (await client.callTool({
2156
+ name: toolName,
2157
+ arguments: args
2158
+ })).content;
2159
+ }
2160
+ });
2161
+ }
2162
+ return {
2163
+ tools: aiTools,
2164
+ close: () => client.close()
2165
+ };
2166
+ }
2167
+ function createBashTool() {
2168
+ return createTool({
2169
+ description: "Execute a shell command and return stdout/stderr.",
2170
+ schema: {
2171
+ type: "object",
2172
+ properties: { command: {
2173
+ type: "string",
2174
+ description: "The shell command to execute"
2175
+ } },
2176
+ required: ["command"]
2177
+ },
2178
+ execute: async (args) => {
2179
+ const command = args.command;
2180
+ try {
2181
+ return execSync(command, {
2182
+ encoding: "utf-8",
2183
+ timeout: 12e4
2184
+ }).trim() || "(no output)";
2185
+ } catch (error) {
2186
+ return `Error (exit ${error.status}): ${error.stderr || error.message}`;
2187
+ }
2188
+ }
2189
+ });
2190
+ }
2191
+ /**
2192
+ * Run an SDK agent with real model + MCP tools + bash.
2193
+ *
2194
+ * Used by the loop when backend.type === 'default'.
2195
+ * Unlike the simple SdkBackend.send() (text-only), this runner:
2196
+ * 1. Connects to MCP server for context tools (channel, document)
2197
+ * 2. Adds bash tool for shell access
2198
+ * 3. Runs generateText with full tool loop
2199
+ */
2200
+ async function runSdkAgent(ctx, debugLog) {
2201
+ const startTime = Date.now();
2202
+ const log = debugLog || (() => {});
2203
+ try {
2204
+ if (!ctx.mcpUrl) return {
2205
+ success: false,
2206
+ error: "SDK runner requires mcpUrl",
2207
+ duration: 0
2208
+ };
2209
+ const mcp = await createMCPToolBridge(ctx.mcpUrl, ctx.name);
2210
+ log(`MCP connected, ${Object.keys(mcp.tools).length} context tools`);
2211
+ const model = await createModelAsync(ctx.agent.model);
2212
+ const tools = {
2213
+ ...mcp.tools,
2214
+ bash: createBashTool()
2215
+ };
2216
+ const prompt = buildAgentPrompt(ctx);
2217
+ log(`Prompt (${prompt.length} chars) → sdk with ${Object.keys(tools).length} tools`);
2218
+ let _stepNum = 0;
2219
+ const result = await generateText({
2220
+ model,
2221
+ tools,
2222
+ system: ctx.agent.resolvedSystemPrompt,
2223
+ prompt,
2224
+ maxOutputTokens: ctx.agent.max_tokens ?? 8192,
2225
+ stopWhen: stepCountIs(ctx.agent.max_steps ?? 200),
2226
+ onStepFinish: (step) => {
2227
+ _stepNum++;
2228
+ if (step.toolCalls?.length && ctx.eventLog) {
2229
+ for (const tc of step.toolCalls) if (tc.toolName === "bash") ctx.eventLog.toolCall(ctx.name, tc.toolName, formatToolCall(tc), "sdk");
2230
+ }
2231
+ }
2232
+ });
2233
+ const totalToolCalls = result.steps.reduce((n, s) => n + s.toolCalls.length, 0);
2234
+ const lastStep = result.steps[result.steps.length - 1];
2235
+ if (ctx.agent.max_steps && result.steps.length >= ctx.agent.max_steps && (lastStep?.toolCalls?.length ?? 0) > 0) {
2236
+ const warning = `⚠️ Agent reached max_steps limit (${ctx.agent.max_steps}) but wanted to continue. Consider increasing max_steps or removing the limit.`;
2237
+ log(warning);
2238
+ await ctx.provider.appendChannel(ctx.name, warning, { kind: "system" }).catch(() => {});
2239
+ }
2240
+ await mcp.close();
2241
+ return {
2242
+ success: true,
2243
+ duration: Date.now() - startTime,
2244
+ content: result.text,
2245
+ steps: result.steps.length,
2246
+ toolCalls: totalToolCalls
2247
+ };
2248
+ } catch (error) {
2249
+ return {
2250
+ success: false,
2251
+ error: formatError(error),
2252
+ duration: Date.now() - startTime
2253
+ };
2254
+ }
2255
+ }
2256
+
2257
+ //#endregion
2258
+ //#region src/workflow/loop/loop.ts
2259
+ /** Check if loop should continue running */
2260
+ function shouldContinue(state) {
2261
+ return state !== "stopped";
2262
+ }
2263
+ /**
2264
+ * Create an agent loop
2265
+ *
2266
+ * The loop:
2267
+ * 1. Polls for inbox messages on an interval
2268
+ * 2. Runs the agent when messages are found
2269
+ * 3. Acknowledges inbox only on successful run
2270
+ * 4. Retries with exponential backoff on failure
2271
+ * 5. Can be woken early via wake()
2272
+ */
2273
+ function createAgentLoop(config) {
2274
+ const { name, agent, contextProvider, eventLog, mcpUrl, workspaceDir, projectDir, backend, onRunComplete, log = () => {}, feedback } = config;
2275
+ const infoLog = config.infoLog ?? log;
2276
+ const errorLog = config.errorLog ?? log;
2277
+ const pollInterval = config.pollInterval ?? LOOP_DEFAULTS.pollInterval;
2278
+ const retryConfig = {
2279
+ maxAttempts: config.retry?.maxAttempts ?? LOOP_DEFAULTS.retry.maxAttempts,
2280
+ backoffMs: config.retry?.backoffMs ?? LOOP_DEFAULTS.retry.backoffMs,
2281
+ backoffMultiplier: config.retry?.backoffMultiplier ?? LOOP_DEFAULTS.retry.backoffMultiplier
2282
+ };
2283
+ let state = "stopped";
2284
+ let wakeResolver = null;
2285
+ let pollTimeout = null;
2286
+ let directRunning = false;
2287
+ const scheduleConfig = agent.schedule;
2288
+ let resolvedSchedule;
2289
+ if (scheduleConfig) try {
2290
+ resolvedSchedule = resolveSchedule(scheduleConfig);
2291
+ } catch (err) {
2292
+ const msg = err instanceof Error ? err.message : String(err);
2293
+ throw new Error(`Agent "${name}" has invalid schedule config: ${msg}`);
2294
+ }
2295
+ let lastActivityTime = Date.now();
2296
+ /**
2297
+ * Wait for either poll interval or wake() call
2298
+ */
2299
+ async function waitForWakeOrPoll() {
2300
+ return new Promise((resolve) => {
2301
+ wakeResolver = resolve;
2302
+ pollTimeout = setTimeout(() => {
2303
+ wakeResolver = null;
2304
+ resolve();
2305
+ }, pollInterval);
2306
+ });
2307
+ }
2308
+ /**
2309
+ * Main poll loop
2310
+ */
2311
+ async function runLoop() {
2312
+ while (shouldContinue(state)) {
2313
+ await waitForWakeOrPoll();
2314
+ if (!shouldContinue(state)) break;
2315
+ if (directRunning) continue;
2316
+ const inbox = await contextProvider.getInbox(name);
2317
+ if (inbox.length === 0) {
2318
+ if (resolvedSchedule) {
2319
+ const now = Date.now();
2320
+ let wakeupDue = false;
2321
+ if (resolvedSchedule.type === "interval") {
2322
+ if (now - lastActivityTime >= resolvedSchedule.ms) wakeupDue = true;
2323
+ } else if (resolvedSchedule.type === "cron") {
2324
+ const msTillNext = msUntilNextCron(resolvedSchedule.expr, new Date(lastActivityTime));
2325
+ if (now >= lastActivityTime + msTillNext) wakeupDue = true;
2326
+ }
2327
+ if (wakeupDue) {
2328
+ const wakeupPrompt = resolvedSchedule.prompt ?? "Scheduled wakeup. Check for any pending work or updates.";
2329
+ log(`Schedule wakeup triggered for ${name}`);
2330
+ await contextProvider.appendChannel("system", `@${name} ${wakeupPrompt}`);
2331
+ lastActivityTime = now;
2332
+ continue;
2333
+ }
2334
+ }
2335
+ state = "idle";
2336
+ await contextProvider.setAgentStatus(name, { state: "idle" });
2337
+ continue;
2338
+ }
2339
+ const senders = inbox.map((m) => m.entry.from);
2340
+ infoLog(`Inbox: ${inbox.length} message(s) from [${senders.join(", ")}]`);
2341
+ for (const msg of inbox) {
2342
+ const preview = msg.entry.content.length > 120 ? msg.entry.content.slice(0, 120) + "..." : msg.entry.content;
2343
+ log(` from @${msg.entry.from}: ${preview}`);
2344
+ }
2345
+ const latestId = inbox[inbox.length - 1].entry.id;
2346
+ await contextProvider.markInboxSeen(name, latestId);
2347
+ let attempt = 0;
2348
+ let lastResult = null;
2349
+ while (attempt < retryConfig.maxAttempts && shouldContinue(state)) {
2350
+ attempt++;
2351
+ state = "running";
2352
+ await contextProvider.setAgentStatus(name, { state: "running" });
2353
+ infoLog(`Running (attempt ${attempt}/${retryConfig.maxAttempts})`);
2354
+ lastResult = await runAgent(backend, {
2355
+ name,
2356
+ agent,
2357
+ inbox,
2358
+ recentChannel: await contextProvider.readChannel({
2359
+ limit: LOOP_DEFAULTS.recentChannelLimit,
2360
+ agent: name
2361
+ }),
2362
+ documentContent: await contextProvider.readDocument(),
2363
+ mcpUrl,
2364
+ workspaceDir,
2365
+ projectDir,
2366
+ retryAttempt: attempt,
2367
+ provider: contextProvider,
2368
+ eventLog,
2369
+ feedback
2370
+ }, log, infoLog);
2371
+ if (lastResult.success) {
2372
+ infoLog(`DONE ${lastResult.steps ? `${lastResult.steps} steps, ${lastResult.toolCalls} tool calls, ${lastResult.duration}ms` : `${lastResult.duration}ms`}`);
2373
+ if (lastResult.content) await contextProvider.appendChannel(name, lastResult.content);
2374
+ await contextProvider.ackInbox(name, latestId);
2375
+ lastActivityTime = Date.now();
2376
+ await contextProvider.setAgentStatus(name, { state: "idle" });
2377
+ break;
2378
+ }
2379
+ errorLog(`ERROR ${lastResult.error}`);
2380
+ if (attempt < retryConfig.maxAttempts && shouldContinue(state)) {
2381
+ const delay = retryConfig.backoffMs * Math.pow(retryConfig.backoffMultiplier, attempt - 1);
2382
+ log(`Retrying in ${delay}ms...`);
2383
+ await sleep(delay);
2384
+ }
2385
+ }
2386
+ if (lastResult && !lastResult.success) {
2387
+ errorLog(`ERROR max retries exhausted, acknowledging to prevent loop`);
2388
+ await contextProvider.ackInbox(name, latestId);
2389
+ }
2390
+ if (lastResult && onRunComplete) onRunComplete(lastResult);
2391
+ state = "idle";
2392
+ await contextProvider.setAgentStatus(name, { state: "idle" });
2393
+ }
2394
+ }
2395
+ return {
2396
+ get name() {
2397
+ return name;
2398
+ },
2399
+ get state() {
2400
+ return state;
2401
+ },
2402
+ async start() {
2403
+ if (state !== "stopped") throw new Error(`Loop ${name} is already running`);
2404
+ state = "idle";
2405
+ lastActivityTime = Date.now();
2406
+ await contextProvider.setAgentStatus(name, { state: "idle" });
2407
+ if (resolvedSchedule) infoLog(`Starting (schedule: ${resolvedSchedule.type === "interval" ? `${resolvedSchedule.ms}ms interval` : `cron "${resolvedSchedule.expr}"`})`);
2408
+ else infoLog(`Starting`);
2409
+ runLoop().catch((error) => {
2410
+ errorLog(`ERROR ${error instanceof Error ? error.message : String(error)}`);
2411
+ state = "stopped";
2412
+ contextProvider.setAgentStatus(name, { state: "stopped" }).catch(() => {});
2413
+ });
2414
+ },
2415
+ async stop() {
2416
+ log(`Stopping`);
2417
+ state = "stopped";
2418
+ await contextProvider.setAgentStatus(name, { state: "stopped" });
2419
+ if (backend.abort) backend.abort();
2420
+ if (pollTimeout) {
2421
+ clearTimeout(pollTimeout);
2422
+ pollTimeout = null;
2423
+ }
2424
+ if (wakeResolver) {
2425
+ wakeResolver();
2426
+ wakeResolver = null;
2427
+ }
2428
+ },
2429
+ wake() {
2430
+ if (state === "idle" && wakeResolver) {
2431
+ log(`Waking`);
2432
+ if (pollTimeout) {
2433
+ clearTimeout(pollTimeout);
2434
+ pollTimeout = null;
2435
+ }
2436
+ wakeResolver();
2437
+ wakeResolver = null;
2438
+ }
2439
+ },
2440
+ async sendDirect(message) {
2441
+ if (directRunning) return {
2442
+ success: false,
2443
+ error: "Agent is already processing a direct request",
2444
+ duration: 0
2445
+ };
2446
+ if (state === "running") return {
2447
+ success: false,
2448
+ error: "Agent is currently running (poll loop)",
2449
+ duration: 0
2450
+ };
2451
+ directRunning = true;
2452
+ const prevState = state;
2453
+ state = "running";
2454
+ await contextProvider.setAgentStatus(name, { state: "running" });
2455
+ try {
2456
+ await contextProvider.appendChannel("user", `@${name} ${message}`);
2457
+ const inbox = await contextProvider.getInbox(name);
2458
+ const latestId = inbox.length > 0 ? inbox[inbox.length - 1].entry.id : void 0;
2459
+ if (latestId) await contextProvider.markInboxSeen(name, latestId);
2460
+ const runContext = {
2461
+ name,
2462
+ agent,
2463
+ inbox,
2464
+ recentChannel: await contextProvider.readChannel({
2465
+ limit: LOOP_DEFAULTS.recentChannelLimit,
2466
+ agent: name
2467
+ }),
2468
+ documentContent: await contextProvider.readDocument(),
2469
+ mcpUrl,
2470
+ workspaceDir,
2471
+ projectDir,
2472
+ retryAttempt: 1,
2473
+ provider: contextProvider,
2474
+ eventLog,
2475
+ feedback
2476
+ };
2477
+ infoLog(`Direct send (${message.length} chars)`);
2478
+ const result = await runAgent(backend, runContext, log, infoLog);
2479
+ if (result.success) {
2480
+ if (result.content) await contextProvider.appendChannel(name, result.content);
2481
+ if (latestId) await contextProvider.ackInbox(name, latestId);
2482
+ lastActivityTime = Date.now();
2483
+ }
2484
+ return result;
2485
+ } finally {
2486
+ directRunning = false;
2487
+ state = prevState === "stopped" ? "stopped" : "idle";
2488
+ await contextProvider.setAgentStatus(name, { state }).catch(() => {});
2489
+ }
2490
+ }
2491
+ };
2492
+ }
2493
+ /**
2494
+ * Run an agent: build prompt, configure workspace, call backend.send()
2495
+ *
2496
+ * This is the single orchestration function that the loop calls.
2497
+ * All the "how to run an agent" logic lives here — backends just send().
2498
+ *
2499
+ * SDK and mock backends get special runners with MCP tool bridge + bash,
2500
+ * because they can't manage tools on their own (unlike CLI backends).
2501
+ */
2502
+ async function runAgent(backend, ctx, log, infoLog) {
2503
+ const info = infoLog ?? log;
2504
+ if (backend.type === "mock") return runMockAgent(ctx, (msg) => log(msg));
2505
+ if (backend.type === "default") return runSdkAgent(ctx, (msg) => log(msg));
2506
+ const startTime = Date.now();
2507
+ try {
2508
+ const mcpConfig = generateWorkflowMCPConfig(ctx.mcpUrl, ctx.name);
2509
+ writeBackendMcpConfig(backend.type, ctx.workspaceDir, mcpConfig);
2510
+ const prompt = buildAgentPrompt(ctx);
2511
+ info(`Prompt (${prompt.length} chars) → ${backend.type} backend`);
2512
+ const response = await backend.send(prompt, { system: ctx.agent.resolvedSystemPrompt });
2513
+ return {
2514
+ success: true,
2515
+ duration: Date.now() - startTime,
2516
+ content: response.content
2517
+ };
2518
+ } catch (error) {
2519
+ return {
2520
+ success: false,
2521
+ error: error instanceof Error ? error.message : String(error),
2522
+ duration: Date.now() - startTime
2523
+ };
2524
+ }
2525
+ }
2526
+ /**
2527
+ * Sleep helper
2528
+ */
2529
+ function sleep(ms) {
2530
+ return new Promise((resolve) => setTimeout(resolve, ms));
2531
+ }
2532
+ /**
2533
+ * Check if workflow is complete (all agents idle, no pending work)
2534
+ */
2535
+ async function checkWorkflowIdle(loops, provider, debounceMs = LOOP_DEFAULTS.idleDebounceMs) {
2536
+ if (![...loops.values()].every((c) => c.state === "idle")) return false;
2537
+ for (const [name] of loops) if ((await provider.getInbox(name)).length > 0) return false;
2538
+ await sleep(debounceMs);
2539
+ return [...loops.values()].every((c) => c.state === "idle");
2540
+ }
1295
2541
 
1296
2542
  //#endregion
1297
- //#region src/workflow/context/file-provider.ts
2543
+ //#region src/workflow/loop/backend.ts
1298
2544
  /**
1299
- * File Context Provider
1300
- * Thin wrapper around ContextProviderImpl + FileStorage.
1301
- * Includes instance lock to prevent concurrent access to the same context directory.
2545
+ * Get backend by explicit backend type
2546
+ *
2547
+ * All backends are created via the canonical createBackend() factory
2548
+ * from backends/index.ts. Mock backend is handled specially (no model needed).
1302
2549
  */
1303
- var file_provider_exports = /* @__PURE__ */ __exportAll({
1304
- FileContextProvider: () => FileContextProvider,
1305
- createFileContextProvider: () => createFileContextProvider,
1306
- getDefaultContextDir: () => getDefaultContextDir,
1307
- resolveContextDir: () => resolveContextDir
2550
+ function getBackendByType(backendType, options) {
2551
+ if (backendType === "mock") return createMockBackend(options?.debugLog);
2552
+ const backendOptions = {};
2553
+ if (options?.timeout) backendOptions.timeout = options.timeout;
2554
+ if (options?.streamCallbacks) backendOptions.streamCallbacks = options.streamCallbacks;
2555
+ if (options?.workspace) backendOptions.workspace = options.workspace;
2556
+ return createBackend({
2557
+ type: backendType,
2558
+ model: options?.model,
2559
+ ...backendType === "default" && options?.provider ? { provider: options.provider } : {},
2560
+ ...Object.keys(backendOptions).length > 0 ? { options: backendOptions } : {}
2561
+ });
2562
+ }
2563
+ /**
2564
+ * Get appropriate backend for a model identifier
2565
+ *
2566
+ * Infers backend type from model name and delegates to getBackendByType.
2567
+ * Prefer using getBackendByType with explicit backend field in workflow configs.
2568
+ */
2569
+ function getBackendForModel(model, options) {
2570
+ if (options?.provider) return getBackendByType("default", {
2571
+ ...options,
2572
+ model
2573
+ });
2574
+ const { provider } = parseModel(model);
2575
+ if (provider === "claude") return getBackendByType("claude", {
2576
+ ...options,
2577
+ model
2578
+ });
2579
+ if (provider === "codex") return getBackendByType("codex", {
2580
+ ...options,
2581
+ model
2582
+ });
2583
+ return getBackendByType("default", {
2584
+ ...options,
2585
+ model
2586
+ });
2587
+ }
2588
+
2589
+ //#endregion
2590
+ //#region src/workflow/logger.ts
2591
+ var logger_exports = /* @__PURE__ */ __exportAll({
2592
+ createChannelLogger: () => createChannelLogger,
2593
+ createSilentLogger: () => createSilentLogger
1308
2594
  });
1309
- /** Lock file name within context directory */
1310
- const LOCK_FILE = "_state/instance.lock";
1311
2595
  /**
1312
- * File-based ContextProvider.
1313
- * All domain logic is in ContextProviderImpl;
1314
- * FileStorage handles I/O.
2596
+ * Create a silent logger (no output)
2597
+ */
2598
+ function createSilentLogger() {
2599
+ const noop = () => {};
2600
+ return {
2601
+ debug: noop,
2602
+ info: noop,
2603
+ warn: noop,
2604
+ error: noop,
2605
+ isDebug: () => false,
2606
+ child: () => createSilentLogger()
2607
+ };
2608
+ }
2609
+ /**
2610
+ * Create a logger that writes to the channel.
1315
2611
  *
1316
- * Adds instance locking: only one process can hold the lock at a time.
1317
- * Stale locks (from crashed processes) are automatically cleaned up.
2612
+ * - info/warn/error channel entry with kind="system" (always shown to user)
2613
+ * - debug channel entry with kind="debug" (only shown with --debug)
2614
+ *
2615
+ * The display layer handles formatting and filtering.
1318
2616
  */
1319
- var FileContextProvider = class extends ContextProviderImpl {
1320
- lockPath;
1321
- constructor(storage, validAgents, contextDir) {
1322
- super(storage, validAgents);
1323
- this.contextDir = contextDir;
1324
- this.lockPath = join(contextDir, LOCK_FILE);
1325
- }
1326
- /**
1327
- * Acquire instance lock.
1328
- * Throws if another live process holds the lock.
1329
- * Automatically cleans up stale locks from dead processes.
1330
- */
1331
- acquireLock() {
1332
- if (existsSync(this.lockPath)) try {
1333
- const existing = JSON.parse(readFileSync(this.lockPath, "utf-8"));
1334
- try {
1335
- process.kill(existing.pid, 0);
1336
- throw new Error(`Context directory is locked by another process (PID ${existing.pid}, started ${existing.startedAt}). If the process is no longer running, delete ${this.lockPath}`);
1337
- } catch (e) {
1338
- if (e instanceof Error && e.message.includes("Context directory is locked")) throw e;
1339
- }
1340
- } catch (e) {
1341
- if (e instanceof Error && e.message.includes("Context directory is locked")) throw e;
2617
+ function createChannelLogger(config) {
2618
+ const { provider, from = "system" } = config;
2619
+ const formatContent = (level, message, args) => {
2620
+ const argsStr = args.length > 0 ? " " + args.map(formatArg).join(" ") : "";
2621
+ if (level === "warn") return `[WARN] ${message}${argsStr}`;
2622
+ if (level === "error") return `[ERROR] ${message}${argsStr}`;
2623
+ return `${message}${argsStr}`;
2624
+ };
2625
+ const write = (level, message, args) => {
2626
+ const content = formatContent(level, message, args);
2627
+ const kind = level === "debug" ? "debug" : "system";
2628
+ provider.appendChannel(from, content, { kind }).catch(() => {});
2629
+ };
2630
+ return {
2631
+ debug: (message, ...args) => write("debug", message, args),
2632
+ info: (message, ...args) => write("info", message, args),
2633
+ warn: (message, ...args) => write("warn", message, args),
2634
+ error: (message, ...args) => write("error", message, args),
2635
+ isDebug: () => true,
2636
+ child: (childPrefix) => {
2637
+ return createChannelLogger({
2638
+ provider,
2639
+ from: from ? `${from}:${childPrefix}` : childPrefix
2640
+ });
1342
2641
  }
1343
- const lock = {
1344
- pid: process.pid,
1345
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
1346
- };
1347
- const stateDir = join(this.contextDir, "_state");
1348
- if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
1349
- writeFileSync(this.lockPath, JSON.stringify(lock, null, 2));
1350
- }
1351
- /**
1352
- * Release instance lock.
1353
- * Safe to call even if lock is not held (no-op).
1354
- */
1355
- releaseLock() {
1356
- try {
1357
- if (existsSync(this.lockPath)) {
1358
- if (JSON.parse(readFileSync(this.lockPath, "utf-8")).pid === process.pid) unlinkSync(this.lockPath);
1359
- }
1360
- } catch {}
1361
- }
1362
- /**
1363
- * Override destroy to release lock and clean up transient state.
1364
- */
1365
- async destroy() {
1366
- await super.destroy();
1367
- this.releaseLock();
2642
+ };
2643
+ }
2644
+ /** Format an argument for logging */
2645
+ function formatArg(arg) {
2646
+ if (arg === null || arg === void 0) return String(arg);
2647
+ if (typeof arg === "object") try {
2648
+ return JSON.stringify(arg);
2649
+ } catch {
2650
+ return String(arg);
1368
2651
  }
1369
- };
2652
+ return String(arg);
2653
+ }
2654
+
2655
+ //#endregion
2656
+ //#region src/workflow/factory.ts
1370
2657
  /**
1371
- * Resolve a context directory template to an absolute path.
2658
+ * Workflow Factory Composable primitives for building workflow runtimes.
1372
2659
  *
1373
- * Supports:
1374
- * - ${{ workflow.name }} substituted with workflowName
1375
- * - ${{ instance }} — substituted with instance
1376
- * - ~ expansion to home directory
1377
- * - Relative paths resolved against baseDir (or cwd if not provided)
1378
- * - Absolute paths used as-is
2660
+ * These functions are the building blocks that both runner.ts (CLI direct)
2661
+ * and daemon.ts (service) use to create workflow infrastructure.
2662
+ *
2663
+ * Extracted from the monolithic runWorkflowWithLoops() so that
2664
+ * the daemon can create and manage workflow components independently.
2665
+ *
2666
+ * Usage:
2667
+ * 1. createMinimalRuntime() — context + MCP + event log (the "workspace")
2668
+ * 2. createWiredLoop() — backend + workspace dir + loop (per agent)
2669
+ * 3. Caller manages lifecycle — start/stop loops, send kickoff, shutdown
1379
2670
  */
1380
- function resolveContextDir(dirTemplate, opts) {
1381
- const workflow = opts.workflow ?? opts.workflowName ?? opts.instance ?? "global";
1382
- const workflowName = opts.workflowName ?? workflow;
1383
- const tag = opts.tag ?? "main";
1384
- let dir = dirTemplate.replace("${{ workflow.name }}", workflowName).replace("${{ workflow.tag }}", tag).replace("${{ instance }}", opts.instance ?? workflow);
1385
- if (dir.startsWith("~/")) dir = join(homedir(), dir.slice(2));
1386
- else if (dir === "~") dir = homedir();
1387
- else if (!isAbsolute(dir)) dir = join(opts.baseDir ?? process.cwd(), dir);
1388
- return dir;
1389
- }
1390
2671
  /**
1391
- * Resolve context dir for a workflow:tag using default template.
1392
- * Shorthand for the common case.
1393
- * @param workflow Workflow name (defaults to "global")
1394
- * @param tag Workflow instance tag (defaults to "main")
1395
- * @param instanceOrWorkflowName (deprecated) Legacy parameter for backward compatibility
2672
+ * Create a minimal workflow runtime.
2673
+ *
2674
+ * Sets up the shared infrastructure (context + MCP + event log) without
2675
+ * creating loops or backends. The daemon can use this to create
2676
+ * workflow infrastructure for both standalone and multi-agent workflows.
2677
+ *
2678
+ * For standalone agents created via `POST /agents`, this gives them
2679
+ * the same context infrastructure that workflow agents get.
1396
2680
  */
1397
- function getDefaultContextDir(workflow, tag, instanceOrWorkflowName) {
1398
- const wf = workflow ?? instanceOrWorkflowName ?? "global";
1399
- const t = tag ?? "main";
1400
- return resolveContextDir(CONTEXT_DEFAULTS.dir, {
1401
- workflow: wf,
1402
- tag: t
2681
+ async function createMinimalRuntime(config) {
2682
+ const { workflowName, tag, agentNames, onMention, feedback: feedbackEnabled, debugLog } = config;
2683
+ let contextProvider;
2684
+ let contextDir;
2685
+ let persistent = false;
2686
+ if (config.contextProvider && config.contextDir) {
2687
+ contextProvider = config.contextProvider;
2688
+ contextDir = config.contextDir;
2689
+ persistent = config.persistent ?? false;
2690
+ } else {
2691
+ contextDir = getDefaultContextDir(workflowName, tag);
2692
+ if (!existsSync(contextDir)) mkdirSync(contextDir, { recursive: true });
2693
+ contextProvider = createFileContextProvider(contextDir, agentNames);
2694
+ persistent = false;
2695
+ }
2696
+ await contextProvider.markRunStart();
2697
+ const projectDir = process.cwd();
2698
+ let mcpGetFeedback;
2699
+ let mcpToolNames = /* @__PURE__ */ new Set();
2700
+ const eventLog = new EventLog(contextProvider);
2701
+ const createMCPServerInstance = () => {
2702
+ const mcp = createContextMCPServer({
2703
+ provider: contextProvider,
2704
+ validAgents: agentNames,
2705
+ name: `${workflowName}-context`,
2706
+ version: "1.0.0",
2707
+ onMention,
2708
+ feedback: feedbackEnabled,
2709
+ debugLog
2710
+ });
2711
+ mcpGetFeedback = mcp.getFeedback;
2712
+ mcpToolNames = mcp.mcpToolNames;
2713
+ return mcp.server;
2714
+ };
2715
+ const httpMcpServer = await runWithHttp({
2716
+ createServerInstance: createMCPServerInstance,
2717
+ port: 0
1403
2718
  });
2719
+ const shutdown = async () => {
2720
+ if (persistent) {
2721
+ if (contextProvider instanceof FileContextProvider) contextProvider.releaseLock();
2722
+ } else await contextProvider.destroy();
2723
+ await httpMcpServer.close();
2724
+ };
2725
+ return {
2726
+ contextProvider,
2727
+ contextDir,
2728
+ persistent,
2729
+ eventLog,
2730
+ httpMcpServer,
2731
+ mcpUrl: httpMcpServer.url,
2732
+ mcpToolNames,
2733
+ projectDir,
2734
+ getFeedback: mcpGetFeedback,
2735
+ shutdown
2736
+ };
1404
2737
  }
1405
2738
  /**
1406
- * Create a FileContextProvider with default paths.
2739
+ * Create a fully-wired agent loop.
1407
2740
  *
1408
- * Directory layout:
1409
- * contextDir/
1410
- * ├── channel.jsonl # Channel log (JSONL)
1411
- * ├── documents/ # Team documents
1412
- * │ └── notes.md # Default document
1413
- * ├── resources/ # Resource blobs
1414
- * ├── _state/
1415
- * │ ├── inbox.json # Inbox read cursors
1416
- * │ ├── instance.lock # Instance lock (PID-based)
1417
- * │ └── proposals.json # Proposal state
1418
- * └── ...
2741
+ * This handles the full setup:
2742
+ * 1. Create backend from agent definition (or use custom factory)
2743
+ * 2. Create isolated workspace directory
2744
+ * 3. Configure stream callbacks for structured event logging
2745
+ * 4. Create the AgentLoop with all wiring
2746
+ *
2747
+ * Extracted from runWorkflowWithLoops() so both runner.ts and
2748
+ * daemon.ts can create loops with the same quality.
1419
2749
  */
1420
- function createFileContextProvider(contextDir, validAgents) {
1421
- return new FileContextProvider(new FileStorage(contextDir), validAgents, contextDir);
2750
+ function createWiredLoop(config) {
2751
+ const { name, agent, runtime, pollInterval, feedback: feedbackEnabled } = config;
2752
+ const logger = config.logger ?? createSilentLogger();
2753
+ const workspaceDir = join(runtime.contextDir, "workspaces", name);
2754
+ if (!existsSync(workspaceDir)) mkdirSync(workspaceDir, { recursive: true });
2755
+ const streamCallbacks = {
2756
+ debugLog: (msg) => logger.debug(msg),
2757
+ outputLog: (msg) => runtime.eventLog.output(name, msg),
2758
+ toolCallLog: (toolName, args) => runtime.eventLog.toolCall(name, toolName, args, "backend"),
2759
+ mcpToolNames: runtime.mcpToolNames
2760
+ };
2761
+ let effectiveModel;
2762
+ let effectiveProvider = agent.provider;
2763
+ if (isAutoProvider(agent.model) || isAutoProvider(agent.provider)) {
2764
+ const resolved = resolveModelFallback({
2765
+ model: agent.model,
2766
+ provider: typeof agent.provider === "string" ? agent.provider : void 0
2767
+ });
2768
+ effectiveModel = resolved.model;
2769
+ effectiveProvider = resolved.provider;
2770
+ logger.info(`Model resolved: ${effectiveModel}`);
2771
+ } else effectiveModel = agent.model;
2772
+ let backend;
2773
+ if (config.createBackend) backend = config.createBackend(name, agent);
2774
+ else if (agent.backend) backend = getBackendByType(agent.backend, {
2775
+ model: effectiveModel,
2776
+ provider: effectiveProvider,
2777
+ debugLog: (msg) => logger.debug(msg),
2778
+ streamCallbacks,
2779
+ timeout: agent.timeout,
2780
+ workspace: workspaceDir
2781
+ });
2782
+ else if (effectiveModel) backend = getBackendForModel(effectiveModel, {
2783
+ provider: effectiveProvider,
2784
+ debugLog: (msg) => logger.debug(msg),
2785
+ streamCallbacks,
2786
+ workspace: workspaceDir
2787
+ });
2788
+ else throw new Error(`Agent "${name}" requires either a backend or model field`);
2789
+ return {
2790
+ loop: createAgentLoop({
2791
+ name,
2792
+ agent: effectiveModel !== agent.model || effectiveProvider !== agent.provider ? {
2793
+ ...agent,
2794
+ model: effectiveModel,
2795
+ provider: effectiveProvider
2796
+ } : agent,
2797
+ contextProvider: runtime.contextProvider,
2798
+ eventLog: runtime.eventLog,
2799
+ mcpUrl: runtime.mcpUrl,
2800
+ workspaceDir,
2801
+ projectDir: runtime.projectDir,
2802
+ backend,
2803
+ pollInterval,
2804
+ log: (msg) => logger.debug(msg),
2805
+ infoLog: (msg) => logger.info(msg),
2806
+ errorLog: (msg) => logger.error(msg),
2807
+ feedback: feedbackEnabled
2808
+ }),
2809
+ backend
2810
+ };
1422
2811
  }
1423
2812
 
1424
2813
  //#endregion
@@ -1426,14 +2815,18 @@ function createFileContextProvider(contextDir, validAgents) {
1426
2815
  /**
1427
2816
  * Daemon — Centralized agent coordinator.
1428
2817
  *
2818
+ * Architecture: Interface → Daemon → Loop (three layers)
2819
+ * Interface: CLI/REST/MCP clients talk to daemon via HTTP
2820
+ * Daemon: This module — owns lifecycle, creates workflows + loops
2821
+ * Loop: AgentLoop + Backend — executes agent reasoning
2822
+ *
1429
2823
  * Data ownership:
1430
- * Registry (configs) — what agents exist and their configuration
1431
- * StateStore (store) conversation history and usage (pluggable)
1432
- * WorkerHandle (workers) — execution, local or remote
1433
- * Workflows (workflows) — running workflow instances with controllers
2824
+ * Registry (configs) — what agents exist and their configuration
2825
+ * Workflows (workflows) running workflow instances with loops + context
1434
2826
  *
1435
- * The daemon is pure glue: receive request lookup config
1436
- * dispatch to worker persist state return response.
2827
+ * Key principle: every agent lives in a workflow. Standalone agents created via
2828
+ * POST /agents get a 1-agent workflow (created lazily on first /run or /serve).
2829
+ * This unifies the runtime so there's one code path for execution.
1437
2830
  *
1438
2831
  * HTTP endpoints:
1439
2832
  * GET /health, POST /shutdown
@@ -1457,9 +2850,6 @@ async function gracefulShutdown() {
1457
2850
  await wf.shutdown();
1458
2851
  } catch {}
1459
2852
  state.workflows.clear();
1460
- for (const [name, handle] of state.workers) try {
1461
- await state.store.save(name, handle.getState());
1462
- } catch {}
1463
2853
  if (state.server) await state.server.close();
1464
2854
  }
1465
2855
  for (const [, session] of mcpSessions) try {
@@ -1477,11 +2867,93 @@ async function parseJsonBody(c) {
1477
2867
  return null;
1478
2868
  }
1479
2869
  }
1480
- function createDaemonApp(optionsOrGetState) {
1481
- const { getState, token } = typeof optionsOrGetState === "function" ? {
1482
- getState: optionsOrGetState,
1483
- token: void 0
1484
- } : optionsOrGetState;
2870
+ /** Map AgentConfig to the ResolvedAgent type needed by the factory */
2871
+ function configToResolvedAgent(cfg) {
2872
+ return {
2873
+ backend: cfg.backend,
2874
+ model: cfg.model,
2875
+ provider: cfg.provider,
2876
+ resolvedSystemPrompt: cfg.system,
2877
+ schedule: cfg.schedule
2878
+ };
2879
+ }
2880
+ /**
2881
+ * Find an agent's loop across all workflows.
2882
+ * Returns the loop if the agent exists in any workflow.
2883
+ */
2884
+ function findLoop(s, agentName) {
2885
+ for (const wf of s.workflows.values()) {
2886
+ const l = wf.loops.get(agentName);
2887
+ if (l) return {
2888
+ loop: l,
2889
+ workflow: wf
2890
+ };
2891
+ }
2892
+ return null;
2893
+ }
2894
+ /**
2895
+ * Ensure a standalone agent has a workflow + loop.
2896
+ * Creates the infrastructure lazily on first call (starts MCP server, etc.).
2897
+ *
2898
+ * This is the bridge between POST /agents (stores config only) and
2899
+ * POST /run or /serve (needs a loop to execute).
2900
+ */
2901
+ async function ensureAgentLoop(s, agentName) {
2902
+ const existing = findLoop(s, agentName);
2903
+ if (existing) return existing;
2904
+ const cfg = s.configs.get(agentName);
2905
+ if (!cfg) throw new Error(`Agent not found: ${agentName}`);
2906
+ const agentDef = configToResolvedAgent(cfg);
2907
+ const wfKey = `standalone:${agentName}`;
2908
+ const runtime = await createMinimalRuntime({
2909
+ workflowName: cfg.workflow,
2910
+ tag: cfg.tag,
2911
+ agentNames: [agentName]
2912
+ });
2913
+ let loop;
2914
+ try {
2915
+ ({loop} = createWiredLoop({
2916
+ name: agentName,
2917
+ agent: agentDef,
2918
+ runtime
2919
+ }));
2920
+ } catch (err) {
2921
+ await runtime.shutdown();
2922
+ throw err;
2923
+ }
2924
+ const handle = {
2925
+ name: cfg.workflow,
2926
+ tag: cfg.tag,
2927
+ key: wfKey,
2928
+ agents: [agentName],
2929
+ loops: new Map([[agentName, loop]]),
2930
+ contextProvider: runtime.contextProvider,
2931
+ shutdown: async () => {
2932
+ try {
2933
+ await loop.stop();
2934
+ } finally {
2935
+ await runtime.shutdown();
2936
+ }
2937
+ },
2938
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2939
+ };
2940
+ s.workflows.set(wfKey, handle);
2941
+ return {
2942
+ loop,
2943
+ workflow: handle
2944
+ };
2945
+ }
2946
+ /**
2947
+ * Create the Hono app with all daemon routes.
2948
+ *
2949
+ * Accepts a state getter so the app can be used both in production
2950
+ * (module-level state set by startDaemon) and in tests (injected state).
2951
+ *
2952
+ * When a token is provided, all endpoints require `Authorization: Bearer <token>`.
2953
+ * This prevents cross-origin attacks from malicious websites.
2954
+ */
2955
+ function createDaemonApp(options) {
2956
+ const { getState, token } = options;
1485
2957
  const app = new Hono();
1486
2958
  if (token) app.use("*", async (c, next) => {
1487
2959
  if (c.req.header("authorization") !== `Bearer ${token}`) return c.json({ error: "Unauthorized" }, 401);
@@ -1517,17 +2989,21 @@ function createDaemonApp(optionsOrGetState) {
1517
2989
  app.get("/agents", (c) => {
1518
2990
  const s = getState();
1519
2991
  if (!s) return c.json({ error: "Not ready" }, 503);
1520
- const standaloneAgents = [...s.configs.values()].map((cfg) => ({
1521
- name: cfg.name,
1522
- model: cfg.model,
1523
- backend: cfg.backend,
1524
- workflow: cfg.workflow,
1525
- tag: cfg.tag,
1526
- createdAt: cfg.createdAt,
1527
- source: "standalone"
1528
- }));
2992
+ const standaloneAgents = [...s.configs.values()].map((cfg) => {
2993
+ const found = findLoop(s, cfg.name);
2994
+ return {
2995
+ name: cfg.name,
2996
+ model: cfg.model,
2997
+ backend: cfg.backend,
2998
+ workflow: cfg.workflow,
2999
+ tag: cfg.tag,
3000
+ createdAt: cfg.createdAt,
3001
+ source: "standalone",
3002
+ state: found?.loop.state
3003
+ };
3004
+ });
1529
3005
  const workflowAgents = [...s.workflows.values()].flatMap((wf) => wf.agents.map((agentName) => {
1530
- const controller = wf.controllers.get(agentName);
3006
+ const loop = wf.loops.get(agentName);
1531
3007
  return {
1532
3008
  name: agentName,
1533
3009
  model: "",
@@ -1536,7 +3012,7 @@ function createDaemonApp(optionsOrGetState) {
1536
3012
  tag: wf.tag,
1537
3013
  createdAt: wf.startedAt,
1538
3014
  source: "workflow",
1539
- state: controller?.state ?? "unknown"
3015
+ state: loop?.state ?? "unknown"
1540
3016
  };
1541
3017
  }));
1542
3018
  return c.json({ agents: [...standaloneAgents, ...workflowAgents] });
@@ -1546,7 +3022,7 @@ function createDaemonApp(optionsOrGetState) {
1546
3022
  if (!s) return c.json({ error: "Not ready" }, 503);
1547
3023
  const body = await parseJsonBody(c);
1548
3024
  if (!body || typeof body !== "object") return c.json({ error: "Invalid JSON body" }, 400);
1549
- const { name, model, system, backend = "default", provider, workflow = "global", tag = "main" } = body;
3025
+ const { name, model, system, backend = "default", provider, workflow = "global", tag = "main", schedule } = body;
1550
3026
  if (!name || !model || !system) return c.json({ error: "name, model, system required" }, 400);
1551
3027
  if (s.configs.has(name)) return c.json({ error: `Agent already exists: ${name}` }, 409);
1552
3028
  const agentConfig = {
@@ -1557,17 +3033,17 @@ function createDaemonApp(optionsOrGetState) {
1557
3033
  provider,
1558
3034
  workflow,
1559
3035
  tag,
1560
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
3036
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3037
+ schedule
1561
3038
  };
1562
- const handle = new LocalWorker(agentConfig, await s.store.load(name) ?? void 0);
1563
3039
  s.configs.set(name, agentConfig);
1564
- s.workers.set(name, handle);
1565
3040
  return c.json({
1566
3041
  name,
1567
3042
  model,
1568
3043
  backend,
1569
3044
  workflow,
1570
- tag
3045
+ tag,
3046
+ schedule
1571
3047
  }, 201);
1572
3048
  });
1573
3049
  app.get("/agents/:name", (c) => {
@@ -1582,7 +3058,8 @@ function createDaemonApp(optionsOrGetState) {
1582
3058
  system: cfg.system,
1583
3059
  workflow: cfg.workflow,
1584
3060
  tag: cfg.tag,
1585
- createdAt: cfg.createdAt
3061
+ createdAt: cfg.createdAt,
3062
+ schedule: cfg.schedule
1586
3063
  });
1587
3064
  });
1588
3065
  app.delete("/agents/:name", async (c) => {
@@ -1590,11 +3067,14 @@ function createDaemonApp(optionsOrGetState) {
1590
3067
  if (!s) return c.json({ error: "Not ready" }, 503);
1591
3068
  const name = c.req.param("name");
1592
3069
  if (!s.configs.delete(name)) return c.json({ error: "Agent not found" }, 404);
1593
- const handle = s.workers.get(name);
1594
- if (handle) try {
1595
- await s.store.save(name, handle.getState());
1596
- } catch {}
1597
- s.workers.delete(name);
3070
+ const wfKey = `standalone:${name}`;
3071
+ const wf = s.workflows.get(wfKey);
3072
+ if (wf) {
3073
+ try {
3074
+ await wf.shutdown();
3075
+ } catch {}
3076
+ s.workflows.delete(wfKey);
3077
+ }
1598
3078
  return c.json({ success: true });
1599
3079
  });
1600
3080
  app.post("/run", async (c) => {
@@ -1604,30 +3084,36 @@ function createDaemonApp(optionsOrGetState) {
1604
3084
  if (!body || typeof body !== "object") return c.json({ error: "Invalid JSON body" }, 400);
1605
3085
  const { agent: agentName, message } = body;
1606
3086
  if (!agentName || !message) return c.json({ error: "agent and message required" }, 400);
1607
- const handle = s.workers.get(agentName);
1608
- if (!handle) return c.json({ error: `Agent not found: ${agentName}` }, 404);
3087
+ let loop;
3088
+ const loopResult = findLoop(s, agentName);
3089
+ if (loopResult) loop = loopResult.loop;
3090
+ else if (s.configs.has(agentName)) try {
3091
+ loop = (await ensureAgentLoop(s, agentName)).loop;
3092
+ } catch (error) {
3093
+ const msg = error instanceof Error ? error.message : String(error);
3094
+ return c.json({ error: `Failed to create agent runtime: ${msg}` }, 500);
3095
+ }
3096
+ if (!loop) return c.json({ error: `Agent not found: ${agentName}` }, 404);
3097
+ const agentLoop = loop;
1609
3098
  return streamSSE(c, async (stream) => {
1610
3099
  try {
1611
- const gen = handle.sendStream(message);
1612
- while (true) {
1613
- const { value, done } = await gen.next();
1614
- if (done) {
1615
- const currentState = getState();
1616
- if (currentState) await currentState.store.save(agentName, handle.getState());
1617
- await stream.writeSSE({
1618
- event: "done",
1619
- data: JSON.stringify(value)
1620
- });
1621
- break;
1622
- }
1623
- await stream.writeSSE({
3100
+ const result = await agentLoop.sendDirect(message);
3101
+ if (result.success) {
3102
+ if (result.content) await stream.writeSSE({
1624
3103
  event: "chunk",
1625
3104
  data: JSON.stringify({
1626
3105
  agent: agentName,
1627
- text: value
3106
+ text: result.content
1628
3107
  })
1629
3108
  });
1630
- }
3109
+ await stream.writeSSE({
3110
+ event: "done",
3111
+ data: JSON.stringify(result)
3112
+ });
3113
+ } else await stream.writeSSE({
3114
+ event: "error",
3115
+ data: JSON.stringify({ error: result.error })
3116
+ });
1631
3117
  } catch (error) {
1632
3118
  const msg = error instanceof Error ? error.message : String(error);
1633
3119
  await stream.writeSSE({
@@ -1644,12 +3130,24 @@ function createDaemonApp(optionsOrGetState) {
1644
3130
  if (!body || typeof body !== "object") return c.json({ error: "Invalid JSON body" }, 400);
1645
3131
  const { agent: agentName, message } = body;
1646
3132
  if (!agentName || !message) return c.json({ error: "agent and message required" }, 400);
1647
- const handle = s.workers.get(agentName);
1648
- if (!handle) return c.json({ error: `Agent not found: ${agentName}` }, 404);
3133
+ let loop;
3134
+ const loopResult = findLoop(s, agentName);
3135
+ if (loopResult) loop = loopResult.loop;
3136
+ else if (s.configs.has(agentName)) try {
3137
+ loop = (await ensureAgentLoop(s, agentName)).loop;
3138
+ } catch (error) {
3139
+ const msg = error instanceof Error ? error.message : String(error);
3140
+ return c.json({ error: msg }, 500);
3141
+ }
3142
+ if (!loop) return c.json({ error: `Agent not found: ${agentName}` }, 404);
1649
3143
  try {
1650
- const response = await handle.send(message);
1651
- await s.store.save(agentName, handle.getState());
1652
- return c.json(response);
3144
+ const result = await loop.sendDirect(message);
3145
+ if (!result.success) return c.json({ error: result.error }, 500);
3146
+ return c.json({
3147
+ content: result.content ?? "",
3148
+ duration: result.duration,
3149
+ success: true
3150
+ });
1653
3151
  } catch (error) {
1654
3152
  const msg = error instanceof Error ? error.message : String(error);
1655
3153
  return c.json({ error: msg }, 500);
@@ -1676,15 +3174,18 @@ function createDaemonApp(optionsOrGetState) {
1676
3174
  const agentCfg = s.configs.get(agentName);
1677
3175
  const workflow = agentCfg?.workflow ?? "global";
1678
3176
  const tag = agentCfg?.tag ?? "main";
3177
+ const existingWf = findLoop(s, agentName)?.workflow ?? s.workflows.get(`${workflow}:${tag}`);
1679
3178
  const workflowAgents = getWorkflowAgentNames(workflow, tag);
1680
3179
  const allNames = [...new Set([
1681
3180
  ...workflowAgents,
1682
3181
  agentName,
1683
3182
  "user"
1684
3183
  ])];
1685
- const contextDir = getDefaultContextDir(workflow, tag);
1686
- mkdirSync(contextDir, { recursive: true });
1687
- const provider = createFileContextProvider(contextDir, allNames);
3184
+ const provider = existingWf?.contextProvider ?? (() => {
3185
+ const contextDir = getDefaultContextDir(workflow, tag);
3186
+ mkdirSync(contextDir, { recursive: true });
3187
+ return createFileContextProvider(contextDir, allNames);
3188
+ })();
1688
3189
  const transport = new WebStandardStreamableHTTPServerTransport({
1689
3190
  sessionIdGenerator: () => `${agentName}-${randomUUID().slice(0, 8)}`,
1690
3191
  onsessioninitialized: (sid) => {
@@ -1714,14 +3215,14 @@ function createDaemonApp(optionsOrGetState) {
1714
3215
  if (!s) return c.json({ error: "Not ready" }, 503);
1715
3216
  const body = await parseJsonBody(c);
1716
3217
  if (!body || typeof body !== "object") return c.json({ error: "Invalid JSON body" }, 400);
1717
- const { workflow, tag = "main", feedback, pollInterval } = body;
3218
+ const { workflow, tag = "main", feedback, pollInterval, params } = body;
1718
3219
  if (!workflow || !workflow.agents) return c.json({ error: "workflow (parsed YAML) required" }, 400);
1719
3220
  const workflowName = workflow.name || "global";
1720
3221
  const key = `${workflowName}:${tag}`;
1721
3222
  if (s.workflows.has(key)) return c.json({ error: `Workflow already running: ${key}` }, 409);
1722
3223
  try {
1723
- const { runWorkflowWithControllers } = await import("../runner-CnxROIev.mjs");
1724
- const result = await runWorkflowWithControllers({
3224
+ const { runWorkflowWithLoops } = await import("../runner-DB-b57iZ.mjs");
3225
+ const result = await runWorkflowWithLoops({
1725
3226
  workflow,
1726
3227
  workflowName,
1727
3228
  tag,
@@ -1729,6 +3230,7 @@ function createDaemonApp(optionsOrGetState) {
1729
3230
  headless: true,
1730
3231
  feedback,
1731
3232
  pollInterval,
3233
+ params,
1732
3234
  log: () => {}
1733
3235
  });
1734
3236
  if (!result.success) return c.json({ error: result.error || "Workflow failed to start" }, 500);
@@ -1737,7 +3239,7 @@ function createDaemonApp(optionsOrGetState) {
1737
3239
  tag,
1738
3240
  key,
1739
3241
  agents: Object.keys(workflow.agents),
1740
- controllers: result.controllers,
3242
+ loops: result.loops,
1741
3243
  contextProvider: result.contextProvider,
1742
3244
  shutdown: result.shutdown,
1743
3245
  workflowPath: workflow.filePath,
@@ -1760,7 +3262,7 @@ function createDaemonApp(optionsOrGetState) {
1760
3262
  if (!s) return c.json({ error: "Not ready" }, 503);
1761
3263
  const workflows = [...s.workflows.values()].map((wf) => {
1762
3264
  const agentStates = {};
1763
- for (const [name, controller] of wf.controllers) agentStates[name] = controller.state;
3265
+ for (const [name, loop] of wf.loops) agentStates[name] = loop.state;
1764
3266
  return {
1765
3267
  name: wf.name,
1766
3268
  tag: wf.tag,
@@ -1822,7 +3324,6 @@ async function startDaemon(config = {}) {
1822
3324
  });
1823
3325
  state = {
1824
3326
  configs: /* @__PURE__ */ new Map(),
1825
- workers: /* @__PURE__ */ new Map(),
1826
3327
  workflows: /* @__PURE__ */ new Map(),
1827
3328
  store,
1828
3329
  server,
@@ -2073,11 +3574,12 @@ function registerAgentCommands(program) {
2073
3574
  "cursor",
2074
3575
  "opencode",
2075
3576
  "mock"
2076
- ]).default("default")).option("--provider <name>", "Provider SDK name (e.g., anthropic, openai)").option("--base-url <url>", "Override provider base URL").option("--api-key <ref>", "API key env var (e.g., $MINIMAX_API_KEY)").option("-s, --system <prompt>", "System prompt", "You are a helpful assistant.").option("-f, --system-file <file>", "Read system prompt from file").option("--workflow <name>", "Workflow name (default: global)").option("--tag <tag>", "Workflow instance tag (default: main)").option("--port <port>", `Daemon port if starting new daemon (default: ${DEFAULT_PORT})`).option("--host <host>", "Daemon host (default: 127.0.0.1)").option("--json", "Output as JSON").addHelpText("after", `
3577
+ ]).default("default")).option("--provider <name>", "Provider SDK name (e.g., anthropic, openai)").option("--base-url <url>", "Override provider base URL").option("--api-key <ref>", "API key env var (e.g., $MINIMAX_API_KEY)").option("-s, --system <prompt>", "System prompt", "You are a helpful assistant.").option("-f, --system-file <file>", "Read system prompt from file").option("--workflow <name>", "Workflow name (default: global)").option("--tag <tag>", "Workflow instance tag (default: main)").option("--wakeup <interval|cron>", "Periodic wakeup schedule (e.g., 30s, 5m, 0 9 * * 1-5)").option("--wakeup-prompt <text>", "Custom prompt for wakeup events").option("--port <port>", `Daemon port if starting new daemon (default: ${DEFAULT_PORT})`).option("--host <host>", "Daemon host (default: 127.0.0.1)").option("--json", "Output as JSON").addHelpText("after", `
2077
3578
  Examples:
2078
3579
  $ agent-worker new alice -m anthropic/claude-sonnet-4-5
2079
3580
  $ agent-worker new bot -b mock
2080
3581
  $ agent-worker new reviewer --workflow review --tag pr-123
3582
+ $ agent-worker new monitor --wakeup 30s --system "Check status"
2081
3583
  $ agent-worker new coder -m MiniMax-M2.5 --provider anthropic --base-url https://api.minimax.io/anthropic/v1 --api-key '$MINIMAX_API_KEY'
2082
3584
  `).action(async (name, options) => {
2083
3585
  let system = options.system;
@@ -2091,6 +3593,15 @@ Examples:
2091
3593
  api_key: options.apiKey
2092
3594
  };
2093
3595
  else provider = options.provider;
3596
+ if (options.wakeupPrompt && !options.wakeup) {
3597
+ console.error("Error: --wakeup-prompt can only be used with --wakeup.");
3598
+ process.exit(1);
3599
+ }
3600
+ let schedule;
3601
+ if (options.wakeup) {
3602
+ schedule = { wakeup: options.wakeup };
3603
+ if (options.wakeupPrompt) schedule.prompt = options.wakeupPrompt;
3604
+ }
2094
3605
  await ensureDaemon(options.port ? parseInt(options.port, 10) : void 0, options.host);
2095
3606
  const res = await createAgent({
2096
3607
  name,
@@ -2099,7 +3610,8 @@ Examples:
2099
3610
  backend,
2100
3611
  provider,
2101
3612
  workflow: options.workflow,
2102
- tag: options.tag
3613
+ tag: options.tag,
3614
+ schedule
2103
3615
  });
2104
3616
  if (res.error) {
2105
3617
  console.error("Error:", res.error);
@@ -2343,28 +3855,49 @@ function buildDisplay(agent, workflow, tag) {
2343
3855
  //#endregion
2344
3856
  //#region src/cli/commands/workflow.ts
2345
3857
  function registerWorkflowCommands(program) {
2346
- program.command("run <file>").description("Execute workflow and exit when complete").option("--tag <tag>", "Workflow instance tag (default: main)", DEFAULT_TAG).option("-d, --debug", "Show debug details (internal logs, MCP traces, idle checks)").option("--feedback", "Enable feedback tool (agents can report tool/workflow observations)").option("--json", "Output results as JSON").addHelpText("after", `
3858
+ program.command("run <file>").description("Execute workflow and exit when complete").option("--tag <tag>", "Workflow instance tag (default: main)", DEFAULT_TAG).option("-d, --debug", "Show debug details (internal logs, MCP traces, idle checks)").option("--feedback", "Enable feedback tool (agents can report tool/workflow observations)").option("--json", "Output results as JSON").allowExcessArguments().addHelpText("after", `
2347
3859
  Examples:
2348
3860
  $ agent-worker run review.yaml # Run review:main
2349
3861
  $ agent-worker run review.yaml --tag pr-123 # Run review:pr-123
2350
3862
  $ agent-worker run review.yaml --json | jq .document # Machine-readable output
3863
+ $ agent-worker run review.yaml -- --target main -n 3 # With workflow params
3864
+
3865
+ Remote workflows (github:owner/repo@ref/path):
3866
+ $ agent-worker run github:acme/workflows/review.yml # Default branch
3867
+ $ agent-worker run github:acme/workflows@v1.0/review.yml # Pinned version
3868
+ $ agent-worker run github:acme/workflows#review # Shorthand
3869
+ $ agent-worker run github:acme/workflows#review -- --target main # With params
2351
3870
 
2352
- Note: Workflow name is inferred from YAML 'name' field or filename
3871
+ Note: Workflow name is inferred from YAML 'name' field or filename.
3872
+ Workflow-defined params (see 'params:' in YAML) are passed after '--'.
3873
+ Set GITHUB_TOKEN env var to access private repositories.
2353
3874
  `).action(async (file, options) => {
2354
- const { parseWorkflowFile, runWorkflowWithControllers } = await import("../workflow-CIE3WPNx.mjs");
3875
+ const { parseWorkflowFile, parseWorkflowParams, formatParamHelp, runWorkflowWithLoops } = await import("../workflow-DQ6Eju4n.mjs");
2355
3876
  const tag = options.tag || DEFAULT_TAG;
2356
3877
  const parsedWorkflow = await parseWorkflowFile(file, { tag });
2357
3878
  const workflowName = parsedWorkflow.name;
2358
- let controllers;
3879
+ let params;
3880
+ if (parsedWorkflow.params && parsedWorkflow.params.length > 0) {
3881
+ const extraArgs = getArgsAfterSeparator();
3882
+ try {
3883
+ params = parseWorkflowParams(parsedWorkflow.params, extraArgs);
3884
+ } catch (error) {
3885
+ const msg = error instanceof Error ? error.message : String(error);
3886
+ console.error(`Error: ${msg}`);
3887
+ console.error(formatParamHelp(parsedWorkflow.params));
3888
+ process.exit(1);
3889
+ }
3890
+ }
3891
+ let loops;
2359
3892
  let isCleaningUp = false;
2360
3893
  const cleanup = async () => {
2361
3894
  if (isCleaningUp) return;
2362
3895
  isCleaningUp = true;
2363
3896
  console.log("\nInterrupted, cleaning up...");
2364
- if (controllers) {
2365
- const { shutdownControllers } = await import("../workflow-CIE3WPNx.mjs");
2366
- const { createSilentLogger } = await import("../logger-Bfdo83xL.mjs");
2367
- await shutdownControllers(controllers, createSilentLogger());
3897
+ if (loops) {
3898
+ const { shutdownLoops } = await import("../workflow-DQ6Eju4n.mjs");
3899
+ const { createSilentLogger } = await Promise.resolve().then(() => logger_exports);
3900
+ await shutdownLoops(loops, createSilentLogger());
2368
3901
  }
2369
3902
  process.exit(130);
2370
3903
  };
@@ -2372,19 +3905,19 @@ Note: Workflow name is inferred from YAML 'name' field or filename
2372
3905
  process.on("SIGTERM", cleanup);
2373
3906
  try {
2374
3907
  const log = options.json ? console.error : console.log;
2375
- const result = await runWorkflowWithControllers({
3908
+ const result = await runWorkflowWithLoops({
2376
3909
  workflow: parsedWorkflow,
2377
3910
  workflowName,
2378
3911
  tag,
2379
3912
  workflowPath: file,
2380
- instance: `${workflowName}:${tag}`,
2381
3913
  debug: options.debug,
2382
3914
  log,
2383
3915
  mode: "run",
2384
3916
  feedback: options.feedback,
2385
- prettyDisplay: !options.debug && !options.json
3917
+ prettyDisplay: !options.debug && !options.json,
3918
+ params
2386
3919
  });
2387
- controllers = result.controllers;
3920
+ loops = result.loops;
2388
3921
  process.off("SIGINT", cleanup);
2389
3922
  process.off("SIGTERM", cleanup);
2390
3923
  if (!result.success) {
@@ -2400,7 +3933,7 @@ Note: Workflow name is inferred from YAML 'name' field or filename
2400
3933
  feedback: result.feedback
2401
3934
  }, null, 2));
2402
3935
  else if (!options.debug) {
2403
- const { showWorkflowSummary } = await import("../display-pretty-BCJq5v9d.mjs");
3936
+ const { showWorkflowSummary } = await import("../display-pretty-Kyd40DEF.mjs");
2404
3937
  showWorkflowSummary({
2405
3938
  duration: result.duration,
2406
3939
  document: finalDoc,
@@ -2424,27 +3957,42 @@ Note: Workflow name is inferred from YAML 'name' field or filename
2424
3957
  process.exit(1);
2425
3958
  }
2426
3959
  });
2427
- program.command("start <file>").description("Start workflow via daemon and keep agents running").option("--tag <tag>", "Workflow instance tag (default: main)", DEFAULT_TAG).option("--feedback", "Enable feedback tool (agents can report tool/workflow observations)").option("--json", "Output as JSON").addHelpText("after", `
3960
+ program.command("start <file>").description("Start workflow via daemon and keep agents running").option("--tag <tag>", "Workflow instance tag (default: main)", DEFAULT_TAG).option("--feedback", "Enable feedback tool (agents can report tool/workflow observations)").option("--json", "Output as JSON").allowExcessArguments().addHelpText("after", `
2428
3961
  Examples:
2429
3962
  $ agent-worker start review.yaml # Start review:main (Ctrl+C to stop)
2430
3963
  $ agent-worker start review.yaml --tag pr-123 # Start review:pr-123
3964
+ $ agent-worker start review.yaml -- --target main # With workflow params
2431
3965
 
2432
3966
  Workflow runs inside the daemon. Use ls/stop to manage:
2433
3967
  $ agent-worker ls # List all agents
2434
3968
  $ agent-worker stop @review:pr-123 # Stop workflow
2435
3969
 
2436
- Note: Workflow name is inferred from YAML 'name' field or filename
3970
+ Note: Workflow name is inferred from YAML 'name' field or filename.
3971
+ Workflow-defined params (see 'params:' in YAML) are passed after '--'.
2437
3972
  `).action(async (file, options) => {
2438
- const { parseWorkflowFile } = await import("../workflow-CIE3WPNx.mjs");
3973
+ const { parseWorkflowFile, parseWorkflowParams, formatParamHelp } = await import("../workflow-DQ6Eju4n.mjs");
2439
3974
  const { ensureDaemon } = await Promise.resolve().then(() => agent_exports);
2440
3975
  const tag = options.tag || DEFAULT_TAG;
2441
3976
  const parsedWorkflow = await parseWorkflowFile(file, { tag });
2442
3977
  const workflowName = parsedWorkflow.name;
3978
+ let params;
3979
+ if (parsedWorkflow.params && parsedWorkflow.params.length > 0) {
3980
+ const extraArgs = getArgsAfterSeparator();
3981
+ try {
3982
+ params = parseWorkflowParams(parsedWorkflow.params, extraArgs);
3983
+ } catch (error) {
3984
+ const msg = error instanceof Error ? error.message : String(error);
3985
+ console.error(`Error: ${msg}`);
3986
+ console.error(formatParamHelp(parsedWorkflow.params));
3987
+ process.exit(1);
3988
+ }
3989
+ }
2443
3990
  await ensureDaemon();
2444
3991
  const res = await startWorkflow({
2445
3992
  workflow: parsedWorkflow,
2446
3993
  tag,
2447
- feedback: options.feedback
3994
+ feedback: options.feedback,
3995
+ params
2448
3996
  });
2449
3997
  if (res.error) {
2450
3998
  console.error("Error:", res.error);
@@ -2480,6 +4028,15 @@ Note: Workflow name is inferred from YAML 'name' field or filename
2480
4028
  await new Promise(() => {});
2481
4029
  });
2482
4030
  }
4031
+ /**
4032
+ * Get arguments after the '--' separator.
4033
+ * Standard Unix convention: everything after '--' is passed through
4034
+ * without being interpreted as options by the CLI framework.
4035
+ */
4036
+ function getArgsAfterSeparator() {
4037
+ const idx = process.argv.indexOf("--");
4038
+ return idx === -1 ? [] : process.argv.slice(idx + 1);
4039
+ }
2483
4040
 
2484
4041
  //#endregion
2485
4042
  //#region src/cli/commands/send.ts
@@ -2607,10 +4164,12 @@ function registerInfoCommands(program) {
2607
4164
  console.log(` Gateway format: provider/model (e.g., ${gatewayExample})`);
2608
4165
  console.log(` Direct format: provider:model (e.g., ${directExample})`);
2609
4166
  console.log(` Custom endpoint: --provider anthropic --base-url <url> --api-key '$KEY'`);
4167
+ console.log(` Auto-detect: model: auto (scan env vars, pick best available)`);
2610
4168
  console.log(`\nDefault: ${defaultModel} (when no model specified)`);
4169
+ console.log(`Auto: AGENT_DEFAULT_MODELS="deepseek-chat, anthropic/claude-sonnet-4-5"`);
2611
4170
  });
2612
4171
  program.command("backends").description("Check available backends (SDK, CLI tools)").action(async () => {
2613
- const { listBackends } = await import("../backends-BWzhErjT.mjs");
4172
+ const { listBackends } = await import("../backends-BYWmuyF9.mjs");
2614
4173
  const backends = await listBackends();
2615
4174
  console.log("Backend Status:\n");
2616
4175
  for (const backend of backends) {
@@ -2640,7 +4199,7 @@ Examples:
2640
4199
  $ agent-worker doc read @review:pr-123 # Read specific workflow:tag document
2641
4200
  `).action(async (targetInput) => {
2642
4201
  const dir = await resolveDir(targetInput);
2643
- const { createFileContextProvider } = await import("../context-BqEyt2SF.mjs");
4202
+ const { createFileContextProvider } = await import("../context-CdcZpO-0.mjs");
2644
4203
  const content = await createFileContextProvider(dir, []).readDocument();
2645
4204
  console.log(content || "(empty document)");
2646
4205
  });
@@ -2658,7 +4217,7 @@ Examples:
2658
4217
  process.exit(1);
2659
4218
  }
2660
4219
  const dir = await resolveDir(targetInput);
2661
- const { createFileContextProvider } = await import("../context-BqEyt2SF.mjs");
4220
+ const { createFileContextProvider } = await import("../context-CdcZpO-0.mjs");
2662
4221
  await createFileContextProvider(dir, []).writeDocument(content);
2663
4222
  console.log("Document written");
2664
4223
  });
@@ -2676,7 +4235,7 @@ Examples:
2676
4235
  process.exit(1);
2677
4236
  }
2678
4237
  const dir = await resolveDir(targetInput);
2679
- const { createFileContextProvider } = await import("../context-BqEyt2SF.mjs");
4238
+ const { createFileContextProvider } = await import("../context-CdcZpO-0.mjs");
2680
4239
  await createFileContextProvider(dir, []).appendDocument(content);
2681
4240
  console.log("Content appended");
2682
4241
  });
@@ -2690,18 +4249,25 @@ async function resolveDir(targetInput) {
2690
4249
 
2691
4250
  //#endregion
2692
4251
  //#region package.json
2693
- var version = "0.13.0";
4252
+ var version = "0.15.0";
2694
4253
 
2695
4254
  //#endregion
2696
4255
  //#region src/cli/index.ts
2697
4256
  globalThis.AI_SDK_LOG_WARNINGS = false;
2698
4257
  const originalStderrWrite = process.stderr.write.bind(process.stderr);
2699
- process.stderr.write = function(chunk, ...rest) {
2700
- if (process.argv.includes("--debug") || process.argv.includes("-d")) {
2701
- const message = typeof chunk === "string" ? chunk : chunk.toString();
2702
- return originalStderrWrite.call(process.stderr, message, ...rest);
2703
- }
2704
- return true;
4258
+ const isDebugMode = process.argv.includes("--debug") || process.argv.includes("-d");
4259
+ const SDK_NOISE_PATTERNS = [
4260
+ "specificationVersion",
4261
+ "AI_SDK",
4262
+ "ai-sdk",
4263
+ "deprecated",
4264
+ "ExperimentalWarning"
4265
+ ];
4266
+ process.stderr.write = function(chunk, encodingOrCb, cb) {
4267
+ const message = typeof chunk === "string" ? chunk : chunk.toString();
4268
+ if (isDebugMode) return originalStderrWrite(message, encodingOrCb, cb);
4269
+ if (SDK_NOISE_PATTERNS.some((pattern) => message.includes(pattern))) return true;
4270
+ return originalStderrWrite(message, encodingOrCb, cb);
2705
4271
  };
2706
4272
  const program = new Command();
2707
4273
  program.name("agent-worker").description("CLI for creating and managing AI agents").version(version);
@@ -2713,4 +4279,4 @@ registerDocCommands(program);
2713
4279
  program.parse();
2714
4280
 
2715
4281
  //#endregion
2716
- export { formatToolParams as C, formatInbox as S, EventLog as T, shouldUseResource as _, FileStorage as a, formatProposalList as b, CONTEXT_DEFAULTS as c, RESOURCE_PREFIX as d, RESOURCE_SCHEME as f, generateResourceId as g, extractMentions as h, resolveContextDir as i, MENTION_PATTERN as l, createResourceRef as m, createFileContextProvider as n, MemoryStorage as o, calculatePriority as p, getDefaultContextDir as r, ContextProviderImpl as s, FileContextProvider as t, MESSAGE_LENGTH_THRESHOLD as u, createContextMCPServer as v, getAgentId as w, createLogTool as x, formatProposal as y };
4282
+ export { MENTION_PATTERN as A, formatProposal as B, DefaultDocumentStore as C, MemoryStorage as D, FileStorage as E, createResourceRef as F, getAgentId as G, createLogTool as H, extractMentions as I, EventLog as K, generateResourceId as L, RESOURCE_PREFIX$1 as M, RESOURCE_SCHEME as N, ContextProviderImpl as O, calculatePriority as P, shouldUseResource as R, DefaultResourceStore as S, DefaultChannelStore as T, formatInbox$1 as U, formatProposalList as V, formatToolParams as W, FileContextProvider as _, getBackendByType as a, resolveContextDir as b, createAgentLoop as c, generateWorkflowMCPConfig as d, writeBackendMcpConfig as f, runWithHttp as g, LOOP_DEFAULTS as h, createSilentLogger as i, MESSAGE_LENGTH_THRESHOLD as j, CONTEXT_DEFAULTS as k, runSdkAgent as l, formatInbox as m, createWiredLoop as n, getBackendForModel as o, buildAgentPrompt as p, createChannelLogger as r, checkWorkflowIdle as s, createMinimalRuntime as t, runMockAgent as u, createFileContextProvider as v, DefaultInboxStore as w, DefaultStatusStore as x, getDefaultContextDir as y, createContextMCPServer as z };