@clanker-code/pi-subagents 0.10.5
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/.plans/PLAN-next-changes.md +183 -0
- package/.plans/README.md +14 -0
- package/AGENTS.md +31 -0
- package/CHANGELOG.md +583 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +630 -0
- package/RELEASE.md +39 -0
- package/dist/abort-resend.d.ts +35 -0
- package/dist/abort-resend.js +71 -0
- package/dist/agent-details.d.ts +17 -0
- package/dist/agent-details.js +22 -0
- package/dist/agent-manager.d.ts +132 -0
- package/dist/agent-manager.js +493 -0
- package/dist/agent-runner.d.ts +165 -0
- package/dist/agent-runner.js +732 -0
- package/dist/agent-tool-description.d.ts +9 -0
- package/dist/agent-tool-description.js +147 -0
- package/dist/agent-types.d.ts +60 -0
- package/dist/agent-types.js +157 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +56 -0
- package/dist/cross-extension-rpc.d.ts +46 -0
- package/dist/cross-extension-rpc.js +76 -0
- package/dist/custom-agents.d.ts +14 -0
- package/dist/custom-agents.js +149 -0
- package/dist/default-agents.d.ts +7 -0
- package/dist/default-agents.js +119 -0
- package/dist/enabled-models.d.ts +49 -0
- package/dist/enabled-models.js +145 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +28 -0
- package/dist/group-join.d.ts +32 -0
- package/dist/group-join.js +116 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +1918 -0
- package/dist/invocation-config.d.ts +25 -0
- package/dist/invocation-config.js +19 -0
- package/dist/memory.d.ts +49 -0
- package/dist/memory.js +151 -0
- package/dist/model-resolver.d.ts +19 -0
- package/dist/model-resolver.js +62 -0
- package/dist/notifications.d.ts +6 -0
- package/dist/notifications.js +107 -0
- package/dist/output-file.d.ts +24 -0
- package/dist/output-file.js +86 -0
- package/dist/peek.d.ts +37 -0
- package/dist/peek.js +121 -0
- package/dist/prompts.d.ts +40 -0
- package/dist/prompts.js +95 -0
- package/dist/schedule-store.d.ts +38 -0
- package/dist/schedule-store.js +155 -0
- package/dist/schedule.d.ts +109 -0
- package/dist/schedule.js +338 -0
- package/dist/settings.d.ts +135 -0
- package/dist/settings.js +168 -0
- package/dist/skill-loader.d.ts +24 -0
- package/dist/skill-loader.js +93 -0
- package/dist/status-note.d.ts +13 -0
- package/dist/status-note.js +24 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.js +7 -0
- package/dist/ui/agent-tool-rendering.d.ts +34 -0
- package/dist/ui/agent-tool-rendering.js +154 -0
- package/dist/ui/agent-widget-tree.d.ts +33 -0
- package/dist/ui/agent-widget-tree.js +130 -0
- package/dist/ui/agent-widget.d.ts +156 -0
- package/dist/ui/agent-widget.js +408 -0
- package/dist/ui/conversation-viewer.d.ts +47 -0
- package/dist/ui/conversation-viewer.js +290 -0
- package/dist/ui/menu-select.d.ts +20 -0
- package/dist/ui/menu-select.js +46 -0
- package/dist/ui/schedule-menu.d.ts +16 -0
- package/dist/ui/schedule-menu.js +99 -0
- package/dist/ui/viewer-keys.d.ts +20 -0
- package/dist/ui/viewer-keys.js +17 -0
- package/dist/usage.d.ts +50 -0
- package/dist/usage.js +49 -0
- package/dist/wait.d.ts +10 -0
- package/dist/wait.js +37 -0
- package/dist/worktree.d.ts +45 -0
- package/dist/worktree.js +160 -0
- package/docs/design/default-extension-tool-exposure.md +56 -0
- package/docs/superpowers/plans/2026-06-19-recursive-subagent-widget.md +600 -0
- package/docs/superpowers/specs/2026-06-19-recursive-subagent-widget-design.md +189 -0
- package/examples/agent-tool-description.md +45 -0
- package/package.json +56 -0
- package/reviews/proposal-structured-output-schema.md +135 -0
- package/reviews/recursive-subagent-widget-preview-rev2.png +0 -0
- package/reviews/recursive-subagent-widget-preview.html +137 -0
- package/reviews/recursive-subagent-widget-preview.png +0 -0
- package/reviews/subagent-features-comparison.md +350 -0
- package/src/abort-resend.ts +75 -0
- package/src/agent-details.ts +31 -0
- package/src/agent-manager.ts +596 -0
- package/src/agent-runner.ts +872 -0
- package/src/agent-tool-description.ts +163 -0
- package/src/agent-types.ts +189 -0
- package/src/context.ts +58 -0
- package/src/cross-extension-rpc.ts +122 -0
- package/src/custom-agents.ts +160 -0
- package/src/default-agents.ts +123 -0
- package/src/enabled-models.ts +180 -0
- package/src/env.ts +33 -0
- package/src/group-join.ts +141 -0
- package/src/index.ts +2115 -0
- package/src/invocation-config.ts +42 -0
- package/src/memory.ts +165 -0
- package/src/model-resolver.ts +81 -0
- package/src/notifications.ts +120 -0
- package/src/output-file.ts +96 -0
- package/src/peek.ts +155 -0
- package/src/prompts.ts +129 -0
- package/src/schedule-store.ts +153 -0
- package/src/schedule.ts +365 -0
- package/src/settings.ts +289 -0
- package/src/skill-loader.ts +102 -0
- package/src/status-note.ts +25 -0
- package/src/types.ts +195 -0
- package/src/ui/agent-tool-rendering.ts +175 -0
- package/src/ui/agent-widget-tree.ts +169 -0
- package/src/ui/agent-widget.ts +497 -0
- package/src/ui/conversation-viewer.ts +297 -0
- package/src/ui/menu-select.ts +68 -0
- package/src/ui/schedule-menu.ts +105 -0
- package/src/ui/viewer-keys.ts +39 -0
- package/src/usage.ts +60 -0
- package/src/wait.ts +44 -0
- package/src/worktree.ts +191 -0
- package/vitest.config.ts +25 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-loader.ts — Preload named skills.
|
|
3
|
+
*
|
|
4
|
+
* Roots, in precedence order:
|
|
5
|
+
* - <cwd>/.pi/skills (project, Pi's standard)
|
|
6
|
+
* - <cwd>/.agents/skills (project, cross-tool Agent Skills spec — https://agentskills.io)
|
|
7
|
+
* - getAgentDir()/skills (user, default ~/.pi/agent/skills — Pi's standard)
|
|
8
|
+
* - ~/.agents/skills (user, cross-tool Agent Skills spec)
|
|
9
|
+
* - ~/.pi/skills (legacy global, pre-Pi)
|
|
10
|
+
*
|
|
11
|
+
* Layout per root:
|
|
12
|
+
* - <root>/<name>.md (flat file at the top level)
|
|
13
|
+
* - <root>/.../<name>/SKILL.md (directory skill, may be nested — Pi's standard)
|
|
14
|
+
*
|
|
15
|
+
* Recursion skips dotfile entries and node_modules. A directory that itself contains
|
|
16
|
+
* SKILL.md is a skill — we don't descend into it (Pi: skills don't nest).
|
|
17
|
+
*
|
|
18
|
+
* Symlinks are rejected for security (deviation from Pi, which follows them).
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { isSymlink, isUnsafeName, safeReadFile } from "./memory.js";
|
|
25
|
+
export function preloadSkills(skillNames, cwd) {
|
|
26
|
+
return skillNames.map((name) => ({ name, content: loadSkillContent(name, cwd) }));
|
|
27
|
+
}
|
|
28
|
+
function loadSkillContent(name, cwd) {
|
|
29
|
+
if (isUnsafeName(name)) {
|
|
30
|
+
return `(Skill "${name}" skipped: name contains path traversal characters)`;
|
|
31
|
+
}
|
|
32
|
+
const roots = [
|
|
33
|
+
join(cwd, ".pi", "skills"), // project — Pi standard
|
|
34
|
+
join(cwd, ".agents", "skills"), // project — Agent Skills spec
|
|
35
|
+
join(getAgentDir(), "skills"), // user — Pi standard
|
|
36
|
+
join(homedir(), ".agents", "skills"), // user — Agent Skills spec
|
|
37
|
+
join(homedir(), ".pi", "skills"), // legacy global, pre-Pi
|
|
38
|
+
];
|
|
39
|
+
for (const root of roots) {
|
|
40
|
+
const content = findInRoot(root, name);
|
|
41
|
+
if (content !== undefined)
|
|
42
|
+
return content;
|
|
43
|
+
}
|
|
44
|
+
return `(Skill "${name}" not found in .pi/skills/, .agents/skills/, or global skill locations)`;
|
|
45
|
+
}
|
|
46
|
+
function findInRoot(root, name) {
|
|
47
|
+
if (isSymlink(root))
|
|
48
|
+
return undefined; // reject symlinked roots entirely
|
|
49
|
+
const flat = safeReadFile(join(root, `${name}.md`))?.trim();
|
|
50
|
+
if (flat !== undefined)
|
|
51
|
+
return flat;
|
|
52
|
+
return findSkillDirectory(root, name);
|
|
53
|
+
}
|
|
54
|
+
/** BFS under `root` for a directory named `name` containing `SKILL.md`. Pi-conforming filters. */
|
|
55
|
+
function findSkillDirectory(root, name) {
|
|
56
|
+
if (!existsSync(root))
|
|
57
|
+
return undefined;
|
|
58
|
+
const queue = [root];
|
|
59
|
+
while (queue.length > 0) {
|
|
60
|
+
const current = queue.shift();
|
|
61
|
+
if (current === undefined)
|
|
62
|
+
continue;
|
|
63
|
+
let entries;
|
|
64
|
+
try {
|
|
65
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Deterministic byte-order traversal — locale-independent.
|
|
71
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!entry.isDirectory())
|
|
74
|
+
continue;
|
|
75
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
76
|
+
continue;
|
|
77
|
+
// Symlinked dirs already filtered by entry.isDirectory() — Dirent uses lstat semantics.
|
|
78
|
+
const path = join(current, entry.name);
|
|
79
|
+
const skillMd = join(path, "SKILL.md");
|
|
80
|
+
const isSkillDir = existsSync(skillMd);
|
|
81
|
+
if (isSkillDir) {
|
|
82
|
+
if (entry.name === name) {
|
|
83
|
+
const content = safeReadFile(skillMd)?.trim();
|
|
84
|
+
if (content !== undefined)
|
|
85
|
+
return content;
|
|
86
|
+
}
|
|
87
|
+
continue; // Pi rule: skills don't nest — don't descend into a skill dir
|
|
88
|
+
}
|
|
89
|
+
queue.push(path);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status-note.ts — Parenthetical status note appended to agent result text.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Explicit parenthetical note for a non-normal terminal outcome, so the parent
|
|
6
|
+
* agent can't mistake partial output for a completed result. Empty string for a
|
|
7
|
+
* clean completion (and any unknown/non-terminal status).
|
|
8
|
+
*
|
|
9
|
+
* `stopped` (a human aborted it) is deliberately distinct from `aborted` (the
|
|
10
|
+
* turn limit was hit) — the parent should treat human intervention differently
|
|
11
|
+
* from a budget cutoff.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getStatusNote(status: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status-note.ts — Parenthetical status note appended to agent result text.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Explicit parenthetical note for a non-normal terminal outcome, so the parent
|
|
6
|
+
* agent can't mistake partial output for a completed result. Empty string for a
|
|
7
|
+
* clean completion (and any unknown/non-terminal status).
|
|
8
|
+
*
|
|
9
|
+
* `stopped` (a human aborted it) is deliberately distinct from `aborted` (the
|
|
10
|
+
* turn limit was hit) — the parent should treat human intervention differently
|
|
11
|
+
* from a budget cutoff.
|
|
12
|
+
*/
|
|
13
|
+
export function getStatusNote(status) {
|
|
14
|
+
switch (status) {
|
|
15
|
+
case "stopped":
|
|
16
|
+
return " (STOPPED BY THE USER before completion — output is partial; the task was NOT finished)";
|
|
17
|
+
case "aborted":
|
|
18
|
+
return " (aborted — hit the turn limit before completion; output may be incomplete)";
|
|
19
|
+
case "steered":
|
|
20
|
+
return " (wrapped up at the turn limit — output may be partial)";
|
|
21
|
+
default:
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — Type definitions for the subagent system.
|
|
3
|
+
*/
|
|
4
|
+
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
5
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { LifetimeUsage } from "./usage.js";
|
|
7
|
+
export type { ThinkingLevel };
|
|
8
|
+
/** Agent type: any string name (built-in defaults or user-defined). */
|
|
9
|
+
export type SubagentType = string;
|
|
10
|
+
/** Names of the three embedded default agents. */
|
|
11
|
+
export declare const DEFAULT_AGENT_NAMES: readonly ["general-purpose", "Explore", "Plan"];
|
|
12
|
+
/** Maximum recursive subagent depth. Parent/orchestrator is depth 0; agents may run at depths 1..4. */
|
|
13
|
+
export declare const MAX_RECURSIVE_DEPTH = 4;
|
|
14
|
+
/** Memory scope for persistent agent memory. */
|
|
15
|
+
export type MemoryScope = "user" | "project" | "local";
|
|
16
|
+
/** Isolation mode for agent execution. */
|
|
17
|
+
export type IsolationMode = "worktree";
|
|
18
|
+
/** Unified agent configuration — used for both default and user-defined agents. */
|
|
19
|
+
export interface AgentConfig {
|
|
20
|
+
name: string;
|
|
21
|
+
displayName?: string;
|
|
22
|
+
description: string;
|
|
23
|
+
builtinToolNames?: string[];
|
|
24
|
+
/** Raw `ext:` selector entries from the `tools:` CSV, e.g. ["ext:foo", "ext:bar/x"].
|
|
25
|
+
* Presence of any entry flips extension tools to an explicit allowlist. */
|
|
26
|
+
extSelectors?: string[];
|
|
27
|
+
/** Tool denylist — these tools are removed even if `builtinToolNames` or extensions include them. */
|
|
28
|
+
disallowedTools?: string[];
|
|
29
|
+
/** true = inherit all, string[] = only listed, false = none */
|
|
30
|
+
extensions: true | string[] | false;
|
|
31
|
+
/** Extension-name denylist applied after the `extensions:` include set. Exclude wins.
|
|
32
|
+
* Plain canonical names only (case-insensitive); no paths, no wildcard. */
|
|
33
|
+
excludeExtensions?: string[];
|
|
34
|
+
/** true = inherit all, string[] = only listed, false = none */
|
|
35
|
+
skills: true | string[] | false;
|
|
36
|
+
model?: string;
|
|
37
|
+
thinking?: ThinkingLevel;
|
|
38
|
+
maxTurns?: number;
|
|
39
|
+
systemPrompt: string;
|
|
40
|
+
promptMode: "replace" | "append";
|
|
41
|
+
/** Default for spawn: fork parent conversation. undefined = caller decides. */
|
|
42
|
+
inheritContext?: boolean;
|
|
43
|
+
/** Default for spawn: run in background. undefined = caller decides.
|
|
44
|
+
* NOTE: this fork always runs agents in the background, so a configured value
|
|
45
|
+
* here is accepted (frontmatter/RPC) but ignored at spawn time. Kept on the
|
|
46
|
+
* type for upstream/fixture compatibility. */
|
|
47
|
+
runInBackground?: boolean;
|
|
48
|
+
/** Default for spawn: no extension tools. undefined = caller decides. */
|
|
49
|
+
isolated?: boolean;
|
|
50
|
+
/** Persistent memory scope — agents with memory get a persistent directory and MEMORY.md */
|
|
51
|
+
memory?: MemoryScope;
|
|
52
|
+
/** Isolation mode — "worktree" runs the agent in a temporary git worktree */
|
|
53
|
+
isolation?: IsolationMode;
|
|
54
|
+
/** true = this is an embedded default agent (informational) */
|
|
55
|
+
isDefault?: boolean;
|
|
56
|
+
/** false = agent is hidden from the registry */
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
/** Where this agent was loaded from */
|
|
59
|
+
source?: "default" | "project" | "global";
|
|
60
|
+
}
|
|
61
|
+
export type JoinMode = 'async' | 'group' | 'smart';
|
|
62
|
+
export interface AgentRecord {
|
|
63
|
+
id: string;
|
|
64
|
+
type: SubagentType;
|
|
65
|
+
description: string;
|
|
66
|
+
status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error";
|
|
67
|
+
result?: string;
|
|
68
|
+
error?: string;
|
|
69
|
+
toolUses: number;
|
|
70
|
+
startedAt: number;
|
|
71
|
+
completedAt?: number;
|
|
72
|
+
session?: AgentSession;
|
|
73
|
+
abortController?: AbortController;
|
|
74
|
+
promise?: Promise<string>;
|
|
75
|
+
groupId?: string;
|
|
76
|
+
joinMode?: JoinMode;
|
|
77
|
+
/** Set when result was already consumed via get_subagent_result — suppresses completion notification. */
|
|
78
|
+
resultConsumed?: boolean;
|
|
79
|
+
/** Steering messages queued before the session was ready. */
|
|
80
|
+
pendingSteers?: string[];
|
|
81
|
+
/** Worktree info if the agent is running in an isolated worktree. */
|
|
82
|
+
worktree?: {
|
|
83
|
+
path: string;
|
|
84
|
+
branch: string;
|
|
85
|
+
baseSha: string;
|
|
86
|
+
workPath: string;
|
|
87
|
+
};
|
|
88
|
+
/** Worktree cleanup result after agent completion. */
|
|
89
|
+
worktreeResult?: {
|
|
90
|
+
hasChanges: boolean;
|
|
91
|
+
branch?: string;
|
|
92
|
+
};
|
|
93
|
+
/** The tool_use_id from the original Agent tool call. */
|
|
94
|
+
toolCallId?: string;
|
|
95
|
+
/** Recursive subagent depth. Parent/orchestrator is 0; spawned agents are 1..4. */
|
|
96
|
+
depth: number;
|
|
97
|
+
/** Parent subagent id when spawned recursively from another subagent. */
|
|
98
|
+
parentAgentId?: string;
|
|
99
|
+
/** Path to the streaming output transcript file. */
|
|
100
|
+
outputFile?: string;
|
|
101
|
+
/** Cleanup function for the output file stream subscription. */
|
|
102
|
+
outputCleanup?: () => void;
|
|
103
|
+
/**
|
|
104
|
+
* Lifetime usage breakdown, accumulated via `message_end` events. Survives
|
|
105
|
+
* compaction. Total = input + output + cacheWrite (cacheRead deliberately
|
|
106
|
+
* excluded — see issue #38). Initialized to zeros at spawn.
|
|
107
|
+
*/
|
|
108
|
+
lifetimeUsage: LifetimeUsage;
|
|
109
|
+
/** Number of times this agent's session has compacted. Initialized to 0 at spawn. */
|
|
110
|
+
compactionCount: number;
|
|
111
|
+
/** Resolved spawn params, captured for UI display. Fixed at spawn time. */
|
|
112
|
+
invocation?: AgentInvocation;
|
|
113
|
+
}
|
|
114
|
+
export interface AgentInvocation {
|
|
115
|
+
/** Short display name, e.g. "haiku" — only set when different from parent. */
|
|
116
|
+
modelName?: string;
|
|
117
|
+
thinking?: ThinkingLevel;
|
|
118
|
+
maxTurns?: number;
|
|
119
|
+
isolated?: boolean;
|
|
120
|
+
inheritContext?: boolean;
|
|
121
|
+
runInBackground?: boolean;
|
|
122
|
+
isolation?: IsolationMode;
|
|
123
|
+
depth?: number;
|
|
124
|
+
parentAgentId?: string;
|
|
125
|
+
maxDepth?: number;
|
|
126
|
+
}
|
|
127
|
+
/** Details attached to custom notification messages for visual rendering. */
|
|
128
|
+
export interface NotificationDetails {
|
|
129
|
+
id: string;
|
|
130
|
+
description: string;
|
|
131
|
+
status: string;
|
|
132
|
+
toolUses: number;
|
|
133
|
+
turnCount: number;
|
|
134
|
+
maxTurns?: number;
|
|
135
|
+
totalTokens: number;
|
|
136
|
+
durationMs: number;
|
|
137
|
+
outputFile?: string;
|
|
138
|
+
error?: string;
|
|
139
|
+
resultPreview: string;
|
|
140
|
+
/** Additional agents in a group notification. */
|
|
141
|
+
others?: NotificationDetails[];
|
|
142
|
+
}
|
|
143
|
+
export interface EnvInfo {
|
|
144
|
+
isGitRepo: boolean;
|
|
145
|
+
branch: string;
|
|
146
|
+
platform: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A subagent spawn registered to fire on a schedule.
|
|
150
|
+
*
|
|
151
|
+
* Stored at `<cwd>/.pi/subagent-schedules/<sessionId>.json`. Session-scoped:
|
|
152
|
+
* survives `/resume` but resets on `/new`, mirroring pi-chonky-tasks.
|
|
153
|
+
*/
|
|
154
|
+
export interface ScheduledSubagent {
|
|
155
|
+
id: string;
|
|
156
|
+
/** Unique within store. Defaults to `description`. */
|
|
157
|
+
name: string;
|
|
158
|
+
description: string;
|
|
159
|
+
/** Raw user input — cron expr | "+10m" | ISO | "5m". */
|
|
160
|
+
schedule: string;
|
|
161
|
+
scheduleType: "cron" | "once" | "interval";
|
|
162
|
+
/** Computed at create time for interval/once. */
|
|
163
|
+
intervalMs?: number;
|
|
164
|
+
subagent_type: SubagentType;
|
|
165
|
+
prompt: string;
|
|
166
|
+
model?: string;
|
|
167
|
+
thinking?: ThinkingLevel;
|
|
168
|
+
max_turns?: number;
|
|
169
|
+
isolated?: boolean;
|
|
170
|
+
isolation?: IsolationMode;
|
|
171
|
+
enabled: boolean;
|
|
172
|
+
/** ISO timestamp. */
|
|
173
|
+
createdAt: string;
|
|
174
|
+
lastRun?: string;
|
|
175
|
+
lastStatus?: "success" | "error" | "running";
|
|
176
|
+
/** Refreshed on every fire and on store load. */
|
|
177
|
+
nextRun?: string;
|
|
178
|
+
runCount: number;
|
|
179
|
+
}
|
|
180
|
+
export interface ScheduleStoreData {
|
|
181
|
+
/** For future migrations. */
|
|
182
|
+
version: 1;
|
|
183
|
+
jobs: ScheduledSubagent[];
|
|
184
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — Type definitions for the subagent system.
|
|
3
|
+
*/
|
|
4
|
+
/** Names of the three embedded default agents. */
|
|
5
|
+
export const DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"];
|
|
6
|
+
/** Maximum recursive subagent depth. Parent/orchestrator is depth 0; agents may run at depths 1..4. */
|
|
7
|
+
export const MAX_RECURSIVE_DEPTH = 4;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
export declare function compactPreview(text: string, maxLen?: number): string;
|
|
3
|
+
export declare function tailPreview(text: string, maxLen?: number): string;
|
|
4
|
+
export declare function snipMiddleLines(text: string, edgeLines?: number): string[];
|
|
5
|
+
/**
|
|
6
|
+
* Shape of the args passed to the Agent tool's `renderCall`. Mirrors the tool
|
|
7
|
+
* parameter schema — only the fields whose presence changes the call-time
|
|
8
|
+
* header are listed. Anything we can't decide at call time (e.g. an agent that
|
|
9
|
+
* inherits its model from the parent) is intentionally omitted here and is
|
|
10
|
+
* surfaced later by `renderResult`.
|
|
11
|
+
*/
|
|
12
|
+
export interface AgentCallArgs {
|
|
13
|
+
subagent_type?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Explicit per-call model override (`provider/modelId` or fuzzy name). */
|
|
16
|
+
model?: string;
|
|
17
|
+
/** Agent ID being resumed; only set on resume calls. */
|
|
18
|
+
resume?: string;
|
|
19
|
+
/** Schedule expression; only set on schedule calls. */
|
|
20
|
+
schedule?: string;
|
|
21
|
+
/** Drop parent extension tools for this run. */
|
|
22
|
+
isolated?: boolean;
|
|
23
|
+
/** Isolation mode — currently only "worktree". */
|
|
24
|
+
isolation?: "worktree";
|
|
25
|
+
}
|
|
26
|
+
export declare function renderAgentCall(args: any, theme: any): Text;
|
|
27
|
+
export declare function renderAgentResult(result: any, { expanded, isPartial }: {
|
|
28
|
+
expanded: boolean;
|
|
29
|
+
isPartial: boolean;
|
|
30
|
+
}, theme: any): Text;
|
|
31
|
+
export declare function renderSteerCall(args: {
|
|
32
|
+
agent_id?: string;
|
|
33
|
+
message?: string;
|
|
34
|
+
}, theme: any): Text;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { getModelLabelFromConfig } from "../agent-tool-description.js";
|
|
3
|
+
import { getAgentConfig } from "../agent-types.js";
|
|
4
|
+
import { extractText } from "../context.js";
|
|
5
|
+
import { formatMs, formatTurns, getDisplayName, SPINNER } from "./agent-widget.js";
|
|
6
|
+
export function compactPreview(text, maxLen = 80) {
|
|
7
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
8
|
+
if (oneLine.length <= maxLen)
|
|
9
|
+
return oneLine;
|
|
10
|
+
return oneLine.slice(0, maxLen - 1).trimEnd() + "…";
|
|
11
|
+
}
|
|
12
|
+
export function tailPreview(text, maxLen = 100) {
|
|
13
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
14
|
+
if (oneLine.length <= maxLen)
|
|
15
|
+
return oneLine;
|
|
16
|
+
return "…" + oneLine.slice(oneLine.length - (maxLen - 1));
|
|
17
|
+
}
|
|
18
|
+
export function snipMiddleLines(text, edgeLines = 20) {
|
|
19
|
+
const lines = text.split("\n");
|
|
20
|
+
const maxLines = edgeLines * 2;
|
|
21
|
+
if (lines.length <= maxLines)
|
|
22
|
+
return lines;
|
|
23
|
+
const omitted = lines.length - maxLines;
|
|
24
|
+
return [
|
|
25
|
+
...lines.slice(0, edgeLines),
|
|
26
|
+
`... ${omitted} lines omitted; expand for full output ...`,
|
|
27
|
+
...lines.slice(-edgeLines),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the model that this call will use, if it's knowable before execute().
|
|
32
|
+
* Resolution order: explicit `args.model` > agent config frontmatter > none.
|
|
33
|
+
* Inheritance from the parent session is decided inside execute() and surfaced
|
|
34
|
+
* by `renderResult` once the resolved model is available.
|
|
35
|
+
*/
|
|
36
|
+
function resolveCallModel(args) {
|
|
37
|
+
if (typeof args.model === "string" && args.model)
|
|
38
|
+
return args.model;
|
|
39
|
+
if (typeof args.subagent_type === "string")
|
|
40
|
+
return getAgentConfig(args.subagent_type)?.model;
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Build the dimmed badge list shown between the agent name and the description. */
|
|
44
|
+
function buildCallBadges(args, theme) {
|
|
45
|
+
const badges = [];
|
|
46
|
+
const callModel = resolveCallModel(args);
|
|
47
|
+
if (callModel)
|
|
48
|
+
badges.push(getModelLabelFromConfig(callModel));
|
|
49
|
+
if (typeof args.resume === "string" && args.resume) {
|
|
50
|
+
badges.push(`resume: ${compactPreview(args.resume, 12)}`);
|
|
51
|
+
}
|
|
52
|
+
if (typeof args.schedule === "string" && args.schedule) {
|
|
53
|
+
badges.push(`schedule: ${compactPreview(args.schedule, 20)}`);
|
|
54
|
+
}
|
|
55
|
+
if (args.isolation === "worktree")
|
|
56
|
+
badges.push("worktree");
|
|
57
|
+
if (args.isolated)
|
|
58
|
+
badges.push("isolated");
|
|
59
|
+
if (badges.length === 0)
|
|
60
|
+
return "";
|
|
61
|
+
const sep = " " + theme.fg("dim", "·") + " ";
|
|
62
|
+
return " " + badges.map((b) => theme.fg("dim", b)).join(sep);
|
|
63
|
+
}
|
|
64
|
+
export function renderAgentCall(args, theme) {
|
|
65
|
+
const displayName = args.subagent_type ? getDisplayName(args.subagent_type) : "Agent";
|
|
66
|
+
const desc = args.description ?? "";
|
|
67
|
+
const badges = buildCallBadges(args, theme);
|
|
68
|
+
const descPart = desc ? " " + theme.fg("muted", desc) : "";
|
|
69
|
+
return new Text("▸ " + theme.fg("toolTitle", theme.bold(displayName)) + badges + descPart, 0, 0);
|
|
70
|
+
}
|
|
71
|
+
function getResultText(content) {
|
|
72
|
+
if (!Array.isArray(content))
|
|
73
|
+
return typeof content === "string" ? content : "";
|
|
74
|
+
return extractText(content);
|
|
75
|
+
}
|
|
76
|
+
export function renderAgentResult(result, { expanded, isPartial }, theme) {
|
|
77
|
+
const details = result.details;
|
|
78
|
+
if (!details) {
|
|
79
|
+
return new Text(getResultText(result.content), 0, 0);
|
|
80
|
+
}
|
|
81
|
+
const stats = (d) => {
|
|
82
|
+
const parts = [];
|
|
83
|
+
if (d.modelName)
|
|
84
|
+
parts.push(d.modelName);
|
|
85
|
+
if (d.tags)
|
|
86
|
+
parts.push(...d.tags);
|
|
87
|
+
if (d.turnCount != null && d.turnCount > 0) {
|
|
88
|
+
parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
89
|
+
}
|
|
90
|
+
if (d.toolUses > 0)
|
|
91
|
+
parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
92
|
+
if (d.tokens)
|
|
93
|
+
parts.push(d.tokens);
|
|
94
|
+
return parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
95
|
+
};
|
|
96
|
+
if (isPartial || details.status === "running") {
|
|
97
|
+
const frame = SPINNER[details.spinnerFrame ?? 0];
|
|
98
|
+
const s = stats(details);
|
|
99
|
+
let line = theme.fg("accent", frame) + (s ? " " + s : "");
|
|
100
|
+
line += "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking…"}`);
|
|
101
|
+
return new Text(line, 0, 0);
|
|
102
|
+
}
|
|
103
|
+
if (details.status === "background") {
|
|
104
|
+
return new Text(theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`), 0, 0);
|
|
105
|
+
}
|
|
106
|
+
if (details.status === "completed" || details.status === "steered") {
|
|
107
|
+
const duration = formatMs(details.durationMs);
|
|
108
|
+
const isSteered = details.status === "steered";
|
|
109
|
+
const icon = isSteered ? theme.fg("warning", "✓") : theme.fg("success", "✓");
|
|
110
|
+
const s = stats(details);
|
|
111
|
+
let line = icon + (s ? " " + s : "");
|
|
112
|
+
line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration);
|
|
113
|
+
const resultText = getResultText(result.content).trim();
|
|
114
|
+
if (expanded) {
|
|
115
|
+
if (resultText) {
|
|
116
|
+
for (const lineText of resultText.split("\n")) {
|
|
117
|
+
line += "\n" + theme.fg("dim", ` ${lineText}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const doneText = isSteered ? "Wrapped up (turn limit)" : "Done";
|
|
123
|
+
if (resultText) {
|
|
124
|
+
for (const lineText of snipMiddleLines(resultText)) {
|
|
125
|
+
line += "\n" + theme.fg("dim", ` ${lineText}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
line += "\n" + theme.fg("dim", ` ⎿ ${doneText}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return new Text(line, 0, 0);
|
|
133
|
+
}
|
|
134
|
+
if (details.status === "stopped") {
|
|
135
|
+
const s = stats(details);
|
|
136
|
+
let line = theme.fg("dim", "■") + (s ? " " + s : "");
|
|
137
|
+
line += "\n" + theme.fg("dim", " ⎿ Stopped");
|
|
138
|
+
return new Text(line, 0, 0);
|
|
139
|
+
}
|
|
140
|
+
const s = stats(details);
|
|
141
|
+
let line = theme.fg("error", "✗") + (s ? " " + s : "");
|
|
142
|
+
if (details.status === "error") {
|
|
143
|
+
line += "\n" + theme.fg("error", ` ⎿ Error: ${details.error ?? "unknown"}`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
line += "\n" + theme.fg("warning", " ⎿ Aborted (max turns exceeded)");
|
|
147
|
+
}
|
|
148
|
+
return new Text(line, 0, 0);
|
|
149
|
+
}
|
|
150
|
+
export function renderSteerCall(args, theme) {
|
|
151
|
+
const agentId = args.agent_id ? ` ${theme.fg("muted", args.agent_id)}` : "";
|
|
152
|
+
const preview = args.message ? ` ${theme.fg("dim", `“${compactPreview(args.message)}”`)}` : "";
|
|
153
|
+
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("Steer Agent"))}${agentId}${preview}`, 0, 0);
|
|
154
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentInvocation, SubagentType } from "../types.js";
|
|
2
|
+
import type { AgentActivity, Theme } from "./agent-widget.js";
|
|
3
|
+
export type WidgetDisplayMode = "auto" | "rich" | "compact";
|
|
4
|
+
export interface WidgetAgentSnapshot {
|
|
5
|
+
id: string;
|
|
6
|
+
parentAgentId?: string;
|
|
7
|
+
depth?: number;
|
|
8
|
+
type: SubagentType;
|
|
9
|
+
description: string;
|
|
10
|
+
status: string;
|
|
11
|
+
startedAt: number;
|
|
12
|
+
completedAt?: number;
|
|
13
|
+
error?: string;
|
|
14
|
+
toolUses: number;
|
|
15
|
+
invocation?: AgentInvocation;
|
|
16
|
+
activity?: AgentActivity;
|
|
17
|
+
}
|
|
18
|
+
export interface WidgetTreeNode {
|
|
19
|
+
snapshot: WidgetAgentSnapshot;
|
|
20
|
+
children: WidgetTreeNode[];
|
|
21
|
+
orphaned?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface RenderTreeOptions {
|
|
24
|
+
mode: WidgetDisplayMode;
|
|
25
|
+
width: number;
|
|
26
|
+
maxLines: number;
|
|
27
|
+
theme: Theme;
|
|
28
|
+
frame: string;
|
|
29
|
+
now?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function buildAgentTree(records: WidgetAgentSnapshot[]): WidgetTreeNode[];
|
|
32
|
+
export declare function chooseEffectiveMode(mode: WidgetDisplayMode, width: number, richLineCount: number, maxLines: number): "rich" | "compact";
|
|
33
|
+
export declare function renderAgentTree(records: WidgetAgentSnapshot[], options: RenderTreeOptions): string[];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { getConfig } from "../agent-types.js";
|
|
3
|
+
function statusRank(status) {
|
|
4
|
+
if (status === "running")
|
|
5
|
+
return 0;
|
|
6
|
+
if (status === "queued")
|
|
7
|
+
return 1;
|
|
8
|
+
return 2;
|
|
9
|
+
}
|
|
10
|
+
function sortNodes(a, b) {
|
|
11
|
+
const status = statusRank(a.snapshot.status) - statusRank(b.snapshot.status);
|
|
12
|
+
if (status !== 0)
|
|
13
|
+
return status;
|
|
14
|
+
return a.snapshot.startedAt - b.snapshot.startedAt;
|
|
15
|
+
}
|
|
16
|
+
export function buildAgentTree(records) {
|
|
17
|
+
const nodes = new Map();
|
|
18
|
+
for (const record of records)
|
|
19
|
+
nodes.set(record.id, { snapshot: record, children: [] });
|
|
20
|
+
const roots = [];
|
|
21
|
+
for (const node of nodes.values()) {
|
|
22
|
+
const parentId = node.snapshot.parentAgentId;
|
|
23
|
+
const parent = parentId ? nodes.get(parentId) : undefined;
|
|
24
|
+
if (parent) {
|
|
25
|
+
parent.children.push(node);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
if (parentId)
|
|
29
|
+
node.orphaned = true;
|
|
30
|
+
roots.push(node);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function sortDeep(items) {
|
|
34
|
+
items.sort(sortNodes);
|
|
35
|
+
for (const item of items)
|
|
36
|
+
sortDeep(item.children);
|
|
37
|
+
}
|
|
38
|
+
sortDeep(roots);
|
|
39
|
+
return roots;
|
|
40
|
+
}
|
|
41
|
+
export function chooseEffectiveMode(mode, width, richLineCount, maxLines) {
|
|
42
|
+
if (mode === "rich" || mode === "compact")
|
|
43
|
+
return mode;
|
|
44
|
+
if (width < 88)
|
|
45
|
+
return "compact";
|
|
46
|
+
if (richLineCount > maxLines)
|
|
47
|
+
return "compact";
|
|
48
|
+
return "rich";
|
|
49
|
+
}
|
|
50
|
+
function formatElapsed(startedAt, now) {
|
|
51
|
+
const seconds = Math.max(0, Math.floor((now - startedAt) / 1000));
|
|
52
|
+
if (seconds < 60)
|
|
53
|
+
return `${seconds}s`;
|
|
54
|
+
const minutes = Math.floor(seconds / 60);
|
|
55
|
+
const rest = seconds % 60;
|
|
56
|
+
return rest > 0 ? `${minutes}m ${rest}s` : `${minutes}m`;
|
|
57
|
+
}
|
|
58
|
+
function statusIcon(snapshot, frame, theme) {
|
|
59
|
+
if (snapshot.status === "running")
|
|
60
|
+
return theme.fg("accent", frame);
|
|
61
|
+
if (snapshot.status === "queued")
|
|
62
|
+
return theme.fg("muted", "◦");
|
|
63
|
+
if (snapshot.status === "completed")
|
|
64
|
+
return theme.fg("success", "✓");
|
|
65
|
+
if (snapshot.status === "stopped")
|
|
66
|
+
return theme.fg("dim", "■");
|
|
67
|
+
return theme.fg("error", "✗");
|
|
68
|
+
}
|
|
69
|
+
function displayName(type) {
|
|
70
|
+
return getConfig(type).displayName;
|
|
71
|
+
}
|
|
72
|
+
function collectRows(nodes, options, mode, prefix = "") {
|
|
73
|
+
const rows = [];
|
|
74
|
+
nodes.forEach((node, index) => {
|
|
75
|
+
const isLast = index === nodes.length - 1;
|
|
76
|
+
const connector = isLast ? "└─" : "├─";
|
|
77
|
+
const childPrefix = prefix + (isLast ? " " : "│ ");
|
|
78
|
+
const s = node.snapshot;
|
|
79
|
+
const name = displayName(s.type);
|
|
80
|
+
const elapsed = formatElapsed(s.startedAt, options.now ?? Date.now());
|
|
81
|
+
const stats = [];
|
|
82
|
+
if (s.activity?.turnCount)
|
|
83
|
+
stats.push(`↻${s.activity.turnCount}`);
|
|
84
|
+
if (s.toolUses > 0)
|
|
85
|
+
stats.push(`${s.toolUses} tool${s.toolUses === 1 ? "" : "s"}`);
|
|
86
|
+
stats.push(elapsed);
|
|
87
|
+
const orphan = node.orphaned ? " ⚠ orphan" : "";
|
|
88
|
+
const error = s.error ? ` error: ${s.error}` : "";
|
|
89
|
+
rows.push(`${prefix}${connector} ${statusIcon(s, options.frame, options.theme)} ${options.theme.bold(name)} ${options.theme.fg("muted", s.description)} ${options.theme.fg("dim", `· ${stats.join(" · ")}${orphan}${error}`)}`);
|
|
90
|
+
if (mode === "rich" && s.status === "running") {
|
|
91
|
+
const activity = s.activity?.activityDescription ?? "thinking…";
|
|
92
|
+
rows.push(`${childPrefix}${options.theme.fg("dim", `⎿ ${activity}`)}`);
|
|
93
|
+
}
|
|
94
|
+
rows.push(...collectRows(node.children, options, mode, childPrefix));
|
|
95
|
+
});
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
function applyOverflow(lines, maxLines, width, hiddenLabel = "agents") {
|
|
99
|
+
if (lines.length <= maxLines)
|
|
100
|
+
return lines.map(line => truncateToWidth(line, width));
|
|
101
|
+
if (maxLines <= 1)
|
|
102
|
+
return [truncateToWidth(`+${lines.length} more ${hiddenLabel} hidden`, width)];
|
|
103
|
+
const visible = lines.slice(0, maxLines - 1);
|
|
104
|
+
const hidden = lines.length - visible.length;
|
|
105
|
+
visible.push(`└─ +${hidden} more ${hiddenLabel} hidden`);
|
|
106
|
+
return visible.map(line => truncateToWidth(line, width));
|
|
107
|
+
}
|
|
108
|
+
export function renderAgentTree(records, options) {
|
|
109
|
+
const tree = buildAgentTree(records);
|
|
110
|
+
const now = options.now ?? Date.now();
|
|
111
|
+
const active = records.filter(r => r.status === "running").length;
|
|
112
|
+
const queued = records.filter(r => r.status === "queued").length;
|
|
113
|
+
const maxDepth = records.reduce((max, r) => Math.max(max, r.depth ?? 0), 0);
|
|
114
|
+
let mode;
|
|
115
|
+
let rows;
|
|
116
|
+
if (options.mode === "rich" || options.mode === "compact") {
|
|
117
|
+
mode = options.mode;
|
|
118
|
+
rows = collectRows(tree, { ...options, now }, mode);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const richRows = collectRows(tree, { ...options, now }, "rich");
|
|
122
|
+
mode = chooseEffectiveMode(options.mode, options.width, richRows.length + 1, options.maxLines);
|
|
123
|
+
rows = mode === "rich" ? richRows : collectRows(tree, { ...options, now }, "compact");
|
|
124
|
+
}
|
|
125
|
+
const suffix = mode === "rich" && records.length > 0
|
|
126
|
+
? options.theme.fg("dim", ` ${active} running · ${queued} queued · depth ${maxDepth}/4`)
|
|
127
|
+
: "";
|
|
128
|
+
const heading = `${active > 0 ? options.theme.fg("accent", "●") : options.theme.fg("dim", "○")} ${options.theme.fg(active > 0 ? "accent" : "dim", "Agents")}${suffix}`;
|
|
129
|
+
return applyOverflow([heading, ...rows], options.maxLines, options.width);
|
|
130
|
+
}
|