chainlesschain 0.46.0 → 0.47.1
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 +16 -5
- 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-DQyDwGut.css +1 -0
- package/src/assets/web-panel/assets/AppLayout-ZHpCFO_p.js +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-B8ZDdRm4.js → Cowork-UmOe7qvE.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-DBt1ueXh.js → Cron-JHS-rc-4.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-jt6XPIjB.js → Dashboard-CpWz2g0n.js} +2 -2
- 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-CLmYSvow.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 +213 -41
- package/src/gateways/ws/action-protocol.js +140 -0
- package/src/gateways/ws/message-dispatcher.js +5 -0
- package/src/gateways/ws/ws-server.js +21 -0
- package/src/lib/cowork-evomap-adapter.js +121 -0
- package/src/lib/cowork-observe-html.js +108 -0
- package/src/lib/cowork-observe.js +160 -0
- package/src/lib/cowork-share.js +114 -10
- package/src/lib/provider-options.js +133 -0
- package/src/lib/skill-loader.js +65 -0
- package/src/lib/sub-agent-context.js +16 -4
- 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/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-BnvARObz.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-cxfKLu-m.css +0 -1
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
- package/src/assets/web-panel/assets/index-C1SPm_5l.js +0 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-options three-layer deep merge — inspired by open-agents'
|
|
3
|
+
* getAnthropicSettings + mergeProviderOptions pattern.
|
|
4
|
+
*
|
|
5
|
+
* Resolves per-call LLM provider options as a deep merge of:
|
|
6
|
+
* 1. PROVIDER_DEFAULTS[provider] — hand-curated baseline per provider
|
|
7
|
+
* 2. MODEL_INFERENCE(modelId) — model-specific overrides (e.g. o1
|
|
8
|
+
* disables temperature, claude-opus
|
|
9
|
+
* enables extended thinking)
|
|
10
|
+
* 3. callOverrides — whatever the caller passes
|
|
11
|
+
*
|
|
12
|
+
* Later layers win at leaf keys; objects are merged recursively, arrays are
|
|
13
|
+
* replaced (not concatenated) to keep behavior predictable.
|
|
14
|
+
*
|
|
15
|
+
* @module provider-options
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ─── Layer 1: per-provider defaults ────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export const PROVIDER_DEFAULTS = Object.freeze({
|
|
21
|
+
anthropic: {
|
|
22
|
+
maxTokens: 8192,
|
|
23
|
+
temperature: 1.0,
|
|
24
|
+
anthropic: { thinking: { type: "disabled" } },
|
|
25
|
+
},
|
|
26
|
+
openai: {
|
|
27
|
+
maxTokens: 4096,
|
|
28
|
+
temperature: 0.7,
|
|
29
|
+
},
|
|
30
|
+
ollama: {
|
|
31
|
+
temperature: 0.7,
|
|
32
|
+
},
|
|
33
|
+
deepseek: {
|
|
34
|
+
maxTokens: 4096,
|
|
35
|
+
temperature: 0.7,
|
|
36
|
+
},
|
|
37
|
+
gemini: {
|
|
38
|
+
maxTokens: 8192,
|
|
39
|
+
temperature: 0.7,
|
|
40
|
+
},
|
|
41
|
+
custom: {
|
|
42
|
+
maxTokens: 4096,
|
|
43
|
+
temperature: 0.7,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── Layer 2: model-id inference ───────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Derive per-model overrides from the model id string. Pure function, no I/O.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} modelId
|
|
53
|
+
* @returns {object} partial options to merge on top of provider defaults.
|
|
54
|
+
*/
|
|
55
|
+
export function inferModelOverrides(modelId) {
|
|
56
|
+
if (!modelId || typeof modelId !== "string") return {};
|
|
57
|
+
const id = modelId.toLowerCase();
|
|
58
|
+
|
|
59
|
+
// OpenAI o1/o3 reasoning models — temperature is unsupported.
|
|
60
|
+
if (
|
|
61
|
+
id.startsWith("o1") ||
|
|
62
|
+
id.startsWith("o3") ||
|
|
63
|
+
id.includes("-o1-") ||
|
|
64
|
+
id.includes("-o3-")
|
|
65
|
+
) {
|
|
66
|
+
return { temperature: undefined, reasoning: { effort: "medium" } };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Claude Opus — enable extended thinking by default (users can turn off).
|
|
70
|
+
if (id.includes("opus-4") || id.includes("opus-3")) {
|
|
71
|
+
return {
|
|
72
|
+
maxTokens: 16384,
|
|
73
|
+
anthropic: { thinking: { type: "enabled", budgetTokens: 8000 } },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Claude Haiku — cheaper, smaller output by default.
|
|
78
|
+
if (id.includes("haiku")) {
|
|
79
|
+
return { maxTokens: 4096 };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// DeepSeek reasoner — reasoning tokens need headroom.
|
|
83
|
+
if (id.includes("deepseek-reasoner")) {
|
|
84
|
+
return { maxTokens: 8192, reasoning: { enabled: true } };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Deep merge primitive ──────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function _isPlainObject(v) {
|
|
93
|
+
return (
|
|
94
|
+
v !== null &&
|
|
95
|
+
typeof v === "object" &&
|
|
96
|
+
!Array.isArray(v) &&
|
|
97
|
+
Object.getPrototypeOf(v) === Object.prototype
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function deepMerge(...layers) {
|
|
102
|
+
const out = {};
|
|
103
|
+
for (const layer of layers) {
|
|
104
|
+
if (!_isPlainObject(layer)) continue;
|
|
105
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
106
|
+
if (value === undefined) {
|
|
107
|
+
// explicit undefined → erase from accumulator (used to disable fields)
|
|
108
|
+
delete out[key];
|
|
109
|
+
} else if (_isPlainObject(value) && _isPlainObject(out[key])) {
|
|
110
|
+
out[key] = deepMerge(out[key], value);
|
|
111
|
+
} else {
|
|
112
|
+
out[key] = value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Merge three layers into a single options object for a given LLM call.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} provider
|
|
125
|
+
* @param {string} modelId
|
|
126
|
+
* @param {object} [callOverrides]
|
|
127
|
+
* @returns {object}
|
|
128
|
+
*/
|
|
129
|
+
export function mergeProviderOptions(provider, modelId, callOverrides = {}) {
|
|
130
|
+
const defaults = PROVIDER_DEFAULTS[provider] || {};
|
|
131
|
+
const modelLayer = inferModelOverrides(modelId);
|
|
132
|
+
return deepMerge(defaults, modelLayer, callOverrides || {});
|
|
133
|
+
}
|
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;
|
|
@@ -115,17 +122,22 @@ export class SubAgentContext {
|
|
|
115
122
|
? options.extraToolDefinitions
|
|
116
123
|
: [];
|
|
117
124
|
this._externalToolDescriptors =
|
|
118
|
-
options.externalToolDescriptors &&
|
|
125
|
+
options.externalToolDescriptors &&
|
|
126
|
+
typeof options.externalToolDescriptors === "object"
|
|
119
127
|
? options.externalToolDescriptors
|
|
120
128
|
: {};
|
|
121
129
|
this._externalToolExecutors =
|
|
122
|
-
options.externalToolExecutors &&
|
|
130
|
+
options.externalToolExecutors &&
|
|
131
|
+
typeof options.externalToolExecutors === "object"
|
|
123
132
|
? options.externalToolExecutors
|
|
124
133
|
: {};
|
|
125
134
|
this._mcpClient = options.mcpClient || null;
|
|
126
135
|
|
|
127
136
|
// Build isolated system prompt
|
|
128
137
|
const basePrompt = buildSystemPrompt(this.cwd);
|
|
138
|
+
const profilePrompt = this._profile?.systemPrompt
|
|
139
|
+
? `\n\n## Profile: ${this._profile.name}\n${this._profile.systemPrompt}`
|
|
140
|
+
: "";
|
|
129
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.`;
|
|
130
142
|
const contextSection = this.inheritedContext
|
|
131
143
|
? `\n\n## Parent Context\n${this.inheritedContext}`
|
|
@@ -133,7 +145,7 @@ export class SubAgentContext {
|
|
|
133
145
|
|
|
134
146
|
this.messages.push({
|
|
135
147
|
role: "system",
|
|
136
|
-
content: basePrompt + rolePrompt + contextSection,
|
|
148
|
+
content: basePrompt + profilePrompt + rolePrompt + contextSection,
|
|
137
149
|
});
|
|
138
150
|
}
|
|
139
151
|
|
|
@@ -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 };
|