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.
- package/README.md +6 -3
- package/dist/{backends-BWzhErjT.mjs → backends-BYWmuyF9.mjs} +1 -1
- package/dist/{backends-CziIqKRg.mjs → backends-C7pQwuAx.mjs} +310 -222
- package/dist/cli/index.mjs +2044 -478
- package/dist/context-CdcZpO-0.mjs +4 -0
- package/dist/create-tool-gcUuI1FD.mjs +32 -0
- package/dist/index.d.mts +65 -87
- package/dist/index.mjs +465 -21
- package/dist/{memory-provider-BtLYtdQH.mjs → memory-provider-ZLOKyCxA.mjs} +8 -3
- package/dist/runner-DB-b57iZ.mjs +670 -0
- package/dist/workflow-DQ6Eju4n.mjs +664 -0
- package/package.json +4 -3
- package/dist/context-BqEyt2SF.mjs +0 -4
- package/dist/logger-Bfdo83xL.mjs +0 -63
- package/dist/runner-CnxROIev.mjs +0 -1496
- package/dist/worker-DBJ8136Q.mjs +0 -448
- package/dist/workflow-CIE3WPNx.mjs +0 -272
- /package/dist/{display-pretty-BCJq5v9d.mjs → display-pretty-Kyd40DEF.mjs} +0 -0
package/dist/cli/index.mjs
CHANGED
|
@@ -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-
|
|
3
|
-
import { t as
|
|
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
|
-
*
|
|
85
|
-
*
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return
|
|
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
|
-
|
|
111
|
-
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
914
|
-
|
|
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
|
-
|
|
952
|
-
|
|
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
|
-
|
|
966
|
-
|
|
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.
|
|
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.
|
|
984
|
-
await this.
|
|
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.
|
|
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
|
-
|
|
1039
|
-
return
|
|
945
|
+
getInbox(agent) {
|
|
946
|
+
return this.inbox.getInbox(agent);
|
|
1040
947
|
}
|
|
1041
|
-
|
|
1042
|
-
return
|
|
948
|
+
markInboxSeen(agent, untilId) {
|
|
949
|
+
return this.inbox.markSeen(agent, untilId);
|
|
1043
950
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
951
|
+
ackInbox(agent, untilId) {
|
|
952
|
+
return this.inbox.ack(agent, untilId);
|
|
1046
953
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
954
|
+
readDocument(file) {
|
|
955
|
+
return this.documents.read(file);
|
|
1049
956
|
}
|
|
1050
|
-
|
|
1051
|
-
return
|
|
957
|
+
writeDocument(content, file) {
|
|
958
|
+
return this.documents.write(content, file);
|
|
1052
959
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1091
|
-
|
|
972
|
+
readResource(id) {
|
|
973
|
+
return this.resources.read(id);
|
|
1092
974
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
1112
|
-
return
|
|
978
|
+
getAgentStatus(agent) {
|
|
979
|
+
return this.status.get(agent);
|
|
1113
980
|
}
|
|
1114
|
-
|
|
1115
|
-
return this.
|
|
981
|
+
listAgentStatus() {
|
|
982
|
+
return this.status.list();
|
|
1116
983
|
}
|
|
1117
984
|
async markRunStart() {
|
|
1118
|
-
|
|
985
|
+
await this.inbox.markRunStart();
|
|
1119
986
|
}
|
|
1120
987
|
async destroy() {
|
|
1121
|
-
await this.
|
|
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/
|
|
2543
|
+
//#region src/workflow/loop/backend.ts
|
|
1298
2544
|
/**
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
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
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
-
*
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
-
*
|
|
1317
|
-
*
|
|
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
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
*
|
|
2658
|
+
* Workflow Factory — Composable primitives for building workflow runtimes.
|
|
1372
2659
|
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
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
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
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
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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
|
|
2739
|
+
* Create a fully-wired agent loop.
|
|
1407
2740
|
*
|
|
1408
|
-
*
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
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
|
|
1421
|
-
|
|
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)
|
|
1431
|
-
*
|
|
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
|
-
*
|
|
1436
|
-
*
|
|
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
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
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
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
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
|
|
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:
|
|
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
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
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
|
-
|
|
1608
|
-
|
|
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
|
|
1612
|
-
|
|
1613
|
-
|
|
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:
|
|
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
|
-
|
|
1648
|
-
|
|
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
|
|
1651
|
-
|
|
1652
|
-
return c.json(
|
|
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
|
|
1686
|
-
|
|
1687
|
-
|
|
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 {
|
|
1724
|
-
const result = await
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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 (
|
|
2365
|
-
const {
|
|
2366
|
-
const { createSilentLogger } = await
|
|
2367
|
-
await
|
|
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
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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.
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
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 {
|
|
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 };
|