chainlesschain 0.45.81 → 0.47.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 +10 -0
- package/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{Analytics-C1AnPdMx.js → Analytics-DgypYeUB.js} +2 -2
- package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +1 -0
- package/src/assets/web-panel/assets/{Backup-D31iZX3l.js → Backup-Ba9UybpT.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DiXJ3TuK.js → Chat-BwXskT21.js} +1 -1
- package/src/assets/web-panel/assets/Cowork-CXuhlHew.css +1 -0
- package/src/assets/web-panel/assets/Cowork-UmOe7qvE.js +7 -0
- package/src/assets/web-panel/assets/{Cron-DBt1ueXh.js → Cron-JHS-rc-4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-HPh9FcPt.js → Dashboard-B95cMCO7.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
- package/src/assets/web-panel/assets/{Git-hwQ1oZHj.js → Git-CSYO0_zk.js} +2 -2
- package/src/assets/web-panel/assets/{Logs-4D9p6PRM.js → Logs-Hxw_K0km.js} +2 -2
- package/src/assets/web-panel/assets/{McpTools-CyAUjbbs.js → McpTools-DIE75TrB.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-BMqOR7S-.js → Memory-C4KVnLlp.js} +2 -2
- package/src/assets/web-panel/assets/{Notes-Cmas8i4E.js → Notes-DuzrHMAk.js} +2 -2
- package/src/assets/web-panel/assets/{Organization-DnSa58Tl.js → Organization-DTq6uF82.js} +4 -4
- package/src/assets/web-panel/assets/{P2P-BxksIBWs.js → P2P-C0hjlhsR.js} +2 -2
- package/src/assets/web-panel/assets/{Permissions-Bq5Qn2s3.js → Permissions-Ec0NH-xC.js} +4 -4
- package/src/assets/web-panel/assets/{Projects-B7EM0uPg.js → Projects-U8D0asCS.js} +2 -2
- package/src/assets/web-panel/assets/{Providers-DAwgG5KV.js → Providers-BngtTLvJ.js} +2 -2
- package/src/assets/web-panel/assets/{RssFeed-HSZoRXvS.js → RssFeed-B9NbwCKM.js} +3 -3
- package/src/assets/web-panel/assets/{Security-Cz17qBny.js → Security-BL5Rkr1T.js} +3 -3
- package/src/assets/web-panel/assets/{Services-D2EsLq-v.js → Services-D4MJzLld.js} +2 -2
- package/src/assets/web-panel/assets/{Skills-C9v-f3vZ.js → Skills-CQTOMDwF.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-yMEcU0n7.js → Tasks-DepbJMnL.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-l7SvlKuB.js → Templates-C24PVZPu.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BHWhLWn9.js → Wallet-PQoSpN_P.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-kWhFYaUK.js → WebAuthn-BcuyQ4Lr.js} +4 -4
- package/src/assets/web-panel/assets/WorkflowEditor-C-SvXbHW.js +1 -0
- package/src/assets/web-panel/assets/WorkflowEditor-D5bX6woe.css +1 -0
- package/src/assets/web-panel/assets/{antd-D6h4fDFf.js → antd-DEjZPGMj.js} +82 -82
- package/src/assets/web-panel/assets/index-CwvzTTw_.js +2 -0
- package/src/assets/web-panel/assets/{markdown-BZsB-Dsv.js → markdown-CusdXFxb.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/cowork.js +867 -0
- package/src/gateways/ws/action-protocol.js +182 -2
- package/src/gateways/ws/message-dispatcher.js +5 -0
- package/src/gateways/ws/ws-server.js +21 -0
- package/src/lib/cowork-cron.js +474 -0
- package/src/lib/cowork-evomap-adapter.js +121 -0
- package/src/lib/cowork-learning.js +438 -0
- package/src/lib/cowork-mcp-tools.js +182 -0
- package/src/lib/cowork-observe-html.js +108 -0
- package/src/lib/cowork-observe.js +160 -0
- package/src/lib/cowork-share.js +322 -0
- package/src/lib/cowork-task-runner.js +317 -3
- package/src/lib/cowork-task-templates.js +101 -13
- package/src/lib/cowork-template-marketplace.js +205 -0
- package/src/lib/cowork-workflow.js +571 -0
- package/src/lib/provider-options.js +133 -0
- package/src/lib/skill-loader.js +65 -0
- package/src/lib/sub-agent-context.js +54 -2
- package/src/lib/sub-agent-profiles.js +164 -0
- package/src/lib/todo-manager.js +108 -0
- package/src/lib/turn-context.js +95 -0
- package/src/lib/web-fetch.js +224 -0
- package/src/lib/workflow-expr.js +318 -0
- package/src/repl/agent-repl.js +4 -0
- package/src/runtime/agent-core.js +135 -3
- package/src/runtime/coding-agent-contract-shared.cjs +131 -0
- package/src/runtime/coding-agent-policy.cjs +30 -0
- package/src/assets/web-panel/assets/AppLayout-YdvJBMHH.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-cxfKLu-m.css +0 -1
- package/src/assets/web-panel/assets/Cowork-BnrHWwZw.js +0 -7
- package/src/assets/web-panel/assets/Cowork-CcSoS3eX.css +0 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
- package/src/assets/web-panel/assets/index-ByUk2Wmr.js +0 -2
package/src/lib/skill-loader.js
CHANGED
|
@@ -109,6 +109,71 @@ export function parseSkillMd(content) {
|
|
|
109
109
|
return { data, body };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Substitute $ARGUMENTS / $1 / $2 / ... placeholders in a skill body.
|
|
114
|
+
* Inspired by open-agents substituteArguments.
|
|
115
|
+
*
|
|
116
|
+
* Rules:
|
|
117
|
+
* - $ARGUMENTS → full args string (joined by space if array)
|
|
118
|
+
* - $1, $2, ... → positional args (shell-like; split on whitespace if string)
|
|
119
|
+
* - Escape $ via $$ → literal $
|
|
120
|
+
* - Unmatched placeholders are left as-is (non-destructive)
|
|
121
|
+
*
|
|
122
|
+
* @param {string} body - Skill body text
|
|
123
|
+
* @param {string|string[]} args - Args as raw string or pre-split array
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
export function substituteArguments(body, args) {
|
|
127
|
+
if (typeof body !== "string" || body.length === 0) return body || "";
|
|
128
|
+
let full = "";
|
|
129
|
+
let positional = [];
|
|
130
|
+
if (Array.isArray(args)) {
|
|
131
|
+
positional = args.map((a) => String(a));
|
|
132
|
+
full = positional.join(" ");
|
|
133
|
+
} else if (typeof args === "string") {
|
|
134
|
+
full = args;
|
|
135
|
+
positional = args.trim() === "" ? [] : args.trim().split(/\s+/);
|
|
136
|
+
}
|
|
137
|
+
// Protect literal $$
|
|
138
|
+
const MARKER = "\u0000DOLLAR\u0000";
|
|
139
|
+
let out = body.replace(/\$\$/g, MARKER);
|
|
140
|
+
out = out.replace(/\$ARGUMENTS\b/g, full);
|
|
141
|
+
out = out.replace(/\$(\d+)/g, (match, idx) => {
|
|
142
|
+
const i = parseInt(idx, 10) - 1;
|
|
143
|
+
if (i < 0 || i >= positional.length) return match;
|
|
144
|
+
return positional[i];
|
|
145
|
+
});
|
|
146
|
+
return out.replace(new RegExp(MARKER, "g"), "$");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Prepend `Skill directory: <abs>` line to body so the LLM can resolve
|
|
151
|
+
* relative paths declared inside the SKILL.md.
|
|
152
|
+
* Inspired by open-agents injectSkillDirectory.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} body
|
|
155
|
+
* @param {string} skillDir - Absolute path to the skill directory
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
export function injectSkillDirectory(body, skillDir) {
|
|
159
|
+
if (!skillDir) return body || "";
|
|
160
|
+
const header = `Skill directory: ${skillDir}\n\n`;
|
|
161
|
+
return header + (body || "");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Prepare a skill body for execution: substitute $ARGUMENTS / $N placeholders
|
|
166
|
+
* and prepend the skill directory header.
|
|
167
|
+
*
|
|
168
|
+
* @param {object} skill - Skill metadata (must have .body and .skillDir)
|
|
169
|
+
* @param {string|string[]} args - Runtime args
|
|
170
|
+
* @returns {string}
|
|
171
|
+
*/
|
|
172
|
+
export function prepareSkillBody(skill, args) {
|
|
173
|
+
const withArgs = substituteArguments(skill?.body || "", args);
|
|
174
|
+
return injectSkillDirectory(withArgs, skill?.skillDir);
|
|
175
|
+
}
|
|
176
|
+
|
|
112
177
|
/**
|
|
113
178
|
* Multi-layer CLI skill loader
|
|
114
179
|
*/
|
|
@@ -61,7 +61,14 @@ export class SubAgentContext {
|
|
|
61
61
|
this.parentId = options.parentId || null;
|
|
62
62
|
this.role = options.role || "general";
|
|
63
63
|
this.task = options.task || "";
|
|
64
|
-
|
|
64
|
+
// Declarative profile (Phase 3) — explorer/executor/design, etc.
|
|
65
|
+
// Provides systemPrompt + maxIterations + modelHint defaults that
|
|
66
|
+
// explicit options can still override.
|
|
67
|
+
this._profile = options.profile || null;
|
|
68
|
+
this.maxIterations =
|
|
69
|
+
options.maxIterations ||
|
|
70
|
+
this._profile?.maxIterations ||
|
|
71
|
+
DEFAULT_MAX_ITERATIONS;
|
|
65
72
|
this.iterationBudget = options.iterationBudget || null; // shared budget from parent
|
|
66
73
|
this.tokenBudget = options.tokenBudget || null;
|
|
67
74
|
this.inheritedContext = options.inheritedContext || null;
|
|
@@ -107,8 +114,30 @@ export class SubAgentContext {
|
|
|
107
114
|
// Optional abort signal for cancellation
|
|
108
115
|
this._signal = options.signal || null;
|
|
109
116
|
|
|
117
|
+
// Optional MCP / external tool plumbing. These are forwarded into the
|
|
118
|
+
// agentLoop options so MCP-backed tools (e.g. from a cowork template's
|
|
119
|
+
// `mcpServers`) appear in the LLM's tool list and route through
|
|
120
|
+
// `mcpClient.callTool()` in agent-core's default-case dispatch.
|
|
121
|
+
this._extraToolDefinitions = Array.isArray(options.extraToolDefinitions)
|
|
122
|
+
? options.extraToolDefinitions
|
|
123
|
+
: [];
|
|
124
|
+
this._externalToolDescriptors =
|
|
125
|
+
options.externalToolDescriptors &&
|
|
126
|
+
typeof options.externalToolDescriptors === "object"
|
|
127
|
+
? options.externalToolDescriptors
|
|
128
|
+
: {};
|
|
129
|
+
this._externalToolExecutors =
|
|
130
|
+
options.externalToolExecutors &&
|
|
131
|
+
typeof options.externalToolExecutors === "object"
|
|
132
|
+
? options.externalToolExecutors
|
|
133
|
+
: {};
|
|
134
|
+
this._mcpClient = options.mcpClient || null;
|
|
135
|
+
|
|
110
136
|
// Build isolated system prompt
|
|
111
137
|
const basePrompt = buildSystemPrompt(this.cwd);
|
|
138
|
+
const profilePrompt = this._profile?.systemPrompt
|
|
139
|
+
? `\n\n## Profile: ${this._profile.name}\n${this._profile.systemPrompt}`
|
|
140
|
+
: "";
|
|
112
141
|
const rolePrompt = `\n\n## Sub-Agent Role: ${this.role}\nYou are a focused sub-agent with the role "${this.role}". Your task is:\n${this.task}\n\nStay focused on this specific task. Be concise and return results directly.`;
|
|
113
142
|
const contextSection = this.inheritedContext
|
|
114
143
|
? `\n\n## Parent Context\n${this.inheritedContext}`
|
|
@@ -116,7 +145,7 @@ export class SubAgentContext {
|
|
|
116
145
|
|
|
117
146
|
this.messages.push({
|
|
118
147
|
role: "system",
|
|
119
|
-
content: basePrompt + rolePrompt + contextSection,
|
|
148
|
+
content: basePrompt + profilePrompt + rolePrompt + contextSection,
|
|
120
149
|
});
|
|
121
150
|
}
|
|
122
151
|
|
|
@@ -226,6 +255,29 @@ export class SubAgentContext {
|
|
|
226
255
|
options.iterationBudget = this.iterationBudget;
|
|
227
256
|
}
|
|
228
257
|
|
|
258
|
+
// Forward MCP / external tool plumbing into the agent loop
|
|
259
|
+
if (this._extraToolDefinitions.length > 0) {
|
|
260
|
+
options.extraToolDefinitions = [
|
|
261
|
+
...(options.extraToolDefinitions || []),
|
|
262
|
+
...this._extraToolDefinitions,
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
if (Object.keys(this._externalToolDescriptors).length > 0) {
|
|
266
|
+
options.externalToolDescriptors = {
|
|
267
|
+
...(options.externalToolDescriptors || {}),
|
|
268
|
+
...this._externalToolDescriptors,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (Object.keys(this._externalToolExecutors).length > 0) {
|
|
272
|
+
options.externalToolExecutors = {
|
|
273
|
+
...(options.externalToolExecutors || {}),
|
|
274
|
+
...this._externalToolExecutors,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (this._mcpClient) {
|
|
278
|
+
options.mcpClient = this._mcpClient;
|
|
279
|
+
}
|
|
280
|
+
|
|
229
281
|
try {
|
|
230
282
|
// Use a separate messages array for the agent loop
|
|
231
283
|
// The agentLoop will append to this.messages directly
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-Agent Profiles — declarative registry of subagent roles.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by open-agents SUBAGENT_REGISTRY. Separate from the runtime
|
|
5
|
+
* `sub-agent-registry.js` which tracks *instances*; this module describes
|
|
6
|
+
* the *kinds* (explorer/executor/design) a parent agent may delegate to.
|
|
7
|
+
*
|
|
8
|
+
* Each profile defines:
|
|
9
|
+
* - name stable identifier used by spawn_sub_agent
|
|
10
|
+
* - shortDescription one-line hook for the parent prompt
|
|
11
|
+
* - systemPrompt prepended to sub-agent messages[0]
|
|
12
|
+
* - toolAllowlist array of tool names the sub-agent may call
|
|
13
|
+
* (null = inherit all)
|
|
14
|
+
* - maxIterations optional per-profile iteration cap
|
|
15
|
+
* - modelHint optional { category } hint for llm-manager
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const READONLY_TOOLS = Object.freeze([
|
|
19
|
+
"read_file",
|
|
20
|
+
"list_dir",
|
|
21
|
+
"search_files",
|
|
22
|
+
"search_sessions",
|
|
23
|
+
"web_fetch",
|
|
24
|
+
"list_skills",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const FULL_TOOLS = Object.freeze([
|
|
28
|
+
"read_file",
|
|
29
|
+
"write_file",
|
|
30
|
+
"edit_file",
|
|
31
|
+
"edit_file_hashed",
|
|
32
|
+
"list_dir",
|
|
33
|
+
"search_files",
|
|
34
|
+
"search_sessions",
|
|
35
|
+
"run_shell",
|
|
36
|
+
"git",
|
|
37
|
+
"run_code",
|
|
38
|
+
"run_skill",
|
|
39
|
+
"list_skills",
|
|
40
|
+
"web_fetch",
|
|
41
|
+
"todo_write",
|
|
42
|
+
"ask_user_question",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const DESIGN_TOOLS = Object.freeze([
|
|
46
|
+
"read_file",
|
|
47
|
+
"write_file",
|
|
48
|
+
"edit_file",
|
|
49
|
+
"edit_file_hashed",
|
|
50
|
+
"list_dir",
|
|
51
|
+
"search_files",
|
|
52
|
+
"web_fetch",
|
|
53
|
+
"run_skill",
|
|
54
|
+
"list_skills",
|
|
55
|
+
"todo_write",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const _builtinProfiles = {
|
|
59
|
+
explorer: {
|
|
60
|
+
name: "explorer",
|
|
61
|
+
shortDescription:
|
|
62
|
+
"Read-only researcher. Investigates code, searches files/sessions, fetches web docs. Cannot write or execute.",
|
|
63
|
+
systemPrompt:
|
|
64
|
+
"You are a read-only research sub-agent. Your job is to gather facts and report back concisely. You MUST NOT write files or execute commands. When done, return a structured summary of findings.",
|
|
65
|
+
toolAllowlist: READONLY_TOOLS,
|
|
66
|
+
maxIterations: 20,
|
|
67
|
+
modelHint: { category: "quick" },
|
|
68
|
+
},
|
|
69
|
+
executor: {
|
|
70
|
+
name: "executor",
|
|
71
|
+
shortDescription:
|
|
72
|
+
"Full-permission implementer. Writes code, runs tests, executes shell/git. Use for end-to-end task completion.",
|
|
73
|
+
systemPrompt:
|
|
74
|
+
"You are a full-permission execution sub-agent. Implement the task to completion. Prefer edit_file_hashed over edit_file. Always verify with tests/build when relevant. Return a summary plus list of files changed.",
|
|
75
|
+
toolAllowlist: FULL_TOOLS,
|
|
76
|
+
maxIterations: 40,
|
|
77
|
+
modelHint: { category: "deep" },
|
|
78
|
+
},
|
|
79
|
+
design: {
|
|
80
|
+
name: "design",
|
|
81
|
+
shortDescription:
|
|
82
|
+
"Frontend/UI specialist. Produces polished Vue/React/HTML with distinctive aesthetics. No shell/git access.",
|
|
83
|
+
systemPrompt:
|
|
84
|
+
"You are a frontend design sub-agent. Produce high-quality, production-grade UI code. Avoid generic AI aesthetics. Prefer semantic HTML, accessible components, and thoughtful typography. You may read/write files and fetch references from the web, but cannot run shell or git.",
|
|
85
|
+
toolAllowlist: DESIGN_TOOLS,
|
|
86
|
+
maxIterations: 30,
|
|
87
|
+
modelHint: { category: "creative" },
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const _registry = new Map(Object.entries(_builtinProfiles));
|
|
92
|
+
|
|
93
|
+
export function getSubAgentProfile(name) {
|
|
94
|
+
if (!name) return null;
|
|
95
|
+
const entry = _registry.get(name);
|
|
96
|
+
if (!entry) return null;
|
|
97
|
+
return {
|
|
98
|
+
...entry,
|
|
99
|
+
toolAllowlist: Array.isArray(entry.toolAllowlist)
|
|
100
|
+
? [...entry.toolAllowlist]
|
|
101
|
+
: null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function listSubAgentProfiles() {
|
|
106
|
+
return Array.from(_registry.values()).map((p) => ({
|
|
107
|
+
...p,
|
|
108
|
+
toolAllowlist: Array.isArray(p.toolAllowlist) ? [...p.toolAllowlist] : null,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Register a custom profile (or override a built-in).
|
|
114
|
+
* Returns true on success, false on invalid input.
|
|
115
|
+
*/
|
|
116
|
+
export function registerSubAgentProfile(profile) {
|
|
117
|
+
if (!profile || typeof profile.name !== "string" || !profile.name) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (typeof profile.shortDescription !== "string") return false;
|
|
121
|
+
if (typeof profile.systemPrompt !== "string") return false;
|
|
122
|
+
const toolAllowlist = Array.isArray(profile.toolAllowlist)
|
|
123
|
+
? [...profile.toolAllowlist]
|
|
124
|
+
: null;
|
|
125
|
+
_registry.set(profile.name, {
|
|
126
|
+
name: profile.name,
|
|
127
|
+
shortDescription: profile.shortDescription,
|
|
128
|
+
systemPrompt: profile.systemPrompt,
|
|
129
|
+
toolAllowlist,
|
|
130
|
+
maxIterations:
|
|
131
|
+
typeof profile.maxIterations === "number" ? profile.maxIterations : 20,
|
|
132
|
+
modelHint: profile.modelHint || null,
|
|
133
|
+
});
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function unregisterSubAgentProfile(name) {
|
|
138
|
+
return _registry.delete(name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resetToBuiltins() {
|
|
142
|
+
_registry.clear();
|
|
143
|
+
for (const [k, v] of Object.entries(_builtinProfiles)) {
|
|
144
|
+
_registry.set(k, v);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build a one-section system-prompt snippet listing available subagents.
|
|
150
|
+
* Inspired by open-agents buildSubagentSummaryLines.
|
|
151
|
+
*
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
export function buildSubagentSummaryLines() {
|
|
155
|
+
const profiles = listSubAgentProfiles();
|
|
156
|
+
if (profiles.length === 0) return "";
|
|
157
|
+
const lines = ["## Available sub-agents (via spawn_sub_agent)"];
|
|
158
|
+
for (const p of profiles) {
|
|
159
|
+
lines.push(`- **${p.name}**: ${p.shortDescription}`);
|
|
160
|
+
}
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const _deps = { _registry, _builtinProfiles };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session TODO Manager
|
|
3
|
+
*
|
|
4
|
+
* In-memory per-session TODO list. One instance per sessionId.
|
|
5
|
+
* Inspired by open-agents todo_write tool.
|
|
6
|
+
*
|
|
7
|
+
* Contract:
|
|
8
|
+
* - Exactly one item may be in_progress at a time (validator enforces)
|
|
9
|
+
* - writeTodos replaces the full list (idempotent updates)
|
|
10
|
+
* - getTodos returns a deep-cloned array
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const VALID_STATUSES = Object.freeze([
|
|
14
|
+
"pending",
|
|
15
|
+
"in_progress",
|
|
16
|
+
"completed",
|
|
17
|
+
"cancelled",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const _stores = new Map();
|
|
21
|
+
|
|
22
|
+
export function getTodoStore(sessionId) {
|
|
23
|
+
const key = sessionId || "__default__";
|
|
24
|
+
if (!_stores.has(key)) {
|
|
25
|
+
_stores.set(key, { todos: [] });
|
|
26
|
+
}
|
|
27
|
+
return _stores.get(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateTodos(todos) {
|
|
31
|
+
if (!Array.isArray(todos)) {
|
|
32
|
+
return { valid: false, error: "todos must be an array" };
|
|
33
|
+
}
|
|
34
|
+
const ids = new Set();
|
|
35
|
+
let inProgressCount = 0;
|
|
36
|
+
for (const todo of todos) {
|
|
37
|
+
if (!todo || typeof todo !== "object") {
|
|
38
|
+
return { valid: false, error: "each todo must be an object" };
|
|
39
|
+
}
|
|
40
|
+
if (typeof todo.id !== "string" || !todo.id) {
|
|
41
|
+
return { valid: false, error: "todo.id must be a non-empty string" };
|
|
42
|
+
}
|
|
43
|
+
if (ids.has(todo.id)) {
|
|
44
|
+
return { valid: false, error: `duplicate todo id: ${todo.id}` };
|
|
45
|
+
}
|
|
46
|
+
ids.add(todo.id);
|
|
47
|
+
if (typeof todo.content !== "string" || !todo.content) {
|
|
48
|
+
return { valid: false, error: `todo.content required for id=${todo.id}` };
|
|
49
|
+
}
|
|
50
|
+
if (!VALID_STATUSES.includes(todo.status)) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
error: `todo.status must be one of ${VALID_STATUSES.join("|")} (id=${todo.id})`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (todo.status === "in_progress") inProgressCount += 1;
|
|
57
|
+
}
|
|
58
|
+
if (inProgressCount > 1) {
|
|
59
|
+
return {
|
|
60
|
+
valid: false,
|
|
61
|
+
error: "only one todo may be in_progress at a time",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { valid: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function writeTodos(sessionId, todos) {
|
|
68
|
+
const check = validateTodos(todos);
|
|
69
|
+
if (!check.valid) {
|
|
70
|
+
return { success: false, error: check.error };
|
|
71
|
+
}
|
|
72
|
+
const store = getTodoStore(sessionId);
|
|
73
|
+
store.todos = todos.map((t) => ({
|
|
74
|
+
id: t.id,
|
|
75
|
+
content: t.content,
|
|
76
|
+
status: t.status,
|
|
77
|
+
}));
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
count: store.todos.length,
|
|
81
|
+
summary: summarizeTodos(store.todos),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getTodos(sessionId) {
|
|
86
|
+
const store = getTodoStore(sessionId);
|
|
87
|
+
return store.todos.map((t) => ({ ...t }));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function clearTodos(sessionId) {
|
|
91
|
+
const store = getTodoStore(sessionId);
|
|
92
|
+
store.todos = [];
|
|
93
|
+
return { success: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function summarizeTodos(todos) {
|
|
97
|
+
const counts = { pending: 0, in_progress: 0, completed: 0, cancelled: 0 };
|
|
98
|
+
for (const t of todos || []) {
|
|
99
|
+
if (counts[t.status] !== undefined) counts[t.status] += 1;
|
|
100
|
+
}
|
|
101
|
+
return counts;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function resetAllStores() {
|
|
105
|
+
_stores.clear();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const _deps = { _stores };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-scoped context builder — inspired by open-agents' prepareCall.
|
|
3
|
+
*
|
|
4
|
+
* Produces a short system-prompt supplement that is re-computed before each
|
|
5
|
+
* LLM call in the agent loop. Gives the model fresh runtime signals (cwd,
|
|
6
|
+
* git HEAD/branch/dirty, active skills, turn counter) without polluting the
|
|
7
|
+
* persistent message history.
|
|
8
|
+
*
|
|
9
|
+
* Callers: agent-core.agentLoop's pre-call hook, via options.prepareCall.
|
|
10
|
+
*
|
|
11
|
+
* @module turn-context
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
import path from "path";
|
|
16
|
+
|
|
17
|
+
const _deps = { execSync };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run a git command with stdio pipe and return stdout or null.
|
|
21
|
+
* @param {string} cmd
|
|
22
|
+
* @param {string} cwd
|
|
23
|
+
* @returns {string|null}
|
|
24
|
+
*/
|
|
25
|
+
function _git(cmd, cwd) {
|
|
26
|
+
try {
|
|
27
|
+
return _deps
|
|
28
|
+
.execSync(`git ${cmd}`, {
|
|
29
|
+
cwd,
|
|
30
|
+
encoding: "utf-8",
|
|
31
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
32
|
+
timeout: 1500,
|
|
33
|
+
})
|
|
34
|
+
.trim();
|
|
35
|
+
} catch (_e) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a compact turn-scoped context block.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} input
|
|
44
|
+
* @param {number} [input.iteration] - 1-based iteration counter for this turn.
|
|
45
|
+
* @param {string} [input.cwd] - Working directory (defaults to process.cwd()).
|
|
46
|
+
* @param {string|null} [input.sessionId] - Agent session id.
|
|
47
|
+
* @param {string[]} [input.activeSkills] - Names of skills currently active.
|
|
48
|
+
* @returns {string} Markdown-formatted supplement, or empty string if nothing useful.
|
|
49
|
+
*/
|
|
50
|
+
export function buildTurnContext({
|
|
51
|
+
iteration = 1,
|
|
52
|
+
cwd = process.cwd(),
|
|
53
|
+
sessionId = null,
|
|
54
|
+
activeSkills = [],
|
|
55
|
+
} = {}) {
|
|
56
|
+
const lines = [];
|
|
57
|
+
lines.push(`## Turn context (iteration ${iteration})`);
|
|
58
|
+
lines.push(`- cwd: ${path.resolve(cwd)}`);
|
|
59
|
+
|
|
60
|
+
const branch = _git("rev-parse --abbrev-ref HEAD", cwd);
|
|
61
|
+
if (branch) {
|
|
62
|
+
const head = _git("rev-parse --short HEAD", cwd);
|
|
63
|
+
const status = _git("status --porcelain", cwd);
|
|
64
|
+
const dirty = status && status.length > 0;
|
|
65
|
+
const fileCount = dirty ? status.split("\n").filter(Boolean).length : 0;
|
|
66
|
+
lines.push(
|
|
67
|
+
`- git: ${branch}@${head || "?"}${dirty ? ` (${fileCount} uncommitted)` : " (clean)"}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (sessionId) {
|
|
72
|
+
lines.push(`- session: ${sessionId}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(activeSkills) && activeSkills.length > 0) {
|
|
76
|
+
lines.push(`- active skills: ${activeSkills.join(", ")}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Default prepareCall implementation — builds a turn-context supplement and
|
|
84
|
+
* returns it as a structured payload. agent-core wraps this into a transient
|
|
85
|
+
* system message for the next llmCall without mutating persistent history.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} ctx - Supplied by agent-core at call site.
|
|
88
|
+
* @returns {{ systemSuffix: string } | null}
|
|
89
|
+
*/
|
|
90
|
+
export function defaultPrepareCall(ctx) {
|
|
91
|
+
const supplement = buildTurnContext(ctx);
|
|
92
|
+
return supplement ? { systemSuffix: supplement } : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { _deps };
|