@meowlynxsea/koi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +34 -0
- package/NOTICE +35 -0
- package/README.md +15 -0
- package/bin/koi +12 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/main.js +489918 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/package.json +51 -0
- package/src/agent/check-permissions.ts +239 -0
- package/src/agent/hooks/message-utils.ts +305 -0
- package/src/agent/hooks/types.ts +32 -0
- package/src/agent/hooks.ts +1560 -0
- package/src/agent/mode.ts +163 -0
- package/src/agent/monitor-registry.ts +308 -0
- package/src/agent/permission-ui.ts +71 -0
- package/src/agent/plan-ui.ts +74 -0
- package/src/agent/question-ui.ts +58 -0
- package/src/agent/session-fork.ts +299 -0
- package/src/agent/session-snapshots.ts +216 -0
- package/src/agent/session-store.ts +649 -0
- package/src/agent/session-tasks.ts +305 -0
- package/src/agent/session.ts +27 -0
- package/src/agent/subagent-registry.ts +176 -0
- package/src/agent/subagent.ts +194 -0
- package/src/agent/tool-orchestration.ts +55 -0
- package/src/agent/tools.ts +8 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/commands.ts +5 -0
- package/src/commands/skills/index.ts +23 -0
- package/src/config/models.ts +6 -0
- package/src/config/settings.ts +392 -0
- package/src/main.tsx +64 -0
- package/src/services/mcp/client.ts +194 -0
- package/src/services/mcp/config.ts +232 -0
- package/src/services/mcp/connection-manager.ts +258 -0
- package/src/services/mcp/index.ts +80 -0
- package/src/services/mcp/mcp-commands.ts +114 -0
- package/src/services/mcp/stdio-transport.ts +246 -0
- package/src/services/mcp/types.ts +155 -0
- package/src/skills/SkillsMenu.tsx +370 -0
- package/src/skills/bundled/batch.ts +106 -0
- package/src/skills/bundled/debug.ts +86 -0
- package/src/skills/bundled/loremIpsum.ts +101 -0
- package/src/skills/bundled/remember.ts +97 -0
- package/src/skills/bundled/simplify.ts +100 -0
- package/src/skills/bundled/skillify.ts +123 -0
- package/src/skills/bundled/stuck.ts +101 -0
- package/src/skills/bundled/updateConfig.ts +228 -0
- package/src/skills/bundled.ts +46 -0
- package/src/skills/frontmatter.ts +179 -0
- package/src/skills/index.ts +87 -0
- package/src/skills/invoke.ts +231 -0
- package/src/skills/loader.ts +710 -0
- package/src/skills/substitution.ts +169 -0
- package/src/skills/types.ts +201 -0
- package/src/tools/agent.ts +143 -0
- package/src/tools/ask-user-question.ts +46 -0
- package/src/tools/bash.ts +148 -0
- package/src/tools/edit.ts +164 -0
- package/src/tools/glob.ts +102 -0
- package/src/tools/grep.ts +248 -0
- package/src/tools/index.ts +73 -0
- package/src/tools/list-mcp-resources.ts +74 -0
- package/src/tools/ls.ts +85 -0
- package/src/tools/mcp.ts +76 -0
- package/src/tools/monitor.ts +159 -0
- package/src/tools/plan-mode.ts +134 -0
- package/src/tools/read-mcp-resource.ts +79 -0
- package/src/tools/read.ts +137 -0
- package/src/tools/skill.ts +176 -0
- package/src/tools/task.ts +349 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/webfetch-domains.ts +239 -0
- package/src/tools/webfetch.ts +533 -0
- package/src/tools/write.ts +101 -0
- package/src/tui/app.tsx +1178 -0
- package/src/tui/components/chat-panel.tsx +1071 -0
- package/src/tui/components/command-panel.tsx +261 -0
- package/src/tui/components/confirm-modal.tsx +135 -0
- package/src/tui/components/connect-modal.tsx +435 -0
- package/src/tui/components/connecting-modal.tsx +167 -0
- package/src/tui/components/edit-pending-modal.tsx +103 -0
- package/src/tui/components/exit-modal.tsx +131 -0
- package/src/tui/components/fork-modal.tsx +377 -0
- package/src/tui/components/image-preview-modal.tsx +141 -0
- package/src/tui/components/image-utils.ts +128 -0
- package/src/tui/components/info-bar.tsx +103 -0
- package/src/tui/components/input-box.tsx +352 -0
- package/src/tui/components/mcp/MCPSettings.tsx +386 -0
- package/src/tui/components/mcp/index.ts +7 -0
- package/src/tui/components/model-modal.tsx +310 -0
- package/src/tui/components/pending-area.tsx +88 -0
- package/src/tui/components/rename-modal.tsx +119 -0
- package/src/tui/components/session-modal.tsx +233 -0
- package/src/tui/components/side-bar.tsx +349 -0
- package/src/tui/components/tool-output.ts +6 -0
- package/src/tui/hooks/user-prompt-history.ts +114 -0
- package/src/tui/theme.ts +63 -0
- package/src/types/commands.ts +80 -0
- package/src/types/cross-spawn.d.ts +24 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument Substitution for Skills
|
|
3
|
+
*
|
|
4
|
+
* Handles {{skill.args}} and <arg> placeholders in skill prompts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ToolUseContext } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Substitute arguments in skill content
|
|
11
|
+
* Supports {{skill.args}}, {{skill.arg.name}}, and <name> placeholders
|
|
12
|
+
*/
|
|
13
|
+
export function substituteArguments(
|
|
14
|
+
content: string,
|
|
15
|
+
args: string,
|
|
16
|
+
strict: boolean,
|
|
17
|
+
argNames?: string[]
|
|
18
|
+
): string {
|
|
19
|
+
let result = content;
|
|
20
|
+
|
|
21
|
+
// Replace {{skill.args}} with the full argument string
|
|
22
|
+
result = result.replace(/\{\{skill\.args\}\}/g, args);
|
|
23
|
+
|
|
24
|
+
// If argNames are provided, try to parse named arguments
|
|
25
|
+
if (argNames && argNames.length > 0) {
|
|
26
|
+
const parsedArgs = parseNamedArguments(args, argNames);
|
|
27
|
+
|
|
28
|
+
// Replace {{skill.arg.name}} patterns
|
|
29
|
+
result = result.replace(/\{\{skill\.arg\.(\w+)\}\}/g, (match, name) => {
|
|
30
|
+
const value = parsedArgs[name as keyof typeof parsedArgs];
|
|
31
|
+
return value ?? (strict ? match : "");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Replace <name> patterns with values
|
|
35
|
+
for (const [name, value] of Object.entries(parsedArgs)) {
|
|
36
|
+
result = result.replace(new RegExp(`<${name}>`, "g"), value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Replace remaining <...> placeholders with args (for backward compatibility)
|
|
41
|
+
result = result.replace(/<[^>]+>/g, (match) => {
|
|
42
|
+
if (match === args) return args; // If the whole thing is the args, keep it
|
|
43
|
+
return match; // Leave other placeholders as-is
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse named arguments from a string
|
|
51
|
+
* e.g., "file.ts --flag value" with argNames ["file", "flag"]
|
|
52
|
+
* -> { file: "file.ts", flag: "value" }
|
|
53
|
+
*/
|
|
54
|
+
export function parseNamedArguments(args: string, argNames: string[]): Record<string, string> {
|
|
55
|
+
const result: Record<string, string> = {};
|
|
56
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
57
|
+
|
|
58
|
+
let partIndex = 0;
|
|
59
|
+
for (const name of argNames) {
|
|
60
|
+
if (partIndex >= parts.length) {
|
|
61
|
+
result[name] = "";
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const part = parts[partIndex]!;
|
|
66
|
+
|
|
67
|
+
// Check if this part looks like a named argument (--name or --name=value)
|
|
68
|
+
if (part.startsWith("--")) {
|
|
69
|
+
const eqIdx = part.indexOf("=");
|
|
70
|
+
if (eqIdx !== -1) {
|
|
71
|
+
// --name=value format
|
|
72
|
+
const argName = part.slice(2, eqIdx);
|
|
73
|
+
const argValue = part.slice(eqIdx + 1);
|
|
74
|
+
result[argName] = argValue;
|
|
75
|
+
partIndex++;
|
|
76
|
+
} else {
|
|
77
|
+
// --name value format
|
|
78
|
+
result[name] = parts[partIndex + 1] ?? "";
|
|
79
|
+
partIndex += 2;
|
|
80
|
+
}
|
|
81
|
+
} else if (part.startsWith("-")) {
|
|
82
|
+
// Single dash flag
|
|
83
|
+
result[name] = part;
|
|
84
|
+
partIndex++;
|
|
85
|
+
} else {
|
|
86
|
+
// Positional argument
|
|
87
|
+
result[name] = part;
|
|
88
|
+
partIndex++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse argument names from arguments frontmatter
|
|
97
|
+
* e.g., "<file> <target>" -> ["file", "target"]
|
|
98
|
+
*/
|
|
99
|
+
export function parseArgumentNames(argumentsFrontmatter?: string | string[]): string[] {
|
|
100
|
+
if (!argumentsFrontmatter) return [];
|
|
101
|
+
|
|
102
|
+
const args = Array.isArray(argumentsFrontmatter)
|
|
103
|
+
? argumentsFrontmatter
|
|
104
|
+
: argumentsFrontmatter.split(/\s+/);
|
|
105
|
+
|
|
106
|
+
return args.map(arg => arg.replace(/[<>]/g, "").trim()).filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Execute shell commands in skill prompt
|
|
111
|
+
* Supports !`command` and ```! ... ``` syntax
|
|
112
|
+
*/
|
|
113
|
+
export async function executeShellCommandsInPrompt(
|
|
114
|
+
content: string,
|
|
115
|
+
_ctx: ToolUseContext,
|
|
116
|
+
skillName: string
|
|
117
|
+
): Promise<string> {
|
|
118
|
+
// For now, we'll just strip shell commands as execution requires careful handling
|
|
119
|
+
// This is a simplified version - full implementation would execute and replace
|
|
120
|
+
|
|
121
|
+
let result = content;
|
|
122
|
+
|
|
123
|
+
// Remove inline shell commands: !`command`
|
|
124
|
+
result = result.replace(/!`([^`]+)`/g, (_, cmd) => {
|
|
125
|
+
// In a full implementation, this would execute the command
|
|
126
|
+
// and replace the !`...` with the output
|
|
127
|
+
console.log(`[skill:${skillName}] Would execute: ${cmd}`);
|
|
128
|
+
return `[shell output placeholder: ${cmd}]`;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Remove block shell commands: ```! ... ```
|
|
132
|
+
result = result.replace(/```!([\s\S]*?)```/g, (_, cmd: string) => {
|
|
133
|
+
const trimmedCmd = cmd.trim();
|
|
134
|
+
console.log(`[skill:${skillName}] Would execute block: ${trimmedCmd}`);
|
|
135
|
+
return `[shell output placeholder]`;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Substitute environment variables in content
|
|
143
|
+
*/
|
|
144
|
+
export function substituteEnvVariables(
|
|
145
|
+
content: string,
|
|
146
|
+
env: Record<string, unknown>,
|
|
147
|
+
sessionId?: string,
|
|
148
|
+
skillDir?: string
|
|
149
|
+
): string {
|
|
150
|
+
let result = content;
|
|
151
|
+
|
|
152
|
+
// Replace ${CLAUDE_SKILL_DIR}
|
|
153
|
+
if (skillDir) {
|
|
154
|
+
result = result.replace(/\$\{CLAUDE_SKILL_DIR\}/g, skillDir);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Replace ${CLAUDE_SESSION_ID}
|
|
158
|
+
if (sessionId) {
|
|
159
|
+
result = result.replace(/\$\{CLAUDE_SESSION_ID\}/g, sessionId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Replace ${ENV:VAR_NAME}
|
|
163
|
+
result = result.replace(/\$\{ENV:(\w+)\}/g, (_, varName) => {
|
|
164
|
+
const value = env[varName as keyof typeof env];
|
|
165
|
+
return value !== undefined ? String(value) : "";
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills System Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Defines the core types for Koi's Skills functionality.
|
|
5
|
+
* Skills are reusable prompt templates with metadata that can be invoked
|
|
6
|
+
* via slash commands (e.g., /review, /test).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Content block type for skill prompts.
|
|
11
|
+
* Simplified version of the AI SDK's ContentBlockParam type.
|
|
12
|
+
*/
|
|
13
|
+
export type ContentBlockParam =
|
|
14
|
+
| { type: "text"; text: string }
|
|
15
|
+
| { type: "image"; image: string | Uint8Array }
|
|
16
|
+
| { type: "tool_result"; toolResultId: string; content: string }
|
|
17
|
+
| { type: "tool_use"; tool: string; args: Record<string, unknown> };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hooks configuration for skills
|
|
21
|
+
*/
|
|
22
|
+
export interface HooksSettings {
|
|
23
|
+
"pre-task"?: string[];
|
|
24
|
+
"post-task"?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Frontmatter shell configuration
|
|
29
|
+
*/
|
|
30
|
+
export interface FrontmatterShell {
|
|
31
|
+
name?: string;
|
|
32
|
+
command?: string;
|
|
33
|
+
env?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Context passed to skill prompt generators
|
|
38
|
+
*/
|
|
39
|
+
export interface ToolUseContext {
|
|
40
|
+
tools: Record<string, (...args: unknown[]) => unknown>;
|
|
41
|
+
env: Record<string, unknown>;
|
|
42
|
+
cwd: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Skill loaded from source
|
|
47
|
+
*/
|
|
48
|
+
export type SkillLoadedFrom =
|
|
49
|
+
| "skills" // Loaded from skills directory
|
|
50
|
+
| "commands" // Loaded from commands directory (legacy)
|
|
51
|
+
| "plugin" // From a plugin
|
|
52
|
+
| "bundled" // Built-in bundled skill
|
|
53
|
+
| "mcp"; // From MCP server
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Skill configuration source
|
|
57
|
+
*/
|
|
58
|
+
export type SkillSource =
|
|
59
|
+
| "userSettings" // User's global skills (~/.config/koi/skills)
|
|
60
|
+
| "projectSettings" // Project-level skills (.claude/skills)
|
|
61
|
+
| "policySettings" // Policy-defined skills
|
|
62
|
+
| "plugin" // From a plugin
|
|
63
|
+
| "bundled" // Built-in bundled skill
|
|
64
|
+
| "mcp"; // From MCP server
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parsed frontmatter fields from SKILL.md
|
|
68
|
+
*/
|
|
69
|
+
export interface ParsedFields {
|
|
70
|
+
name?: string;
|
|
71
|
+
description?: string | string[];
|
|
72
|
+
when_to_use?: string;
|
|
73
|
+
argument_hint?: string;
|
|
74
|
+
arguments?: string | string[];
|
|
75
|
+
allowed_tools?: string | string[];
|
|
76
|
+
model?: string;
|
|
77
|
+
disable_model_invocation?: boolean;
|
|
78
|
+
user_invocable?: boolean;
|
|
79
|
+
hooks?: HooksSettings;
|
|
80
|
+
context?: "fork" | "inline";
|
|
81
|
+
agent?: string;
|
|
82
|
+
effort?: string;
|
|
83
|
+
paths?: string | string[];
|
|
84
|
+
shell?: FrontmatterShell;
|
|
85
|
+
version?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A skill command that can be invoked
|
|
90
|
+
*/
|
|
91
|
+
export interface SkillCommand {
|
|
92
|
+
type: "prompt";
|
|
93
|
+
name: string;
|
|
94
|
+
description: string;
|
|
95
|
+
hasUserSpecifiedDescription: boolean;
|
|
96
|
+
allowedTools: string[];
|
|
97
|
+
argumentHint?: string;
|
|
98
|
+
argNames?: string[];
|
|
99
|
+
whenToUse?: string;
|
|
100
|
+
version?: string;
|
|
101
|
+
model?: string;
|
|
102
|
+
disableModelInvocation: boolean;
|
|
103
|
+
userInvocable: boolean;
|
|
104
|
+
context?: "fork" | "inline";
|
|
105
|
+
agent?: string;
|
|
106
|
+
effort?: string;
|
|
107
|
+
paths?: string[];
|
|
108
|
+
contentLength: number;
|
|
109
|
+
isHidden: boolean;
|
|
110
|
+
progressMessage: string;
|
|
111
|
+
source: SkillSource;
|
|
112
|
+
loadedFrom: SkillLoadedFrom;
|
|
113
|
+
hooks?: HooksSettings;
|
|
114
|
+
skillRoot?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Generate the prompt content for this skill with given arguments
|
|
117
|
+
*/
|
|
118
|
+
getPromptForCommand: (args: string, ctx: ToolUseContext) => Promise<ContentBlockParam[]>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A skill with its source file path
|
|
123
|
+
*/
|
|
124
|
+
export interface SkillWithPath {
|
|
125
|
+
skill: SkillCommand;
|
|
126
|
+
filePath: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Bundled skill definition (used for registration)
|
|
131
|
+
*/
|
|
132
|
+
export interface BundledSkillDefinition {
|
|
133
|
+
name: string;
|
|
134
|
+
description: string;
|
|
135
|
+
aliases?: string[];
|
|
136
|
+
whenToUse?: string;
|
|
137
|
+
argumentHint?: string;
|
|
138
|
+
allowedTools?: string[];
|
|
139
|
+
model?: string;
|
|
140
|
+
disableModelInvocation?: boolean;
|
|
141
|
+
userInvocable?: boolean;
|
|
142
|
+
isEnabled?: () => boolean;
|
|
143
|
+
hooks?: HooksSettings;
|
|
144
|
+
context?: "inline" | "fork";
|
|
145
|
+
agent?: string;
|
|
146
|
+
effort?: string;
|
|
147
|
+
template?: string;
|
|
148
|
+
files?: Record<string, string>;
|
|
149
|
+
getPromptForCommand: (args: string, ctx: ToolUseContext) => Promise<ContentBlockParam[]>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Skill invocation result
|
|
154
|
+
*/
|
|
155
|
+
export interface SkillInvocationResult {
|
|
156
|
+
success: boolean;
|
|
157
|
+
content?: ContentBlockParam[];
|
|
158
|
+
error?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Frontmatter parsed from a SKILL.md file
|
|
163
|
+
*/
|
|
164
|
+
export interface SkillFrontmatter {
|
|
165
|
+
name?: string;
|
|
166
|
+
description?: string | string[];
|
|
167
|
+
when_to_use?: string;
|
|
168
|
+
"argument-hint"?: string;
|
|
169
|
+
arguments?: string | string[];
|
|
170
|
+
"allowed-tools"?: string | string[];
|
|
171
|
+
model?: string;
|
|
172
|
+
"disable-model-invocation"?: boolean;
|
|
173
|
+
"user-invocable"?: boolean;
|
|
174
|
+
hooks?: HooksSettings;
|
|
175
|
+
context?: "fork" | "inline";
|
|
176
|
+
agent?: string;
|
|
177
|
+
effort?: string;
|
|
178
|
+
paths?: string | string[];
|
|
179
|
+
shell?: FrontmatterShell;
|
|
180
|
+
version?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Skill file info for discovery
|
|
185
|
+
*/
|
|
186
|
+
export interface SkillFileInfo {
|
|
187
|
+
path: string;
|
|
188
|
+
name: string;
|
|
189
|
+
root: string;
|
|
190
|
+
source: SkillSource;
|
|
191
|
+
loadedFrom: SkillLoadedFrom;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Dynamic skill loading event
|
|
196
|
+
*/
|
|
197
|
+
export interface SkillsLoadedEvent {
|
|
198
|
+
skills: SkillCommand[];
|
|
199
|
+
source: SkillSource;
|
|
200
|
+
timestamp: number;
|
|
201
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Tool — Spawn a subagent to perform a focused task.
|
|
3
|
+
*
|
|
4
|
+
* Implements Claude Code's AgentTool semantics on top of Pi's agent framework:
|
|
5
|
+
* • Built-in types: "explore" (read-only), "plan" (read-only + tasks)
|
|
6
|
+
* • Synchronous mode: wait for completion, return result text
|
|
7
|
+
* • Asynchronous mode: fire-and-forget, return agentId immediately
|
|
8
|
+
*
|
|
9
|
+
* Subagents are intentionally isolated:
|
|
10
|
+
* • Fresh message history (only the prompt)
|
|
11
|
+
* • Filtered tool set (no nested agents, no user questions, no plan-mode exit)
|
|
12
|
+
* • Max turn limit to prevent runaway token consumption
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
17
|
+
import type { ToolResultWithError } from "./types.js";
|
|
18
|
+
import { activeSessionRef } from "../agent/hooks.js";
|
|
19
|
+
import { runSubagent, type SubagentConfig } from "../agent/subagent.js";
|
|
20
|
+
import { subagentRegistry } from "../agent/subagent-registry.js";
|
|
21
|
+
|
|
22
|
+
// ─── Schema ──────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export const agentSchema = Type.Object({
|
|
25
|
+
description: Type.String({
|
|
26
|
+
description: "A short (3-5 word) description of the task",
|
|
27
|
+
maxLength: 60,
|
|
28
|
+
}),
|
|
29
|
+
prompt: Type.String({
|
|
30
|
+
description: "The complete task description for the subagent",
|
|
31
|
+
}),
|
|
32
|
+
subagent_type: Type.Optional(
|
|
33
|
+
Type.Union(
|
|
34
|
+
[Type.Literal("explore"), Type.Literal("plan")],
|
|
35
|
+
{ description: "Built-in agent type: 'explore' = read-only, 'plan' = planning mode. Defaults to 'explore' if omitted." }
|
|
36
|
+
)
|
|
37
|
+
),
|
|
38
|
+
model: Type.Optional(
|
|
39
|
+
Type.String({
|
|
40
|
+
description: "Model override (optional, not yet implemented)",
|
|
41
|
+
})
|
|
42
|
+
),
|
|
43
|
+
run_in_background: Type.Optional(
|
|
44
|
+
Type.Boolean({
|
|
45
|
+
description: "Run asynchronously in the background. A <task-notification> will be injected into your context when it completes. The user does not see this notification.",
|
|
46
|
+
})
|
|
47
|
+
),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export type AgentToolInput = {
|
|
51
|
+
description: string;
|
|
52
|
+
prompt: string;
|
|
53
|
+
subagent_type?: "explore" | "plan";
|
|
54
|
+
model?: string;
|
|
55
|
+
run_in_background?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ─── Execute ─────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export async function executeAgent(
|
|
61
|
+
_toolCallId: string,
|
|
62
|
+
params: AgentToolInput
|
|
63
|
+
): Promise<ToolResultWithError<{ result?: string; agentId?: string; status: string }>> {
|
|
64
|
+
if (!activeSessionRef.current) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: "Error: No active session available to spawn subagent." }],
|
|
67
|
+
details: { status: "error" },
|
|
68
|
+
isError: true,
|
|
69
|
+
} as ToolResultWithError<{ result?: string; agentId?: string; status: string }>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config: SubagentConfig = {
|
|
73
|
+
description: params.description,
|
|
74
|
+
prompt: params.prompt,
|
|
75
|
+
subagentType: params.subagent_type ?? "explore",
|
|
76
|
+
runInBackground: params.run_in_background,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (params.run_in_background) {
|
|
80
|
+
const agentId = await subagentRegistry.launch(config);
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: `Launched background agent ${agentId}: ${params.description}\n\nThe agent is running asynchronously. A <task-notification> will be injected into your context when it completes. The user does not see this notification.`,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
details: { status: "async_launched", agentId },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const result = await runSubagent(config);
|
|
94
|
+
return {
|
|
95
|
+
content: [
|
|
96
|
+
{ type: "text", text: result || "[Agent completed with empty output]" },
|
|
97
|
+
],
|
|
98
|
+
details: { status: "completed", result },
|
|
99
|
+
};
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: `Subagent failed: ${message}` }],
|
|
104
|
+
details: { status: "failed" },
|
|
105
|
+
isError: true,
|
|
106
|
+
} as ToolResultWithError<{ result?: string; agentId?: string; status: string }>;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Tool Definition ─────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export function createAgentToolDefinition(): ToolDefinition<
|
|
113
|
+
typeof agentSchema,
|
|
114
|
+
{ result?: string; agentId?: string; status: string }
|
|
115
|
+
> {
|
|
116
|
+
return {
|
|
117
|
+
name: "agent",
|
|
118
|
+
label: "Agent",
|
|
119
|
+
description:
|
|
120
|
+
"Spawn a focused subagent to perform a specific task in isolation.\n\n" +
|
|
121
|
+
"Use this tool to delegate parallelizable work (e.g., exploring a directory, " +
|
|
122
|
+
"researching a file, drafting a plan) while you continue with other tasks.\n\n" +
|
|
123
|
+
"Built-in types:\n" +
|
|
124
|
+
" • explore — read-only tools only; safe for research and discovery (default)\n" +
|
|
125
|
+
" • plan — read-only + task tools; for formulating implementation plans\n\n" +
|
|
126
|
+
"If subagent_type is omitted, it defaults to 'explore' for safety.\n" +
|
|
127
|
+
"Set run_in_background to true for fire-and-forget execution. " +
|
|
128
|
+
"You will receive a <task-notification> when the background agent completes.",
|
|
129
|
+
promptSnippet: "Agent: spawn a focused subagent to perform a task",
|
|
130
|
+
promptGuidelines: [
|
|
131
|
+
"Use Agent to delegate independent, parallelizable tasks.",
|
|
132
|
+
"Provide a concise description (3-5 words) and a detailed prompt.",
|
|
133
|
+
"Choose 'explore' for read-only research, 'plan' for drafting plans.",
|
|
134
|
+
"Set run_in_background when you don't need the result immediately.",
|
|
135
|
+
"Subagents cannot spawn other subagents or ask the user questions.",
|
|
136
|
+
],
|
|
137
|
+
parameters: agentSchema,
|
|
138
|
+
executionMode: "parallel",
|
|
139
|
+
async execute(toolCallId, params, _signal, _onUpdate) {
|
|
140
|
+
return executeAgent(toolCallId, params);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AskUserQuestion Tool
|
|
3
|
+
*
|
|
4
|
+
* Allows the agent to ask the user a multiple-choice question.
|
|
5
|
+
* The question is shown in a modal dialog; the user's answer is returned.
|
|
6
|
+
* An "Other (custom)" option is always appended automatically.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Type } from "typebox";
|
|
10
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { askUserQuestion } from "../agent/question-ui.js";
|
|
12
|
+
|
|
13
|
+
export const askUserQuestionSchema = Type.Object({
|
|
14
|
+
question: Type.String({ description: "The question to ask the user" }),
|
|
15
|
+
options: Type.Array(Type.String(), {
|
|
16
|
+
description: "List of answer options for the user to choose from",
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type AskUserQuestionToolInput = {
|
|
21
|
+
question: string;
|
|
22
|
+
options: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createAskUserQuestionToolDefinition(): ToolDefinition {
|
|
26
|
+
return {
|
|
27
|
+
name: "askUserQuestion",
|
|
28
|
+
label: "Ask User Question",
|
|
29
|
+
description:
|
|
30
|
+
"Ask the user a multiple-choice question to clarify requirements or collect preferences. " +
|
|
31
|
+
"An additional 'Other (custom)' option is always provided automatically.",
|
|
32
|
+
parameters: askUserQuestionSchema,
|
|
33
|
+
executionMode: "parallel",
|
|
34
|
+
async execute(_toolCallId, params, _signal, _onUpdate) {
|
|
35
|
+
const input = params as AskUserQuestionToolInput;
|
|
36
|
+
const answer = await askUserQuestion({
|
|
37
|
+
question: input.question,
|
|
38
|
+
options: input.options,
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: answer }],
|
|
42
|
+
details: { answer },
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
} as ToolDefinition;
|
|
46
|
+
}
|