chainlesschain 0.46.0 → 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.
Files changed (57) hide show
  1. package/README.md +10 -0
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-C1AnPdMx.js → Analytics-DgypYeUB.js} +2 -2
  5. package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +1 -0
  6. package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +1 -0
  7. package/src/assets/web-panel/assets/{Backup-D31iZX3l.js → Backup-Ba9UybpT.js} +1 -1
  8. package/src/assets/web-panel/assets/{Chat-DiXJ3TuK.js → Chat-BwXskT21.js} +1 -1
  9. package/src/assets/web-panel/assets/{Cowork-B8ZDdRm4.js → Cowork-UmOe7qvE.js} +1 -1
  10. package/src/assets/web-panel/assets/{Cron-DBt1ueXh.js → Cron-JHS-rc-4.js} +2 -2
  11. package/src/assets/web-panel/assets/{Dashboard-jt6XPIjB.js → Dashboard-B95cMCO7.js} +1 -1
  12. package/src/assets/web-panel/assets/{Git-hwQ1oZHj.js → Git-CSYO0_zk.js} +2 -2
  13. package/src/assets/web-panel/assets/{Logs-4D9p6PRM.js → Logs-Hxw_K0km.js} +2 -2
  14. package/src/assets/web-panel/assets/{McpTools-CyAUjbbs.js → McpTools-DIE75TrB.js} +2 -2
  15. package/src/assets/web-panel/assets/{Memory-BMqOR7S-.js → Memory-C4KVnLlp.js} +2 -2
  16. package/src/assets/web-panel/assets/{Notes-Cmas8i4E.js → Notes-DuzrHMAk.js} +2 -2
  17. package/src/assets/web-panel/assets/{Organization-DnSa58Tl.js → Organization-DTq6uF82.js} +4 -4
  18. package/src/assets/web-panel/assets/{P2P-BxksIBWs.js → P2P-C0hjlhsR.js} +2 -2
  19. package/src/assets/web-panel/assets/{Permissions-Bq5Qn2s3.js → Permissions-Ec0NH-xC.js} +4 -4
  20. package/src/assets/web-panel/assets/{Projects-B7EM0uPg.js → Projects-U8D0asCS.js} +2 -2
  21. package/src/assets/web-panel/assets/{Providers-DAwgG5KV.js → Providers-BngtTLvJ.js} +2 -2
  22. package/src/assets/web-panel/assets/{RssFeed-HSZoRXvS.js → RssFeed-B9NbwCKM.js} +3 -3
  23. package/src/assets/web-panel/assets/{Security-Cz17qBny.js → Security-BL5Rkr1T.js} +3 -3
  24. package/src/assets/web-panel/assets/{Services-D2EsLq-v.js → Services-D4MJzLld.js} +2 -2
  25. package/src/assets/web-panel/assets/{Skills-C9v-f3vZ.js → Skills-CQTOMDwF.js} +1 -1
  26. package/src/assets/web-panel/assets/{Tasks-yMEcU0n7.js → Tasks-DepbJMnL.js} +1 -1
  27. package/src/assets/web-panel/assets/{Templates-l7SvlKuB.js → Templates-C24PVZPu.js} +1 -1
  28. package/src/assets/web-panel/assets/{Wallet-BHWhLWn9.js → Wallet-PQoSpN_P.js} +3 -3
  29. package/src/assets/web-panel/assets/{WebAuthn-kWhFYaUK.js → WebAuthn-BcuyQ4Lr.js} +4 -4
  30. package/src/assets/web-panel/assets/WorkflowEditor-C-SvXbHW.js +1 -0
  31. package/src/assets/web-panel/assets/WorkflowEditor-D5bX6woe.css +1 -0
  32. package/src/assets/web-panel/assets/{antd-D6h4fDFf.js → antd-DEjZPGMj.js} +82 -82
  33. package/src/assets/web-panel/assets/index-CwvzTTw_.js +2 -0
  34. package/src/assets/web-panel/assets/{markdown-BZsB-Dsv.js → markdown-CusdXFxb.js} +1 -1
  35. package/src/assets/web-panel/index.html +2 -2
  36. package/src/commands/cowork.js +213 -41
  37. package/src/gateways/ws/action-protocol.js +140 -0
  38. package/src/gateways/ws/message-dispatcher.js +5 -0
  39. package/src/gateways/ws/ws-server.js +21 -0
  40. package/src/lib/cowork-evomap-adapter.js +121 -0
  41. package/src/lib/cowork-observe-html.js +108 -0
  42. package/src/lib/cowork-observe.js +160 -0
  43. package/src/lib/cowork-share.js +114 -10
  44. package/src/lib/provider-options.js +133 -0
  45. package/src/lib/skill-loader.js +65 -0
  46. package/src/lib/sub-agent-context.js +16 -4
  47. package/src/lib/sub-agent-profiles.js +164 -0
  48. package/src/lib/todo-manager.js +108 -0
  49. package/src/lib/turn-context.js +95 -0
  50. package/src/lib/web-fetch.js +224 -0
  51. package/src/repl/agent-repl.js +4 -0
  52. package/src/runtime/agent-core.js +135 -3
  53. package/src/runtime/coding-agent-contract-shared.cjs +131 -0
  54. package/src/runtime/coding-agent-policy.cjs +30 -0
  55. package/src/assets/web-panel/assets/AppLayout-BnvARObz.js +0 -1
  56. package/src/assets/web-panel/assets/AppLayout-cxfKLu-m.css +0 -1
  57. 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
+ }
@@ -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
- this.maxIterations = options.maxIterations || DEFAULT_MAX_ITERATIONS;
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 && typeof options.externalToolDescriptors === "object"
125
+ options.externalToolDescriptors &&
126
+ typeof options.externalToolDescriptors === "object"
119
127
  ? options.externalToolDescriptors
120
128
  : {};
121
129
  this._externalToolExecutors =
122
- options.externalToolExecutors && typeof options.externalToolExecutors === "object"
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 };