@oyasmi/pipiclaw 0.3.2 → 0.3.4
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 +60 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +111 -9
- package/dist/agent.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -4
- package/dist/main.js.map +1 -1
- package/dist/paths.d.ts +2 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +2 -0
- package/dist/paths.js.map +1 -1
- package/dist/prompt-builder.d.ts +4 -1
- package/dist/prompt-builder.d.ts.map +1 -1
- package/dist/prompt-builder.js +31 -1
- package/dist/prompt-builder.js.map +1 -1
- package/dist/store.d.ts +31 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +6 -0
- package/dist/store.js.map +1 -1
- package/dist/sub-agents.d.ts +51 -0
- package/dist/sub-agents.d.ts.map +1 -0
- package/dist/sub-agents.js +247 -0
- package/dist/sub-agents.js.map +1 -0
- package/dist/tools/bash.d.ts +4 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +3 -2
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/index.d.ts +16 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +21 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/subagent.d.ts +74 -0
- package/dist/tools/subagent.d.ts.map +1 -0
- package/dist/tools/subagent.js +282 -0
- package/dist/tools/subagent.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-builder.d.ts","sourceRoot":"","sources":["../src/prompt-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,wBAAgB,uBAAuB,CACtC,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"prompt-builder.d.ts","sourceRoot":"","sources":["../src/prompt-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,yBAAyB;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,uBAAuB,CACtC,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,aAAa,EAC5B,OAAO,GAAE,yBAA8B,GACrC,MAAM,CAoJR","sourcesContent":["import type { SandboxConfig } from \"./sandbox.js\";\n\nexport interface AppendSystemPromptOptions {\n\tsubAgentList?: string;\n}\n\nexport function buildAppendSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\tsandboxConfig: SandboxConfig,\n\toptions: AppendSystemPromptOptions = {},\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst subAgentsPath = `${workspacePath}/sub-agents`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Bash working directory: / (use cd or absolute paths)\n- Install tools with: apk add <package>\n- Your changes persist across sessions`\n\t\t: `You are running directly on the host machine.\n- Bash working directory: ${process.cwd()}\n- Be careful with system modifications`;\n\n\tconst sections: string[] = [];\n\n\tsections.push(`## Pipiclaw Runtime\nYou are running inside Pipiclaw, a DingTalk-oriented runtime built on top of pi.\n\n## Context\n- For current date/time, use: date\n- You have access to the active session context for this session.\n- Raw transcript files are cold storage. Do not assume they are preloaded.\n\n## Formatting\nUse Markdown for formatting. DingTalk AI Card supports basic Markdown:\nBold: **text**, Italic: *text*, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: [text](url)\n\n## Environment\n${envDescription}\n\n## Workspace Layout\n${workspacePath}/\n├── SOUL.md # Your identity/personality (read-only)\n├── AGENTS.md # Custom behavior instructions (read-only)\n├── MEMORY.md # Stable workspace memory (admin-managed, read on demand)\n├── sub-agents/ # Predefined sub-agent definitions\n├── skills/ # Global CLI tools you create\n├── events/ # Scheduled events\n└── ${channelId}/ # This channel\n ├── MEMORY.md # Channel durable memory (read on demand, runtime-managed)\n ├── HISTORY.md # Channel summarized history (read on demand, runtime-managed)\n ├── log.jsonl # Raw message archive (cold storage)\n ├── context.jsonl # Raw session archive (cold storage)\n ├── scratch/ # Your working directory\n └── skills/ # Channel-specific tools`);\n\n\tsections.push(`## Events\nYou can schedule events that wake you up at specific times or when external things happen. Events are JSON files in \\`${workspacePath}/events/\\`.\n\n### Event Types\n\n**Immediate** - Triggers as soon as harness sees the file.\n\\`\\`\\`json\n{\"type\": \"immediate\", \"channelId\": \"${channelId}\", \"text\": \"New event occurred\"}\n\\`\\`\\`\n\n**One-shot** - Triggers once at a specific time.\n\\`\\`\\`json\n{\"type\": \"one-shot\", \"channelId\": \"${channelId}\", \"text\": \"Reminder\", \"at\": \"2025-12-15T09:00:00+08:00\"}\n\\`\\`\\`\n\n**Periodic** - Triggers on a cron schedule.\n\\`\\`\\`json\n{\"type\": \"periodic\", \"channelId\": \"${channelId}\", \"text\": \"Check inbox\", \"schedule\": \"0 9 * * 1-5\", \"timezone\": \"${Intl.DateTimeFormat().resolvedOptions().timeZone}\"}\n\\`\\`\\`\n\n### Cron Format\n\\`minute hour day-of-month month day-of-week\\`\n\n### Creating Events\nCreate a JSON file under \\`${workspacePath}/events/\\` with the appropriate event payload.\nPrefer the file tools for creating or editing the event file. Use shell commands only when they are the clearest option.\n\n### Silent Completion\nFor periodic events where there's nothing to report, respond with just \\`[SILENT]\\`. This deletes the status message. Use this to avoid spam when periodic checks find nothing.\n\n### Limits\nMaximum 5 events can be queued.`);\n\n\tsections.push(`## Memory\nMemory files are not preloaded into session context. Read them explicitly when memory or history matters.\n\n### Files\n- Workspace memory: ${workspacePath}/MEMORY.md\n Stable shared background memory. Admin-managed. Read on demand.\n- Channel memory: ${channelPath}/MEMORY.md\n Durable channel memory. Runtime-managed via consolidation. You may update this file manually when necessary.\n- Channel history: ${channelPath}/HISTORY.md\n Summarized older channel history. Runtime-managed. Read on demand. Do not maintain this file manually during normal work.\n\n### Runtime Behavior\n- The runtime automatically consolidates channel MEMORY.md and HISTORY.md before compaction or session trimming.\n- Workspace MEMORY.md is not updated by normal runtime consolidation.\n\n### Cold Storage\n- ${channelPath}/log.jsonl is a raw archive. It is not normal memory and is not proactively loaded.\n- ${channelPath}/context.jsonl is a raw session archive. It is not normal memory and is not proactively loaded.\n\nWhen a task depends on prior decisions, preferences, or long-running work, read channel MEMORY.md and HISTORY.md first.`);\n\n\tsections.push(`## System Configuration Log\nMaintain ${workspacePath}/SYSTEM.md to log all environment modifications:\n- Installed packages (apk add, npm install, pip install)\n- Environment variables set\n- Config files modified\n- Skill dependencies installed\n\nUpdate this file whenever you modify the environment.`);\n\n\tsections.push(`## Tools\n- read: Read files\n- edit: Surgical file edits\n- write: Create or overwrite files when needed\n- bash: Run shell commands and external programs\n- subagent: Delegate a focused task to a sub-agent with its own isolated context\n\nEach tool requires a \"label\" parameter (shown to user).`);\n\n\tsections.push(`## Sub-Agents\nYou have a \\`subagent\\` tool for delegating focused work to a separate agent with an isolated context window.\n\n### Predefined Sub-Agents\nPredefined sub-agent definitions live in \\`${subAgentsPath}/\\`.\n${options.subAgentList ? `Available predefined sub-agents:\\n${options.subAgentList}` : \"Available predefined sub-agents: none\"}\n\n### Temporary Inline Sub-Agents\nIf no predefined sub-agent fits, you may define a temporary inline sub-agent directly in the \\`subagent\\` tool call by providing a focused \\`systemPrompt\\` plus optional tools, model, and budget settings.\n\nUse sub-agents when:\n- The task can be decomposed into a focused sub-problem\n- You need a fresh context for heavy file reading, shell work, or review\n- A specialized role would produce better results\n- The main conversation has grown long and you want to offload a bounded task\n\nDo not use sub-agents when:\n- The task is simple and direct\n- The task depends heavily on the full current conversation state\n- The task requires frequent user confirmation\n\nImportant rules:\n- Sub-agents cannot see your conversation history unless you include the needed context in \\`task\\`\n- The runtime injects a small fixed execution context (workspace path, channel id, sandbox), but you must still include task-specific context yourself\n- Sub-agents do not receive the \\`subagent\\` tool, so they cannot create nested agents\n- Prefer predefined sub-agents when one clearly fits\n- Use temporary inline sub-agents only when that extra flexibility is genuinely useful`);\n\n\treturn sections.join(\"\\n\\n\");\n}\n"]}
|
package/dist/prompt-builder.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export function buildAppendSystemPrompt(workspacePath, channelId, sandboxConfig) {
|
|
1
|
+
export function buildAppendSystemPrompt(workspacePath, channelId, sandboxConfig, options = {}) {
|
|
2
2
|
const channelPath = `${workspacePath}/${channelId}`;
|
|
3
|
+
const subAgentsPath = `${workspacePath}/sub-agents`;
|
|
3
4
|
const isDocker = sandboxConfig.type === "docker";
|
|
4
5
|
const envDescription = isDocker
|
|
5
6
|
? `You are running inside a Docker container (Alpine Linux).
|
|
@@ -30,6 +31,7 @@ ${workspacePath}/
|
|
|
30
31
|
├── SOUL.md # Your identity/personality (read-only)
|
|
31
32
|
├── AGENTS.md # Custom behavior instructions (read-only)
|
|
32
33
|
├── MEMORY.md # Stable workspace memory (admin-managed, read on demand)
|
|
34
|
+
├── sub-agents/ # Predefined sub-agent definitions
|
|
33
35
|
├── skills/ # Global CLI tools you create
|
|
34
36
|
├── events/ # Scheduled events
|
|
35
37
|
└── ${channelId}/ # This channel
|
|
@@ -104,8 +106,36 @@ Update this file whenever you modify the environment.`);
|
|
|
104
106
|
- edit: Surgical file edits
|
|
105
107
|
- write: Create or overwrite files when needed
|
|
106
108
|
- bash: Run shell commands and external programs
|
|
109
|
+
- subagent: Delegate a focused task to a sub-agent with its own isolated context
|
|
107
110
|
|
|
108
111
|
Each tool requires a "label" parameter (shown to user).`);
|
|
112
|
+
sections.push(`## Sub-Agents
|
|
113
|
+
You have a \`subagent\` tool for delegating focused work to a separate agent with an isolated context window.
|
|
114
|
+
|
|
115
|
+
### Predefined Sub-Agents
|
|
116
|
+
Predefined sub-agent definitions live in \`${subAgentsPath}/\`.
|
|
117
|
+
${options.subAgentList ? `Available predefined sub-agents:\n${options.subAgentList}` : "Available predefined sub-agents: none"}
|
|
118
|
+
|
|
119
|
+
### Temporary Inline Sub-Agents
|
|
120
|
+
If no predefined sub-agent fits, you may define a temporary inline sub-agent directly in the \`subagent\` tool call by providing a focused \`systemPrompt\` plus optional tools, model, and budget settings.
|
|
121
|
+
|
|
122
|
+
Use sub-agents when:
|
|
123
|
+
- The task can be decomposed into a focused sub-problem
|
|
124
|
+
- You need a fresh context for heavy file reading, shell work, or review
|
|
125
|
+
- A specialized role would produce better results
|
|
126
|
+
- The main conversation has grown long and you want to offload a bounded task
|
|
127
|
+
|
|
128
|
+
Do not use sub-agents when:
|
|
129
|
+
- The task is simple and direct
|
|
130
|
+
- The task depends heavily on the full current conversation state
|
|
131
|
+
- The task requires frequent user confirmation
|
|
132
|
+
|
|
133
|
+
Important rules:
|
|
134
|
+
- Sub-agents cannot see your conversation history unless you include the needed context in \`task\`
|
|
135
|
+
- The runtime injects a small fixed execution context (workspace path, channel id, sandbox), but you must still include task-specific context yourself
|
|
136
|
+
- Sub-agents do not receive the \`subagent\` tool, so they cannot create nested agents
|
|
137
|
+
- Prefer predefined sub-agents when one clearly fits
|
|
138
|
+
- Use temporary inline sub-agents only when that extra flexibility is genuinely useful`);
|
|
109
139
|
return sections.join("\n\n");
|
|
110
140
|
}
|
|
111
141
|
//# sourceMappingURL=prompt-builder.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-builder.js","sourceRoot":"","sources":["../src/prompt-builder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prompt-builder.js","sourceRoot":"","sources":["../src/prompt-builder.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,uBAAuB,CACtC,aAAqB,EACrB,SAAiB,EACjB,aAA4B,EAC5B,OAAO,GAA8B,EAAE,EAC9B;IACT,MAAM,WAAW,GAAG,GAAG,aAAa,IAAI,SAAS,EAAE,CAAC;IACpD,MAAM,aAAa,GAAG,GAAG,aAAa,aAAa,CAAC;IACpD,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,KAAK,QAAQ,CAAC;IAEjD,MAAM,cAAc,GAAG,QAAQ;QAC9B,CAAC,CAAC;;;uCAGmC;QACrC,CAAC,CAAC;4BACwB,OAAO,CAAC,GAAG,EAAE;uCACF,CAAC;IAEvC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,QAAQ,CAAC,IAAI,CAAC;;;;;;;;;;;;;EAab,cAAc;;;EAGd,aAAa;;;;;;;YAOT,SAAS;;;;;;gEAM2C,CAAC,CAAC;IAE3D,QAAQ,CAAC,IAAI,CAAC;wHACyG,aAAa;;;;;;sCAM/F,SAAS;;;;;qCAKV,SAAS;;;;;qCAKT,SAAS,qEAAqE,IAAI,CAAC,cAAc,EAAE,CAAC,eAAe,EAAE,CAAC,QAAQ;;;;;;;6BAOtI,aAAa;;;;;;;gCAOV,CAAC,CAAC;IAEjC,QAAQ,CAAC,IAAI,CAAC;;;;sBAIO,aAAa;;oBAEf,WAAW;;qBAEV,WAAW;;;;;;;;IAQ5B,WAAW;IACX,WAAW;;wHAEyG,CAAC,CAAC;IAEzH,QAAQ,CAAC,IAAI,CAAC;WACJ,aAAa;;;;;;sDAM8B,CAAC,CAAC;IAEvD,QAAQ,CAAC,IAAI,CAAC;;;;;;;wDAOyC,CAAC,CAAC;IAEzD,QAAQ,CAAC,IAAI,CAAC;;;;6CAI8B,aAAa;EACxD,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,qCAAqC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,uCAAuC;;;;;;;;;;;;;;;;;;;;;uFAqBvC,CAAC,CAAC;IAExF,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAAA,CAC7B","sourcesContent":["import type { SandboxConfig } from \"./sandbox.js\";\n\nexport interface AppendSystemPromptOptions {\n\tsubAgentList?: string;\n}\n\nexport function buildAppendSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\tsandboxConfig: SandboxConfig,\n\toptions: AppendSystemPromptOptions = {},\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst subAgentsPath = `${workspacePath}/sub-agents`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Bash working directory: / (use cd or absolute paths)\n- Install tools with: apk add <package>\n- Your changes persist across sessions`\n\t\t: `You are running directly on the host machine.\n- Bash working directory: ${process.cwd()}\n- Be careful with system modifications`;\n\n\tconst sections: string[] = [];\n\n\tsections.push(`## Pipiclaw Runtime\nYou are running inside Pipiclaw, a DingTalk-oriented runtime built on top of pi.\n\n## Context\n- For current date/time, use: date\n- You have access to the active session context for this session.\n- Raw transcript files are cold storage. Do not assume they are preloaded.\n\n## Formatting\nUse Markdown for formatting. DingTalk AI Card supports basic Markdown:\nBold: **text**, Italic: *text*, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: [text](url)\n\n## Environment\n${envDescription}\n\n## Workspace Layout\n${workspacePath}/\n├── SOUL.md # Your identity/personality (read-only)\n├── AGENTS.md # Custom behavior instructions (read-only)\n├── MEMORY.md # Stable workspace memory (admin-managed, read on demand)\n├── sub-agents/ # Predefined sub-agent definitions\n├── skills/ # Global CLI tools you create\n├── events/ # Scheduled events\n└── ${channelId}/ # This channel\n ├── MEMORY.md # Channel durable memory (read on demand, runtime-managed)\n ├── HISTORY.md # Channel summarized history (read on demand, runtime-managed)\n ├── log.jsonl # Raw message archive (cold storage)\n ├── context.jsonl # Raw session archive (cold storage)\n ├── scratch/ # Your working directory\n └── skills/ # Channel-specific tools`);\n\n\tsections.push(`## Events\nYou can schedule events that wake you up at specific times or when external things happen. Events are JSON files in \\`${workspacePath}/events/\\`.\n\n### Event Types\n\n**Immediate** - Triggers as soon as harness sees the file.\n\\`\\`\\`json\n{\"type\": \"immediate\", \"channelId\": \"${channelId}\", \"text\": \"New event occurred\"}\n\\`\\`\\`\n\n**One-shot** - Triggers once at a specific time.\n\\`\\`\\`json\n{\"type\": \"one-shot\", \"channelId\": \"${channelId}\", \"text\": \"Reminder\", \"at\": \"2025-12-15T09:00:00+08:00\"}\n\\`\\`\\`\n\n**Periodic** - Triggers on a cron schedule.\n\\`\\`\\`json\n{\"type\": \"periodic\", \"channelId\": \"${channelId}\", \"text\": \"Check inbox\", \"schedule\": \"0 9 * * 1-5\", \"timezone\": \"${Intl.DateTimeFormat().resolvedOptions().timeZone}\"}\n\\`\\`\\`\n\n### Cron Format\n\\`minute hour day-of-month month day-of-week\\`\n\n### Creating Events\nCreate a JSON file under \\`${workspacePath}/events/\\` with the appropriate event payload.\nPrefer the file tools for creating or editing the event file. Use shell commands only when they are the clearest option.\n\n### Silent Completion\nFor periodic events where there's nothing to report, respond with just \\`[SILENT]\\`. This deletes the status message. Use this to avoid spam when periodic checks find nothing.\n\n### Limits\nMaximum 5 events can be queued.`);\n\n\tsections.push(`## Memory\nMemory files are not preloaded into session context. Read them explicitly when memory or history matters.\n\n### Files\n- Workspace memory: ${workspacePath}/MEMORY.md\n Stable shared background memory. Admin-managed. Read on demand.\n- Channel memory: ${channelPath}/MEMORY.md\n Durable channel memory. Runtime-managed via consolidation. You may update this file manually when necessary.\n- Channel history: ${channelPath}/HISTORY.md\n Summarized older channel history. Runtime-managed. Read on demand. Do not maintain this file manually during normal work.\n\n### Runtime Behavior\n- The runtime automatically consolidates channel MEMORY.md and HISTORY.md before compaction or session trimming.\n- Workspace MEMORY.md is not updated by normal runtime consolidation.\n\n### Cold Storage\n- ${channelPath}/log.jsonl is a raw archive. It is not normal memory and is not proactively loaded.\n- ${channelPath}/context.jsonl is a raw session archive. It is not normal memory and is not proactively loaded.\n\nWhen a task depends on prior decisions, preferences, or long-running work, read channel MEMORY.md and HISTORY.md first.`);\n\n\tsections.push(`## System Configuration Log\nMaintain ${workspacePath}/SYSTEM.md to log all environment modifications:\n- Installed packages (apk add, npm install, pip install)\n- Environment variables set\n- Config files modified\n- Skill dependencies installed\n\nUpdate this file whenever you modify the environment.`);\n\n\tsections.push(`## Tools\n- read: Read files\n- edit: Surgical file edits\n- write: Create or overwrite files when needed\n- bash: Run shell commands and external programs\n- subagent: Delegate a focused task to a sub-agent with its own isolated context\n\nEach tool requires a \"label\" parameter (shown to user).`);\n\n\tsections.push(`## Sub-Agents\nYou have a \\`subagent\\` tool for delegating focused work to a separate agent with an isolated context window.\n\n### Predefined Sub-Agents\nPredefined sub-agent definitions live in \\`${subAgentsPath}/\\`.\n${options.subAgentList ? `Available predefined sub-agents:\\n${options.subAgentList}` : \"Available predefined sub-agents: none\"}\n\n### Temporary Inline Sub-Agents\nIf no predefined sub-agent fits, you may define a temporary inline sub-agent directly in the \\`subagent\\` tool call by providing a focused \\`systemPrompt\\` plus optional tools, model, and budget settings.\n\nUse sub-agents when:\n- The task can be decomposed into a focused sub-problem\n- You need a fresh context for heavy file reading, shell work, or review\n- A specialized role would produce better results\n- The main conversation has grown long and you want to offload a bounded task\n\nDo not use sub-agents when:\n- The task is simple and direct\n- The task depends heavily on the full current conversation state\n- The task requires frequent user confirmation\n\nImportant rules:\n- Sub-agents cannot see your conversation history unless you include the needed context in \\`task\\`\n- The runtime injects a small fixed execution context (workspace path, channel id, sandbox), but you must still include task-specific context yourself\n- Sub-agents do not receive the \\`subagent\\` tool, so they cannot create nested agents\n- Prefer predefined sub-agents when one clearly fits\n- Use temporary inline sub-agents only when that extra flexibility is genuinely useful`);\n\n\treturn sections.join(\"\\n\\n\");\n}\n"]}
|
package/dist/store.d.ts
CHANGED
|
@@ -12,6 +12,36 @@ export interface LoggedMessage {
|
|
|
12
12
|
export interface ChannelStoreConfig {
|
|
13
13
|
workingDir: string;
|
|
14
14
|
}
|
|
15
|
+
export interface LoggedSubAgentRun {
|
|
16
|
+
date: string;
|
|
17
|
+
toolCallId: string;
|
|
18
|
+
label: string;
|
|
19
|
+
agent: string;
|
|
20
|
+
source: "predefined" | "inline";
|
|
21
|
+
model: string;
|
|
22
|
+
tools: string[];
|
|
23
|
+
turns: number;
|
|
24
|
+
toolCalls: number;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
failed: boolean;
|
|
27
|
+
failureReason?: string;
|
|
28
|
+
output: string;
|
|
29
|
+
outputTruncated: boolean;
|
|
30
|
+
usage: {
|
|
31
|
+
input: number;
|
|
32
|
+
output: number;
|
|
33
|
+
cacheRead: number;
|
|
34
|
+
cacheWrite: number;
|
|
35
|
+
total: number;
|
|
36
|
+
cost: {
|
|
37
|
+
input: number;
|
|
38
|
+
output: number;
|
|
39
|
+
cacheRead: number;
|
|
40
|
+
cacheWrite: number;
|
|
41
|
+
total: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|
|
15
45
|
export declare class ChannelStore {
|
|
16
46
|
private workingDir;
|
|
17
47
|
private recentlyLogged;
|
|
@@ -26,6 +56,7 @@ export declare class ChannelStore {
|
|
|
26
56
|
* Returns false if message was already logged (duplicate)
|
|
27
57
|
*/
|
|
28
58
|
logMessage(channelId: string, message: LoggedMessage): Promise<boolean>;
|
|
59
|
+
logSubAgentRun(channelId: string, run: LoggedSubAgentRun): Promise<void>;
|
|
29
60
|
/**
|
|
30
61
|
* Rotate log file if it exceeds 1MB.
|
|
31
62
|
* Keeps one backup (log.jsonl.1) and resets the sync offset.
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,YAAY;IACxB,OAAO,CAAC,UAAU,CAAS;IAE3B,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,MAAM,EAAE,kBAAkB,EAOrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMvC;IAED;;;;OAIG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAwB5E;IAED;;;OAGG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/E;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiEjD;CACD","sourcesContent":["import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n\tdeliveryMode?: \"steer\" | \"followUp\";\n\tskipContextSync?: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl raw archive.\n\t * This file is cold storage and is not proactively loaded into memory context.\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size === 0) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst fd = openSync(logPath, \"r\");\n\t\t\ttry {\n\t\t\t\tlet end = stats.size;\n\t\t\t\tconst trailing = Buffer.alloc(1);\n\t\t\t\twhile (end > 0) {\n\t\t\t\t\treadSync(fd, trailing, 0, 1, end - 1);\n\t\t\t\t\tif (trailing[0] !== 0x0a && trailing[0] !== 0x0d) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tend--;\n\t\t\t\t}\n\n\t\t\t\tif (end === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst chunkSize = 4096;\n\t\t\t\tconst buffer = Buffer.alloc(chunkSize);\n\t\t\t\tlet lineStart = 0;\n\t\t\t\tlet position = end;\n\n\t\t\t\twhile (position > 0) {\n\t\t\t\t\tconst bytesToRead = Math.min(chunkSize, position);\n\t\t\t\t\tposition -= bytesToRead;\n\t\t\t\t\treadSync(fd, buffer, 0, bytesToRead, position);\n\n\t\t\t\t\tconst newlineIndex = buffer.subarray(0, bytesToRead).lastIndexOf(0x0a);\n\t\t\t\t\tif (newlineIndex !== -1) {\n\t\t\t\t\t\tlineStart = position + newlineIndex + 1;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst lineLength = end - lineStart;\n\t\t\t\tif (lineLength <= 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst lineBuffer = Buffer.alloc(lineLength);\n\t\t\t\treadSync(fd, lineBuffer, 0, lineLength, lineStart);\n\t\t\t\tconst lastLine = lineBuffer.toString(\"utf-8\").replace(/\\r+$/, \"\");\n\t\t\t\tif (!lastLine) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\t\treturn message.ts;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,GAAG,QAAQ,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE;YACL,KAAK,EAAE,MAAM,CAAC;YACd,MAAM,EAAE,MAAM,CAAC;YACf,SAAS,EAAE,MAAM,CAAC;YAClB,UAAU,EAAE,MAAM,CAAC;YACnB,KAAK,EAAE,MAAM,CAAC;SACd,CAAC;KACF,CAAC;CACF;AAED,qBAAa,YAAY;IACxB,OAAO,CAAC,UAAU,CAAS;IAE3B,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,MAAM,EAAE,kBAAkB,EAOrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMvC;IAED;;;;OAIG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAwB5E;IAEK,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAK7E;IAED;;;OAGG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/E;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiEjD;CACD","sourcesContent":["import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n\tdeliveryMode?: \"steer\" | \"followUp\";\n\tskipContextSync?: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport interface LoggedSubAgentRun {\n\tdate: string;\n\ttoolCallId: string;\n\tlabel: string;\n\tagent: string;\n\tsource: \"predefined\" | \"inline\";\n\tmodel: string;\n\ttools: string[];\n\tturns: number;\n\ttoolCalls: number;\n\tdurationMs: number;\n\tfailed: boolean;\n\tfailureReason?: string;\n\toutput: string;\n\toutputTruncated: boolean;\n\tusage: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t\tcost: {\n\t\t\tinput: number;\n\t\t\toutput: number;\n\t\t\tcacheRead: number;\n\t\t\tcacheWrite: number;\n\t\t\ttotal: number;\n\t\t};\n\t};\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl raw archive.\n\t * This file is cold storage and is not proactively loaded into memory context.\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\tasync logSubAgentRun(channelId: string, run: LoggedSubAgentRun): Promise<void> {\n\t\tconst logPath = join(this.getChannelDir(channelId), \"subagent-runs.jsonl\");\n\t\tthis.rotateIfNeeded(logPath);\n\t\tconst line = `${JSON.stringify(run)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size === 0) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst fd = openSync(logPath, \"r\");\n\t\t\ttry {\n\t\t\t\tlet end = stats.size;\n\t\t\t\tconst trailing = Buffer.alloc(1);\n\t\t\t\twhile (end > 0) {\n\t\t\t\t\treadSync(fd, trailing, 0, 1, end - 1);\n\t\t\t\t\tif (trailing[0] !== 0x0a && trailing[0] !== 0x0d) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tend--;\n\t\t\t\t}\n\n\t\t\t\tif (end === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst chunkSize = 4096;\n\t\t\t\tconst buffer = Buffer.alloc(chunkSize);\n\t\t\t\tlet lineStart = 0;\n\t\t\t\tlet position = end;\n\n\t\t\t\twhile (position > 0) {\n\t\t\t\t\tconst bytesToRead = Math.min(chunkSize, position);\n\t\t\t\t\tposition -= bytesToRead;\n\t\t\t\t\treadSync(fd, buffer, 0, bytesToRead, position);\n\n\t\t\t\t\tconst newlineIndex = buffer.subarray(0, bytesToRead).lastIndexOf(0x0a);\n\t\t\t\t\tif (newlineIndex !== -1) {\n\t\t\t\t\t\tlineStart = position + newlineIndex + 1;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst lineLength = end - lineStart;\n\t\t\t\tif (lineLength <= 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst lineBuffer = Buffer.alloc(lineLength);\n\t\t\t\treadSync(fd, lineBuffer, 0, lineLength, lineStart);\n\t\t\t\tconst lastLine = lineBuffer.toString(\"utf-8\").replace(/\\r+$/, \"\");\n\t\t\t\tif (!lastLine) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\t\treturn message.ts;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
package/dist/store.js
CHANGED
|
@@ -47,6 +47,12 @@ export class ChannelStore {
|
|
|
47
47
|
await appendFile(logPath, line, "utf-8");
|
|
48
48
|
return true;
|
|
49
49
|
}
|
|
50
|
+
async logSubAgentRun(channelId, run) {
|
|
51
|
+
const logPath = join(this.getChannelDir(channelId), "subagent-runs.jsonl");
|
|
52
|
+
this.rotateIfNeeded(logPath);
|
|
53
|
+
const line = `${JSON.stringify(run)}\n`;
|
|
54
|
+
await appendFile(logPath, line, "utf-8");
|
|
55
|
+
}
|
|
50
56
|
/**
|
|
51
57
|
* Rotate log file if it exceeds 1MB.
|
|
52
58
|
* Keeps one backup (log.jsonl.1) and resets the sync offset.
|
package/dist/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n\tdeliveryMode?: \"steer\" | \"followUp\";\n\tskipContextSync?: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl raw archive.\n\t * This file is cold storage and is not proactively loaded into memory context.\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size === 0) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst fd = openSync(logPath, \"r\");\n\t\t\ttry {\n\t\t\t\tlet end = stats.size;\n\t\t\t\tconst trailing = Buffer.alloc(1);\n\t\t\t\twhile (end > 0) {\n\t\t\t\t\treadSync(fd, trailing, 0, 1, end - 1);\n\t\t\t\t\tif (trailing[0] !== 0x0a && trailing[0] !== 0x0d) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tend--;\n\t\t\t\t}\n\n\t\t\t\tif (end === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst chunkSize = 4096;\n\t\t\t\tconst buffer = Buffer.alloc(chunkSize);\n\t\t\t\tlet lineStart = 0;\n\t\t\t\tlet position = end;\n\n\t\t\t\twhile (position > 0) {\n\t\t\t\t\tconst bytesToRead = Math.min(chunkSize, position);\n\t\t\t\t\tposition -= bytesToRead;\n\t\t\t\t\treadSync(fd, buffer, 0, bytesToRead, position);\n\n\t\t\t\t\tconst newlineIndex = buffer.subarray(0, bytesToRead).lastIndexOf(0x0a);\n\t\t\t\t\tif (newlineIndex !== -1) {\n\t\t\t\t\t\tlineStart = position + newlineIndex + 1;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst lineLength = end - lineStart;\n\t\t\t\tif (lineLength <= 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst lineBuffer = Buffer.alloc(lineLength);\n\t\t\t\treadSync(fd, lineBuffer, 0, lineLength, lineStart);\n\t\t\t\tconst lastLine = lineBuffer.toString(\"utf-8\").replace(/\\r+$/, \"\");\n\t\t\t\tif (!lastLine) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\t\treturn message.ts;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n\tdeliveryMode?: \"steer\" | \"followUp\";\n\tskipContextSync?: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport interface LoggedSubAgentRun {\n\tdate: string;\n\ttoolCallId: string;\n\tlabel: string;\n\tagent: string;\n\tsource: \"predefined\" | \"inline\";\n\tmodel: string;\n\ttools: string[];\n\tturns: number;\n\ttoolCalls: number;\n\tdurationMs: number;\n\tfailed: boolean;\n\tfailureReason?: string;\n\toutput: string;\n\toutputTruncated: boolean;\n\tusage: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t\tcost: {\n\t\t\tinput: number;\n\t\t\toutput: number;\n\t\t\tcacheRead: number;\n\t\t\tcacheWrite: number;\n\t\t\ttotal: number;\n\t\t};\n\t};\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl raw archive.\n\t * This file is cold storage and is not proactively loaded into memory context.\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\tasync logSubAgentRun(channelId: string, run: LoggedSubAgentRun): Promise<void> {\n\t\tconst logPath = join(this.getChannelDir(channelId), \"subagent-runs.jsonl\");\n\t\tthis.rotateIfNeeded(logPath);\n\t\tconst line = `${JSON.stringify(run)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size === 0) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst fd = openSync(logPath, \"r\");\n\t\t\ttry {\n\t\t\t\tlet end = stats.size;\n\t\t\t\tconst trailing = Buffer.alloc(1);\n\t\t\t\twhile (end > 0) {\n\t\t\t\t\treadSync(fd, trailing, 0, 1, end - 1);\n\t\t\t\t\tif (trailing[0] !== 0x0a && trailing[0] !== 0x0d) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tend--;\n\t\t\t\t}\n\n\t\t\t\tif (end === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst chunkSize = 4096;\n\t\t\t\tconst buffer = Buffer.alloc(chunkSize);\n\t\t\t\tlet lineStart = 0;\n\t\t\t\tlet position = end;\n\n\t\t\t\twhile (position > 0) {\n\t\t\t\t\tconst bytesToRead = Math.min(chunkSize, position);\n\t\t\t\t\tposition -= bytesToRead;\n\t\t\t\t\treadSync(fd, buffer, 0, bytesToRead, position);\n\n\t\t\t\t\tconst newlineIndex = buffer.subarray(0, bytesToRead).lastIndexOf(0x0a);\n\t\t\t\t\tif (newlineIndex !== -1) {\n\t\t\t\t\t\tlineStart = position + newlineIndex + 1;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst lineLength = end - lineStart;\n\t\t\t\tif (lineLength <= 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst lineBuffer = Buffer.alloc(lineLength);\n\t\t\t\treadSync(fd, lineBuffer, 0, lineLength, lineStart);\n\t\t\t\tconst lastLine = lineBuffer.toString(\"utf-8\").replace(/\\r+$/, \"\");\n\t\t\t\tif (!lastLine) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\t\treturn message.ts;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
2
|
+
declare const ALLOWED_SUB_AGENT_TOOLS: readonly ["read", "bash", "edit", "write"];
|
|
3
|
+
export type SubAgentToolName = (typeof ALLOWED_SUB_AGENT_TOOLS)[number];
|
|
4
|
+
export interface SubAgentConfig {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
systemPrompt: string;
|
|
8
|
+
tools: SubAgentToolName[];
|
|
9
|
+
model?: Model<Api>;
|
|
10
|
+
modelRef?: string;
|
|
11
|
+
maxTurns: number;
|
|
12
|
+
maxToolCalls: number;
|
|
13
|
+
maxWallTimeSec: number;
|
|
14
|
+
bashTimeoutSec: number;
|
|
15
|
+
filePath?: string;
|
|
16
|
+
source: "predefined" | "inline";
|
|
17
|
+
}
|
|
18
|
+
export interface ResolvedSubAgentConfig extends Omit<SubAgentConfig, "model" | "modelRef"> {
|
|
19
|
+
model: Model<Api>;
|
|
20
|
+
modelRef: string;
|
|
21
|
+
}
|
|
22
|
+
export interface SubAgentDiscoveryResult {
|
|
23
|
+
directory: string;
|
|
24
|
+
agents: SubAgentConfig[];
|
|
25
|
+
warnings: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface SubAgentInvocationOverrides {
|
|
28
|
+
agent?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
systemPrompt?: string;
|
|
31
|
+
tools?: string[];
|
|
32
|
+
model?: string;
|
|
33
|
+
maxTurns?: number;
|
|
34
|
+
maxToolCalls?: number;
|
|
35
|
+
maxWallTimeSec?: number;
|
|
36
|
+
bashTimeoutSec?: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function validateSubAgentTask(task: string): string | undefined;
|
|
39
|
+
export declare function getSubAgentsDir(workspaceDir: string): string;
|
|
40
|
+
export declare function validateToolNames(values: string[] | undefined): {
|
|
41
|
+
tools: SubAgentToolName[];
|
|
42
|
+
error?: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function discoverSubAgents(workspaceDir: string, availableModels: Model<Api>[]): SubAgentDiscoveryResult;
|
|
45
|
+
export declare function resolveSubAgentConfig(availableModels: Model<Api>[], currentModel: Model<Api>, predefinedAgents: SubAgentConfig[], overrides: SubAgentInvocationOverrides): {
|
|
46
|
+
config?: ResolvedSubAgentConfig;
|
|
47
|
+
error?: string;
|
|
48
|
+
};
|
|
49
|
+
export declare function formatSubAgentList(agents: SubAgentConfig[], maxItems?: number): string;
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=sub-agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sub-agents.d.ts","sourceRoot":"","sources":["../src/sub-agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAQtD,QAAA,MAAM,uBAAuB,4CAA6C,CAAC;AAS3E,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,GAAG,QAAQ,CAAC;CAChC;AAED,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,UAAU,CAAC;IACzF,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AASD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAErE;AAMD,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE5D;AAeD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG;IAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAuB7G;AAoCD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,uBAAuB,CAyG9G;AAED,wBAAgB,qBAAqB,CACpC,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,EAC7B,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,EACxB,gBAAgB,EAAE,cAAc,EAAE,EAClC,SAAS,EAAE,2BAA2B,GACpC;IAAE,MAAM,CAAC,EAAE,sBAAsB,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAyErD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,QAAQ,GAAE,MAAW,GAAG,MAAM,CAW1F","sourcesContent":["import type { Api, Model } from \"@mariozechner/pi-ai\";\nimport { parseFrontmatter } from \"@mariozechner/pi-coding-agent\";\nimport type { Dirent } from \"fs\";\nimport { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { findExactModelReferenceMatch, formatModelReference } from \"./model-utils.js\";\nimport { SUB_AGENTS_DIR_NAME } from \"./paths.js\";\n\nconst ALLOWED_SUB_AGENT_TOOLS = [\"read\", \"bash\", \"edit\", \"write\"] as const;\nconst DEFAULT_SUB_AGENT_TOOLS = [\"read\", \"bash\"] as const;\nconst DEFAULT_MAX_TURNS = 24;\nconst DEFAULT_MAX_TOOL_CALLS = 48;\nconst DEFAULT_MAX_WALL_TIME_SEC = 300;\nconst DEFAULT_BASH_TIMEOUT_SEC = 120;\nconst MAX_SUB_AGENT_TASK_CHARS = 12000;\nconst MAX_SUB_AGENT_SYSTEM_PROMPT_CHARS = 16000;\n\nexport type SubAgentToolName = (typeof ALLOWED_SUB_AGENT_TOOLS)[number];\n\nexport interface SubAgentConfig {\n\tname: string;\n\tdescription: string;\n\tsystemPrompt: string;\n\ttools: SubAgentToolName[];\n\tmodel?: Model<Api>;\n\tmodelRef?: string;\n\tmaxTurns: number;\n\tmaxToolCalls: number;\n\tmaxWallTimeSec: number;\n\tbashTimeoutSec: number;\n\tfilePath?: string;\n\tsource: \"predefined\" | \"inline\";\n}\n\nexport interface ResolvedSubAgentConfig extends Omit<SubAgentConfig, \"model\" | \"modelRef\"> {\n\tmodel: Model<Api>;\n\tmodelRef: string;\n}\n\nexport interface SubAgentDiscoveryResult {\n\tdirectory: string;\n\tagents: SubAgentConfig[];\n\twarnings: string[];\n}\n\nexport interface SubAgentInvocationOverrides {\n\tagent?: string;\n\tname?: string;\n\tsystemPrompt?: string;\n\ttools?: string[];\n\tmodel?: string;\n\tmaxTurns?: number;\n\tmaxToolCalls?: number;\n\tmaxWallTimeSec?: number;\n\tbashTimeoutSec?: number;\n}\n\nfunction validateTextLength(value: string, maxChars: number, label: string): string | undefined {\n\tif (value.length <= maxChars) {\n\t\treturn undefined;\n\t}\n\treturn `${label} exceeds ${maxChars} characters (got ${value.length}).`;\n}\n\nexport function validateSubAgentTask(task: string): string | undefined {\n\treturn validateTextLength(task, MAX_SUB_AGENT_TASK_CHARS, \"Sub-agent task\");\n}\n\nfunction validateSubAgentSystemPrompt(systemPrompt: string, label: string): string | undefined {\n\treturn validateTextLength(systemPrompt, MAX_SUB_AGENT_SYSTEM_PROMPT_CHARS, label);\n}\n\nexport function getSubAgentsDir(workspaceDir: string): string {\n\treturn join(workspaceDir, SUB_AGENTS_DIR_NAME);\n}\n\nfunction parseToolNames(raw: string | undefined): { tools: SubAgentToolName[]; error?: string } {\n\tif (!raw || !raw.trim()) {\n\t\treturn { tools: [...DEFAULT_SUB_AGENT_TOOLS] };\n\t}\n\n\tconst values = raw\n\t\t.split(\",\")\n\t\t.map((value) => value.trim())\n\t\t.filter((value) => value.length > 0);\n\n\treturn validateToolNames(values);\n}\n\nexport function validateToolNames(values: string[] | undefined): { tools: SubAgentToolName[]; error?: string } {\n\tif (!values || values.length === 0) {\n\t\treturn { tools: [...DEFAULT_SUB_AGENT_TOOLS] };\n\t}\n\n\tconst tools: SubAgentToolName[] = [];\n\tconst seen = new Set<string>();\n\tfor (const value of values) {\n\t\tconst normalized = value.trim();\n\t\tif (!normalized || seen.has(normalized)) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!ALLOWED_SUB_AGENT_TOOLS.includes(normalized as SubAgentToolName)) {\n\t\t\treturn {\n\t\t\t\ttools: [],\n\t\t\t\terror: `Unknown tool \"${normalized}\". Allowed tools: ${ALLOWED_SUB_AGENT_TOOLS.join(\", \")}`,\n\t\t\t};\n\t\t}\n\t\tseen.add(normalized);\n\t\ttools.push(normalized as SubAgentToolName);\n\t}\n\n\treturn { tools: tools.length > 0 ? tools : [...DEFAULT_SUB_AGENT_TOOLS] };\n}\n\nfunction parsePositiveInteger(raw: string | undefined, fallback: number): { value: number; warning?: string } {\n\tif (!raw || !raw.trim()) {\n\t\treturn { value: fallback };\n\t}\n\n\tconst parsed = Number.parseInt(raw.trim(), 10);\n\tif (!Number.isFinite(parsed) || parsed <= 0) {\n\t\treturn { value: fallback, warning: `Invalid numeric value \"${raw}\", using default ${fallback}` };\n\t}\n\n\treturn { value: parsed };\n}\n\nfunction resolvePositiveOverride(value: number | undefined, fallback: number): number {\n\tif (!Number.isFinite(value) || value === undefined || value <= 0) {\n\t\treturn fallback;\n\t}\n\treturn Math.floor(value);\n}\n\nfunction resolveModelReference(\n\tmodelRef: string,\n\tavailableModels: Model<Api>[],\n): { model?: Model<Api>; error?: string } {\n\tconst { match, ambiguous } = findExactModelReferenceMatch(modelRef, availableModels);\n\tif (match) {\n\t\treturn { model: match };\n\t}\n\tif (ambiguous) {\n\t\treturn { error: `Model reference \"${modelRef}\" is ambiguous. Use provider/modelId.` };\n\t}\n\treturn { error: `Model reference \"${modelRef}\" was not found among available models.` };\n}\n\nexport function discoverSubAgents(workspaceDir: string, availableModels: Model<Api>[]): SubAgentDiscoveryResult {\n\tconst directory = getSubAgentsDir(workspaceDir);\n\tif (!existsSync(directory)) {\n\t\treturn { directory, agents: [], warnings: [] };\n\t}\n\n\tconst warnings: string[] = [];\n\tconst agents: SubAgentConfig[] = [];\n\tconst seenNames = new Set<string>();\n\tlet entries: Dirent<string>[];\n\ttry {\n\t\tentries = readdirSync(directory, { withFileTypes: true })\n\t\t\t.filter((entry) => entry.name.endsWith(\".md\") && (entry.isFile() || entry.isSymbolicLink()))\n\t\t\t.sort((a, b) => a.name.localeCompare(b.name));\n\t} catch (error) {\n\t\treturn {\n\t\t\tdirectory,\n\t\t\tagents: [],\n\t\t\twarnings: [`Failed to read sub-agents directory (${error instanceof Error ? error.message : String(error)})`],\n\t\t};\n\t}\n\n\tfor (const entry of entries) {\n\t\tconst filePath = join(directory, entry.name);\n\t\tlet content = \"\";\n\t\ttry {\n\t\t\tcontent = readFileSync(filePath, \"utf-8\");\n\t\t} catch (error) {\n\t\t\twarnings.push(\n\t\t\t\t`${entry.name}: failed to read file (${error instanceof Error ? error.message : String(error)})`,\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);\n\t\tconst name = frontmatter.name?.trim();\n\t\tconst description = frontmatter.description?.trim();\n\n\t\tif (!name || !description) {\n\t\t\twarnings.push(`${entry.name}: missing required frontmatter fields \"name\" or \"description\"`);\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (seenNames.has(name)) {\n\t\t\twarnings.push(`${entry.name}: duplicate sub-agent name \"${name}\" ignored`);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst toolParse = parseToolNames(frontmatter.tools);\n\t\tif (toolParse.error) {\n\t\t\twarnings.push(`${entry.name}: ${toolParse.error}`);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst maxTurns = parsePositiveInteger(frontmatter.maxTurns, DEFAULT_MAX_TURNS);\n\t\tconst maxToolCalls = parsePositiveInteger(frontmatter.maxToolCalls, DEFAULT_MAX_TOOL_CALLS);\n\t\tconst maxWallTimeSec = parsePositiveInteger(frontmatter.maxWallTimeSec, DEFAULT_MAX_WALL_TIME_SEC);\n\t\tconst bashTimeoutSec = parsePositiveInteger(frontmatter.bashTimeoutSec, DEFAULT_BASH_TIMEOUT_SEC);\n\n\t\tfor (const warning of [maxTurns.warning, maxToolCalls.warning, maxWallTimeSec.warning, bashTimeoutSec.warning]) {\n\t\t\tif (warning) {\n\t\t\t\twarnings.push(`${entry.name}: ${warning}`);\n\t\t\t}\n\t\t}\n\n\t\tconst modelRef = frontmatter.model?.trim();\n\t\tlet model: Model<Api> | undefined;\n\t\tif (modelRef) {\n\t\t\tconst resolved = resolveModelReference(modelRef, availableModels);\n\t\t\tif (!resolved.model) {\n\t\t\t\twarnings.push(`${entry.name}: ${resolved.error}`);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tmodel = resolved.model;\n\t\t}\n\n\t\tconst trimmedBody = body.trim();\n\t\tif (!trimmedBody) {\n\t\t\twarnings.push(`${entry.name}: empty system prompt body`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst promptLengthError = validateSubAgentSystemPrompt(trimmedBody, \"Sub-agent system prompt\");\n\t\tif (promptLengthError) {\n\t\t\twarnings.push(`${entry.name}: ${promptLengthError}`);\n\t\t\tcontinue;\n\t\t}\n\n\t\tseenNames.add(name);\n\t\tagents.push({\n\t\t\tname,\n\t\t\tdescription,\n\t\t\tsystemPrompt: trimmedBody,\n\t\t\ttools: toolParse.tools,\n\t\t\tmodel,\n\t\t\tmodelRef: modelRef || (model ? formatModelReference(model) : undefined),\n\t\t\tmaxTurns: maxTurns.value,\n\t\t\tmaxToolCalls: maxToolCalls.value,\n\t\t\tmaxWallTimeSec: maxWallTimeSec.value,\n\t\t\tbashTimeoutSec: bashTimeoutSec.value,\n\t\t\tfilePath,\n\t\t\tsource: \"predefined\",\n\t\t});\n\t}\n\n\treturn { directory, agents, warnings };\n}\n\nexport function resolveSubAgentConfig(\n\tavailableModels: Model<Api>[],\n\tcurrentModel: Model<Api>,\n\tpredefinedAgents: SubAgentConfig[],\n\toverrides: SubAgentInvocationOverrides,\n): { config?: ResolvedSubAgentConfig; error?: string } {\n\tconst baseConfig = overrides.agent ? predefinedAgents.find((agent) => agent.name === overrides.agent) : undefined;\n\tif (overrides.agent && !baseConfig) {\n\t\tconst available = predefinedAgents.length > 0 ? predefinedAgents.map((agent) => agent.name).join(\", \") : \"none\";\n\t\treturn { error: `Unknown sub-agent \"${overrides.agent}\". Available sub-agents: ${available}.` };\n\t}\n\n\tif (!baseConfig && (!overrides.systemPrompt || !overrides.systemPrompt.trim())) {\n\t\treturn { error: 'Provide either \"agent\" or \"systemPrompt\" to define the sub-agent.' };\n\t}\n\n\tconst tools = overrides.tools\n\t\t? validateToolNames(overrides.tools)\n\t\t: { tools: baseConfig?.tools ?? [...DEFAULT_SUB_AGENT_TOOLS] };\n\tif (tools.error) {\n\t\treturn { error: tools.error };\n\t}\n\n\tlet model = baseConfig?.model;\n\tlet modelRef = baseConfig?.modelRef;\n\tif (overrides.model?.trim()) {\n\t\tconst resolved = resolveModelReference(overrides.model.trim(), availableModels);\n\t\tif (!resolved.model) {\n\t\t\treturn { error: resolved.error };\n\t\t}\n\t\tmodel = resolved.model;\n\t\tmodelRef = formatModelReference(resolved.model);\n\t}\n\n\tconst maxTurns = resolvePositiveOverride(overrides.maxTurns, baseConfig?.maxTurns ?? DEFAULT_MAX_TURNS);\n\tconst maxToolCalls = resolvePositiveOverride(\n\t\toverrides.maxToolCalls,\n\t\tbaseConfig?.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS,\n\t);\n\tconst maxWallTimeSec = resolvePositiveOverride(\n\t\toverrides.maxWallTimeSec,\n\t\tbaseConfig?.maxWallTimeSec ?? DEFAULT_MAX_WALL_TIME_SEC,\n\t);\n\tconst bashTimeoutSec = resolvePositiveOverride(\n\t\toverrides.bashTimeoutSec,\n\t\tbaseConfig?.bashTimeoutSec ?? DEFAULT_BASH_TIMEOUT_SEC,\n\t);\n\n\tconst systemPrompt = overrides.systemPrompt?.trim() || baseConfig?.systemPrompt || \"\";\n\tif (!systemPrompt) {\n\t\treturn { error: \"Sub-agent system prompt cannot be empty.\" };\n\t}\n\tif (overrides.systemPrompt?.trim()) {\n\t\tconst promptLengthError = validateSubAgentSystemPrompt(\n\t\t\toverrides.systemPrompt.trim(),\n\t\t\t\"Inline sub-agent systemPrompt\",\n\t\t);\n\t\tif (promptLengthError) {\n\t\t\treturn { error: promptLengthError };\n\t\t}\n\t}\n\n\treturn {\n\t\tconfig: {\n\t\t\tname: overrides.name?.trim() || baseConfig?.name || \"dynamic-subagent\",\n\t\t\tdescription: baseConfig?.description || \"Inline sub-agent\",\n\t\t\tsystemPrompt,\n\t\t\ttools: tools.tools,\n\t\t\tmodel: model ?? currentModel,\n\t\t\tmodelRef: modelRef ?? formatModelReference(model ?? currentModel),\n\t\t\tmaxTurns,\n\t\t\tmaxToolCalls,\n\t\t\tmaxWallTimeSec,\n\t\t\tbashTimeoutSec,\n\t\t\tfilePath: baseConfig?.filePath,\n\t\t\tsource: baseConfig ? \"predefined\" : \"inline\",\n\t\t},\n\t};\n}\n\nexport function formatSubAgentList(agents: SubAgentConfig[], maxItems: number = 12): string {\n\tif (agents.length === 0) {\n\t\treturn \"none\";\n\t}\n\n\tconst listed = agents.slice(0, maxItems).map((agent) => `- \\`${agent.name}\\`: ${agent.description}`);\n\tif (agents.length <= maxItems) {\n\t\treturn listed.join(\"\\n\");\n\t}\n\n\treturn `${listed.join(\"\\n\")}\\n- ... and ${agents.length - maxItems} more`;\n}\n"]}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { findExactModelReferenceMatch, formatModelReference } from "./model-utils.js";
|
|
5
|
+
import { SUB_AGENTS_DIR_NAME } from "./paths.js";
|
|
6
|
+
const ALLOWED_SUB_AGENT_TOOLS = ["read", "bash", "edit", "write"];
|
|
7
|
+
const DEFAULT_SUB_AGENT_TOOLS = ["read", "bash"];
|
|
8
|
+
const DEFAULT_MAX_TURNS = 24;
|
|
9
|
+
const DEFAULT_MAX_TOOL_CALLS = 48;
|
|
10
|
+
const DEFAULT_MAX_WALL_TIME_SEC = 300;
|
|
11
|
+
const DEFAULT_BASH_TIMEOUT_SEC = 120;
|
|
12
|
+
const MAX_SUB_AGENT_TASK_CHARS = 12000;
|
|
13
|
+
const MAX_SUB_AGENT_SYSTEM_PROMPT_CHARS = 16000;
|
|
14
|
+
function validateTextLength(value, maxChars, label) {
|
|
15
|
+
if (value.length <= maxChars) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return `${label} exceeds ${maxChars} characters (got ${value.length}).`;
|
|
19
|
+
}
|
|
20
|
+
export function validateSubAgentTask(task) {
|
|
21
|
+
return validateTextLength(task, MAX_SUB_AGENT_TASK_CHARS, "Sub-agent task");
|
|
22
|
+
}
|
|
23
|
+
function validateSubAgentSystemPrompt(systemPrompt, label) {
|
|
24
|
+
return validateTextLength(systemPrompt, MAX_SUB_AGENT_SYSTEM_PROMPT_CHARS, label);
|
|
25
|
+
}
|
|
26
|
+
export function getSubAgentsDir(workspaceDir) {
|
|
27
|
+
return join(workspaceDir, SUB_AGENTS_DIR_NAME);
|
|
28
|
+
}
|
|
29
|
+
function parseToolNames(raw) {
|
|
30
|
+
if (!raw || !raw.trim()) {
|
|
31
|
+
return { tools: [...DEFAULT_SUB_AGENT_TOOLS] };
|
|
32
|
+
}
|
|
33
|
+
const values = raw
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((value) => value.trim())
|
|
36
|
+
.filter((value) => value.length > 0);
|
|
37
|
+
return validateToolNames(values);
|
|
38
|
+
}
|
|
39
|
+
export function validateToolNames(values) {
|
|
40
|
+
if (!values || values.length === 0) {
|
|
41
|
+
return { tools: [...DEFAULT_SUB_AGENT_TOOLS] };
|
|
42
|
+
}
|
|
43
|
+
const tools = [];
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
for (const value of values) {
|
|
46
|
+
const normalized = value.trim();
|
|
47
|
+
if (!normalized || seen.has(normalized)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!ALLOWED_SUB_AGENT_TOOLS.includes(normalized)) {
|
|
51
|
+
return {
|
|
52
|
+
tools: [],
|
|
53
|
+
error: `Unknown tool "${normalized}". Allowed tools: ${ALLOWED_SUB_AGENT_TOOLS.join(", ")}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
seen.add(normalized);
|
|
57
|
+
tools.push(normalized);
|
|
58
|
+
}
|
|
59
|
+
return { tools: tools.length > 0 ? tools : [...DEFAULT_SUB_AGENT_TOOLS] };
|
|
60
|
+
}
|
|
61
|
+
function parsePositiveInteger(raw, fallback) {
|
|
62
|
+
if (!raw || !raw.trim()) {
|
|
63
|
+
return { value: fallback };
|
|
64
|
+
}
|
|
65
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
66
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
67
|
+
return { value: fallback, warning: `Invalid numeric value "${raw}", using default ${fallback}` };
|
|
68
|
+
}
|
|
69
|
+
return { value: parsed };
|
|
70
|
+
}
|
|
71
|
+
function resolvePositiveOverride(value, fallback) {
|
|
72
|
+
if (!Number.isFinite(value) || value === undefined || value <= 0) {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
return Math.floor(value);
|
|
76
|
+
}
|
|
77
|
+
function resolveModelReference(modelRef, availableModels) {
|
|
78
|
+
const { match, ambiguous } = findExactModelReferenceMatch(modelRef, availableModels);
|
|
79
|
+
if (match) {
|
|
80
|
+
return { model: match };
|
|
81
|
+
}
|
|
82
|
+
if (ambiguous) {
|
|
83
|
+
return { error: `Model reference "${modelRef}" is ambiguous. Use provider/modelId.` };
|
|
84
|
+
}
|
|
85
|
+
return { error: `Model reference "${modelRef}" was not found among available models.` };
|
|
86
|
+
}
|
|
87
|
+
export function discoverSubAgents(workspaceDir, availableModels) {
|
|
88
|
+
const directory = getSubAgentsDir(workspaceDir);
|
|
89
|
+
if (!existsSync(directory)) {
|
|
90
|
+
return { directory, agents: [], warnings: [] };
|
|
91
|
+
}
|
|
92
|
+
const warnings = [];
|
|
93
|
+
const agents = [];
|
|
94
|
+
const seenNames = new Set();
|
|
95
|
+
let entries;
|
|
96
|
+
try {
|
|
97
|
+
entries = readdirSync(directory, { withFileTypes: true })
|
|
98
|
+
.filter((entry) => entry.name.endsWith(".md") && (entry.isFile() || entry.isSymbolicLink()))
|
|
99
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
directory,
|
|
104
|
+
agents: [],
|
|
105
|
+
warnings: [`Failed to read sub-agents directory (${error instanceof Error ? error.message : String(error)})`],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
const filePath = join(directory, entry.name);
|
|
110
|
+
let content = "";
|
|
111
|
+
try {
|
|
112
|
+
content = readFileSync(filePath, "utf-8");
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
warnings.push(`${entry.name}: failed to read file (${error instanceof Error ? error.message : String(error)})`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
119
|
+
const name = frontmatter.name?.trim();
|
|
120
|
+
const description = frontmatter.description?.trim();
|
|
121
|
+
if (!name || !description) {
|
|
122
|
+
warnings.push(`${entry.name}: missing required frontmatter fields "name" or "description"`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (seenNames.has(name)) {
|
|
126
|
+
warnings.push(`${entry.name}: duplicate sub-agent name "${name}" ignored`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const toolParse = parseToolNames(frontmatter.tools);
|
|
130
|
+
if (toolParse.error) {
|
|
131
|
+
warnings.push(`${entry.name}: ${toolParse.error}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const maxTurns = parsePositiveInteger(frontmatter.maxTurns, DEFAULT_MAX_TURNS);
|
|
135
|
+
const maxToolCalls = parsePositiveInteger(frontmatter.maxToolCalls, DEFAULT_MAX_TOOL_CALLS);
|
|
136
|
+
const maxWallTimeSec = parsePositiveInteger(frontmatter.maxWallTimeSec, DEFAULT_MAX_WALL_TIME_SEC);
|
|
137
|
+
const bashTimeoutSec = parsePositiveInteger(frontmatter.bashTimeoutSec, DEFAULT_BASH_TIMEOUT_SEC);
|
|
138
|
+
for (const warning of [maxTurns.warning, maxToolCalls.warning, maxWallTimeSec.warning, bashTimeoutSec.warning]) {
|
|
139
|
+
if (warning) {
|
|
140
|
+
warnings.push(`${entry.name}: ${warning}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const modelRef = frontmatter.model?.trim();
|
|
144
|
+
let model;
|
|
145
|
+
if (modelRef) {
|
|
146
|
+
const resolved = resolveModelReference(modelRef, availableModels);
|
|
147
|
+
if (!resolved.model) {
|
|
148
|
+
warnings.push(`${entry.name}: ${resolved.error}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
model = resolved.model;
|
|
152
|
+
}
|
|
153
|
+
const trimmedBody = body.trim();
|
|
154
|
+
if (!trimmedBody) {
|
|
155
|
+
warnings.push(`${entry.name}: empty system prompt body`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const promptLengthError = validateSubAgentSystemPrompt(trimmedBody, "Sub-agent system prompt");
|
|
159
|
+
if (promptLengthError) {
|
|
160
|
+
warnings.push(`${entry.name}: ${promptLengthError}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
seenNames.add(name);
|
|
164
|
+
agents.push({
|
|
165
|
+
name,
|
|
166
|
+
description,
|
|
167
|
+
systemPrompt: trimmedBody,
|
|
168
|
+
tools: toolParse.tools,
|
|
169
|
+
model,
|
|
170
|
+
modelRef: modelRef || (model ? formatModelReference(model) : undefined),
|
|
171
|
+
maxTurns: maxTurns.value,
|
|
172
|
+
maxToolCalls: maxToolCalls.value,
|
|
173
|
+
maxWallTimeSec: maxWallTimeSec.value,
|
|
174
|
+
bashTimeoutSec: bashTimeoutSec.value,
|
|
175
|
+
filePath,
|
|
176
|
+
source: "predefined",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return { directory, agents, warnings };
|
|
180
|
+
}
|
|
181
|
+
export function resolveSubAgentConfig(availableModels, currentModel, predefinedAgents, overrides) {
|
|
182
|
+
const baseConfig = overrides.agent ? predefinedAgents.find((agent) => agent.name === overrides.agent) : undefined;
|
|
183
|
+
if (overrides.agent && !baseConfig) {
|
|
184
|
+
const available = predefinedAgents.length > 0 ? predefinedAgents.map((agent) => agent.name).join(", ") : "none";
|
|
185
|
+
return { error: `Unknown sub-agent "${overrides.agent}". Available sub-agents: ${available}.` };
|
|
186
|
+
}
|
|
187
|
+
if (!baseConfig && (!overrides.systemPrompt || !overrides.systemPrompt.trim())) {
|
|
188
|
+
return { error: 'Provide either "agent" or "systemPrompt" to define the sub-agent.' };
|
|
189
|
+
}
|
|
190
|
+
const tools = overrides.tools
|
|
191
|
+
? validateToolNames(overrides.tools)
|
|
192
|
+
: { tools: baseConfig?.tools ?? [...DEFAULT_SUB_AGENT_TOOLS] };
|
|
193
|
+
if (tools.error) {
|
|
194
|
+
return { error: tools.error };
|
|
195
|
+
}
|
|
196
|
+
let model = baseConfig?.model;
|
|
197
|
+
let modelRef = baseConfig?.modelRef;
|
|
198
|
+
if (overrides.model?.trim()) {
|
|
199
|
+
const resolved = resolveModelReference(overrides.model.trim(), availableModels);
|
|
200
|
+
if (!resolved.model) {
|
|
201
|
+
return { error: resolved.error };
|
|
202
|
+
}
|
|
203
|
+
model = resolved.model;
|
|
204
|
+
modelRef = formatModelReference(resolved.model);
|
|
205
|
+
}
|
|
206
|
+
const maxTurns = resolvePositiveOverride(overrides.maxTurns, baseConfig?.maxTurns ?? DEFAULT_MAX_TURNS);
|
|
207
|
+
const maxToolCalls = resolvePositiveOverride(overrides.maxToolCalls, baseConfig?.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS);
|
|
208
|
+
const maxWallTimeSec = resolvePositiveOverride(overrides.maxWallTimeSec, baseConfig?.maxWallTimeSec ?? DEFAULT_MAX_WALL_TIME_SEC);
|
|
209
|
+
const bashTimeoutSec = resolvePositiveOverride(overrides.bashTimeoutSec, baseConfig?.bashTimeoutSec ?? DEFAULT_BASH_TIMEOUT_SEC);
|
|
210
|
+
const systemPrompt = overrides.systemPrompt?.trim() || baseConfig?.systemPrompt || "";
|
|
211
|
+
if (!systemPrompt) {
|
|
212
|
+
return { error: "Sub-agent system prompt cannot be empty." };
|
|
213
|
+
}
|
|
214
|
+
if (overrides.systemPrompt?.trim()) {
|
|
215
|
+
const promptLengthError = validateSubAgentSystemPrompt(overrides.systemPrompt.trim(), "Inline sub-agent systemPrompt");
|
|
216
|
+
if (promptLengthError) {
|
|
217
|
+
return { error: promptLengthError };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
config: {
|
|
222
|
+
name: overrides.name?.trim() || baseConfig?.name || "dynamic-subagent",
|
|
223
|
+
description: baseConfig?.description || "Inline sub-agent",
|
|
224
|
+
systemPrompt,
|
|
225
|
+
tools: tools.tools,
|
|
226
|
+
model: model ?? currentModel,
|
|
227
|
+
modelRef: modelRef ?? formatModelReference(model ?? currentModel),
|
|
228
|
+
maxTurns,
|
|
229
|
+
maxToolCalls,
|
|
230
|
+
maxWallTimeSec,
|
|
231
|
+
bashTimeoutSec,
|
|
232
|
+
filePath: baseConfig?.filePath,
|
|
233
|
+
source: baseConfig ? "predefined" : "inline",
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
export function formatSubAgentList(agents, maxItems = 12) {
|
|
238
|
+
if (agents.length === 0) {
|
|
239
|
+
return "none";
|
|
240
|
+
}
|
|
241
|
+
const listed = agents.slice(0, maxItems).map((agent) => `- \`${agent.name}\`: ${agent.description}`);
|
|
242
|
+
if (agents.length <= maxItems) {
|
|
243
|
+
return listed.join("\n");
|
|
244
|
+
}
|
|
245
|
+
return `${listed.join("\n")}\n- ... and ${agents.length - maxItems} more`;
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=sub-agents.js.map
|