@ymy88/pi-subagents 0.0.0-stage → 0.24.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/CHANGELOG.md +904 -0
- package/CONTRIBUTING.md +72 -0
- package/LICENSE +21 -0
- package/README.md +1170 -2
- package/SECURITY.md +95 -0
- package/dist/abortable.d.ts +12 -0
- package/dist/abortable.js +42 -0
- package/dist/agent-color.d.ts +35 -0
- package/dist/agent-color.js +123 -0
- package/dist/agent-file-toggle.d.ts +133 -0
- package/dist/agent-file-toggle.js +287 -0
- package/dist/agent-manager.d.ts +552 -0
- package/dist/agent-manager.js +1466 -0
- package/dist/agent-record-entry.d.ts +96 -0
- package/dist/agent-record-entry.js +121 -0
- package/dist/agent-runner.d.ts +310 -0
- package/dist/agent-runner.js +1195 -0
- package/dist/agent-source-badge.d.ts +50 -0
- package/dist/agent-source-badge.js +74 -0
- package/dist/agent-types.d.ts +119 -0
- package/dist/agent-types.js +286 -0
- package/dist/child-context.d.ts +2 -0
- package/dist/child-context.js +12 -0
- package/dist/completion-outbox.d.ts +63 -0
- package/dist/completion-outbox.js +104 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +56 -0
- package/dist/cross-extension-rpc.d.ts +72 -0
- package/dist/cross-extension-rpc.js +153 -0
- package/dist/custom-agents.d.ts +77 -0
- package/dist/custom-agents.js +396 -0
- package/dist/default-agents.d.ts +7 -0
- package/dist/default-agents.js +121 -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 +50 -0
- package/dist/index.js +4111 -0
- package/dist/invocation-config.d.ts +108 -0
- package/dist/invocation-config.js +83 -0
- package/dist/memory.d.ts +53 -0
- package/dist/memory.js +165 -0
- package/dist/mention-clone.d.ts +79 -0
- package/dist/mention-clone.js +155 -0
- package/dist/mention.d.ts +81 -0
- package/dist/mention.js +131 -0
- package/dist/model-resolver.d.ts +36 -0
- package/dist/model-resolver.js +95 -0
- package/dist/model-scope.d.ts +49 -0
- package/dist/model-scope.js +48 -0
- package/dist/nested-tools.d.ts +57 -0
- package/dist/nested-tools.js +303 -0
- package/dist/output-file.d.ts +43 -0
- package/dist/output-file.js +153 -0
- package/dist/package-resources.d.ts +240 -0
- package/dist/package-resources.js +519 -0
- package/dist/prompts.d.ts +76 -0
- package/dist/prompts.js +121 -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 +359 -0
- package/dist/settings.d.ts +398 -0
- package/dist/settings.js +282 -0
- package/dist/skill-loader.d.ts +24 -0
- package/dist/skill-loader.js +93 -0
- package/dist/status-note.d.ts +61 -0
- package/dist/status-note.js +85 -0
- package/dist/structured-output.d.ts +61 -0
- package/dist/structured-output.js +112 -0
- package/dist/types.d.ts +391 -0
- package/dist/types.js +5 -0
- package/dist/ui/agent-mention.d.ts +82 -0
- package/dist/ui/agent-mention.js +187 -0
- package/dist/ui/agent-widget.d.ts +219 -0
- package/dist/ui/agent-widget.js +585 -0
- package/dist/ui/conversation-viewer.d.ts +177 -0
- package/dist/ui/conversation-viewer.js +905 -0
- package/dist/ui/fleet-list.d.ts +211 -0
- package/dist/ui/fleet-list.js +489 -0
- package/dist/ui/schedule-menu.d.ts +16 -0
- package/dist/ui/schedule-menu.js +94 -0
- package/dist/ui/select-item.d.ts +27 -0
- package/dist/ui/select-item.js +34 -0
- package/dist/ui/viewer-keys.d.ts +22 -0
- package/dist/ui/viewer-keys.js +18 -0
- package/dist/ui/workflow-card.d.ts +175 -0
- package/dist/ui/workflow-card.js +332 -0
- package/dist/ui/workflow-dialog.d.ts +305 -0
- package/dist/ui/workflow-dialog.js +843 -0
- package/dist/ui/workflow-menu.d.ts +60 -0
- package/dist/ui/workflow-menu.js +147 -0
- package/dist/usage.d.ts +135 -0
- package/dist/usage.js +120 -0
- package/dist/workflow/collisions.d.ts +95 -0
- package/dist/workflow/collisions.js +88 -0
- package/dist/workflow/entry.d.ts +32 -0
- package/dist/workflow/entry.js +29 -0
- package/dist/workflow/host.d.ts +62 -0
- package/dist/workflow/host.js +419 -0
- package/dist/workflow/journal.d.ts +97 -0
- package/dist/workflow/journal.js +120 -0
- package/dist/workflow/json-schema.d.ts +52 -0
- package/dist/workflow/json-schema.js +112 -0
- package/dist/workflow/meta.d.ts +67 -0
- package/dist/workflow/meta.js +317 -0
- package/dist/workflow/progress.d.ts +224 -0
- package/dist/workflow/progress.js +361 -0
- package/dist/workflow/runtime.d.ts +334 -0
- package/dist/workflow/runtime.js +830 -0
- package/dist/workflow/saved.d.ts +102 -0
- package/dist/workflow/saved.js +247 -0
- package/dist/workflow/task.d.ts +136 -0
- package/dist/workflow/task.js +207 -0
- package/dist/workflow/tool-description.d.ts +38 -0
- package/dist/workflow/tool-description.js +199 -0
- package/dist/workflow/worker-source.d.ts +47 -0
- package/dist/workflow/worker-source.js +778 -0
- package/dist/worktree.d.ts +52 -0
- package/dist/worktree.js +164 -0
- package/dist/xml.d.ts +10 -0
- package/dist/xml.js +12 -0
- package/docs/packages.md +211 -0
- package/docs/rpc.md +192 -0
- package/docs/workflows.md +447 -0
- package/examples/agent-tool-description.md +42 -0
- package/examples/workflows/compose.js +51 -0
- package/examples/workflows/fan-out-audit.js +47 -0
- package/examples/workflows/gated-fix.js +60 -0
- package/examples/workflows/lib/count-child.js +27 -0
- package/examples/workflows/review-panel.js +63 -0
- package/examples/workflows/structured-findings.js +78 -0
- package/package.json +74 -4
- package/src/abortable.ts +43 -0
- package/src/agent-color.ts +161 -0
- package/src/agent-file-toggle.ts +297 -0
- package/src/agent-manager.ts +1735 -0
- package/src/agent-record-entry.ts +153 -0
- package/src/agent-runner.ts +1468 -0
- package/src/agent-source-badge.ts +75 -0
- package/src/agent-types.ts +346 -0
- package/src/child-context.ts +15 -0
- package/src/completion-outbox.ts +116 -0
- package/src/context.ts +58 -0
- package/src/cross-extension-rpc.ts +221 -0
- package/src/custom-agents.ts +452 -0
- package/src/default-agents.ts +125 -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 +4433 -0
- package/src/invocation-config.ts +155 -0
- package/src/memory.ts +179 -0
- package/src/mention-clone.ts +195 -0
- package/src/mention.ts +141 -0
- package/src/model-resolver.ts +118 -0
- package/src/model-scope.ts +70 -0
- package/src/nested-tools.ts +426 -0
- package/src/output-file.ts +164 -0
- package/src/package-resources.ts +652 -0
- package/src/prompts.ts +171 -0
- package/src/schedule-store.ts +153 -0
- package/src/schedule.ts +386 -0
- package/src/settings.ts +650 -0
- package/src/skill-loader.ts +102 -0
- package/src/status-note.ts +90 -0
- package/src/structured-output.ts +130 -0
- package/src/types.ts +404 -0
- package/src/ui/agent-mention.ts +216 -0
- package/src/ui/agent-widget.ts +661 -0
- package/src/ui/conversation-viewer.ts +928 -0
- package/src/ui/fleet-list.ts +559 -0
- package/src/ui/schedule-menu.ts +105 -0
- package/src/ui/select-item.ts +45 -0
- package/src/ui/viewer-keys.ts +43 -0
- package/src/ui/workflow-card.ts +470 -0
- package/src/ui/workflow-dialog.ts +1115 -0
- package/src/ui/workflow-menu.ts +193 -0
- package/src/usage.ts +167 -0
- package/src/workflow/collisions.ts +123 -0
- package/src/workflow/entry.ts +47 -0
- package/src/workflow/host.ts +467 -0
- package/src/workflow/journal.ts +164 -0
- package/src/workflow/json-schema.ts +129 -0
- package/src/workflow/meta.ts +325 -0
- package/src/workflow/progress.ts +551 -0
- package/src/workflow/runtime.ts +1219 -0
- package/src/workflow/saved.ts +259 -0
- package/src/workflow/task.ts +302 -0
- package/src/workflow/tool-description.ts +200 -0
- package/src/workflow/worker-source.ts +781 -0
- package/src/worktree.ts +205 -0
- package/src/xml.ts +13 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-record-entry.ts — what a finished top-level agent leaves behind in the session.
|
|
3
|
+
*
|
|
4
|
+
* The live record is evicted about ten minutes after the agent finishes, read or
|
|
5
|
+
* not, and is gone after a reload or a restart. Its completion notification
|
|
6
|
+
* carries only a preview of the result and points at `get_subagent_result` for
|
|
7
|
+
* the rest, so that pointer has to outlive the record: the final record is
|
|
8
|
+
* appended to the session as this entry, and `get_subagent_result` falls back
|
|
9
|
+
* to it.
|
|
10
|
+
*
|
|
11
|
+
* It is also what lets `Agent({ resume })` reopen an evicted agent: the entry
|
|
12
|
+
* names the session file the conversation lives in, so the id the model holds
|
|
13
|
+
* still leads somewhere after eviction, a reload or a restart.
|
|
14
|
+
*/
|
|
15
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { AgentRecord, SubagentType } from "./types.js";
|
|
17
|
+
/** `customType` of the session entry a finished top-level agent appends. */
|
|
18
|
+
export declare const AGENT_RECORD_ENTRY_TYPE = "subagents:record";
|
|
19
|
+
/** The persisted snapshot of a finished agent. */
|
|
20
|
+
export interface AgentRecordEntryData {
|
|
21
|
+
id: string;
|
|
22
|
+
type: AgentRecord["type"];
|
|
23
|
+
description: string;
|
|
24
|
+
status: AgentRecord["status"];
|
|
25
|
+
result?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
startedAt: number;
|
|
28
|
+
completedAt?: number;
|
|
29
|
+
/**
|
|
30
|
+
* The agent's persisted pi session, when it has one — what a resume reopens.
|
|
31
|
+
* Absent for an in-memory session (`rememberAgents` off, or
|
|
32
|
+
* `persist_session: false`), which leaves nothing to come back to.
|
|
33
|
+
*/
|
|
34
|
+
sessionFile?: string;
|
|
35
|
+
/** Names to give back to the reopened agent, as the `@handle` path does. */
|
|
36
|
+
handle?: string;
|
|
37
|
+
alias?: string;
|
|
38
|
+
/** Set when the agent's work lived in a worktree that is now gone — see {@link worktreeLost}. */
|
|
39
|
+
worktreeLost?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether the agent ran in a worktree that held its work and has since been
|
|
43
|
+
* removed. Settling always removes the worktree, committing any changes to a
|
|
44
|
+
* branch first, so only a clean one (`hasChanges: false`) leaves nothing
|
|
45
|
+
* behind; a run that failed before cleanup reported nothing and counts as lost.
|
|
46
|
+
* Claude Code draws the same line: it resumes an isolated agent outside its
|
|
47
|
+
* worktree only when the worktree was removed clean, and never builds a fresh
|
|
48
|
+
* one on resume.
|
|
49
|
+
*/
|
|
50
|
+
export declare function worktreeLost(record: Pick<AgentRecord, "worktree" | "worktreeResult">): boolean;
|
|
51
|
+
/** Snapshot a finished record for {@link AGENT_RECORD_ENTRY_TYPE}. */
|
|
52
|
+
export declare function agentRecordEntryData(record: AgentRecord): AgentRecordEntryData;
|
|
53
|
+
/**
|
|
54
|
+
* The latest persisted record of `id` on the current branch. A resumed agent
|
|
55
|
+
* appends a new entry on each finish, so the last one wins; an entry on an
|
|
56
|
+
* abandoned branch is not this conversation's.
|
|
57
|
+
*
|
|
58
|
+
* When `matchHandle` is set, the handle and alias match too, case-insensitively
|
|
59
|
+
* like `@handle`, and only if no entry's id is `id`. Resume leaves it off: it
|
|
60
|
+
* reopens whatever this returns, and a handle would reopen a saved session
|
|
61
|
+
* while that agent's live record still exists.
|
|
62
|
+
*/
|
|
63
|
+
export declare function findAgentRecordEntry(ctx: ExtensionContext, id: string, matchHandle?: boolean): AgentRecordEntryData | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Whether an evicted agent's saved conversation can be reopened, and the
|
|
66
|
+
* agent type to reopen it under. Shared by `@handle`, `Agent({ resume })` and
|
|
67
|
+
* a workflow's `resume`, so all three refuse the same cases in the same words;
|
|
68
|
+
* `reason` completes "Could not resume X —" and "cannot be resumed —".
|
|
69
|
+
*
|
|
70
|
+
* The file is checked here rather than left to SessionManager.open: that runs
|
|
71
|
+
* inside runAgent, whose rejection lands on the record as an agent error, not
|
|
72
|
+
* as a refusal. A `/new` in another pi window or a manual delete makes the
|
|
73
|
+
* conversation unrecoverable (Claude Code's `not_reachable`); `gone` marks
|
|
74
|
+
* that, and the no-file case, as refusals that can never clear.
|
|
75
|
+
*
|
|
76
|
+
* The Agent tool deliberately falls back to general-purpose for a type it
|
|
77
|
+
* cannot resolve (#183), which covers a deleted file AND a merely disabled
|
|
78
|
+
* one. A resume must not inherit that: reopening this conversation under a
|
|
79
|
+
* different agent's prompt and tools is not continuing it, and the new record
|
|
80
|
+
* would re-tombstone under the substitute, so the handle would never find its
|
|
81
|
+
* way back. `@handle` and the Agent tool reload the agent files first.
|
|
82
|
+
*
|
|
83
|
+
* `worktreeLost` refuses for good, as Claude Code does ("its worktree no
|
|
84
|
+
* longer exists"): the conversation's edits were made in a directory that is
|
|
85
|
+
* gone, and reopening it in the parent's checkout or a fresh worktree would
|
|
86
|
+
* continue that work somewhere it never happened.
|
|
87
|
+
*/
|
|
88
|
+
export declare function reopenTarget(type: string, sessionFile: string | undefined, worktreeLost?: boolean): {
|
|
89
|
+
ok: true;
|
|
90
|
+
type: SubagentType;
|
|
91
|
+
sessionFile: string;
|
|
92
|
+
} | {
|
|
93
|
+
ok: false;
|
|
94
|
+
reason: string;
|
|
95
|
+
gone: boolean;
|
|
96
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-record-entry.ts — what a finished top-level agent leaves behind in the session.
|
|
3
|
+
*
|
|
4
|
+
* The live record is evicted about ten minutes after the agent finishes, read or
|
|
5
|
+
* not, and is gone after a reload or a restart. Its completion notification
|
|
6
|
+
* carries only a preview of the result and points at `get_subagent_result` for
|
|
7
|
+
* the rest, so that pointer has to outlive the record: the final record is
|
|
8
|
+
* appended to the session as this entry, and `get_subagent_result` falls back
|
|
9
|
+
* to it.
|
|
10
|
+
*
|
|
11
|
+
* It is also what lets `Agent({ resume })` reopen an evicted agent: the entry
|
|
12
|
+
* names the session file the conversation lives in, so the id the model holds
|
|
13
|
+
* still leads somewhere after eviction, a reload or a restart.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { resolveSpawnType } from "./agent-types.js";
|
|
17
|
+
/** `customType` of the session entry a finished top-level agent appends. */
|
|
18
|
+
export const AGENT_RECORD_ENTRY_TYPE = "subagents:record";
|
|
19
|
+
/**
|
|
20
|
+
* Whether the agent ran in a worktree that held its work and has since been
|
|
21
|
+
* removed. Settling always removes the worktree, committing any changes to a
|
|
22
|
+
* branch first, so only a clean one (`hasChanges: false`) leaves nothing
|
|
23
|
+
* behind; a run that failed before cleanup reported nothing and counts as lost.
|
|
24
|
+
* Claude Code draws the same line: it resumes an isolated agent outside its
|
|
25
|
+
* worktree only when the worktree was removed clean, and never builds a fresh
|
|
26
|
+
* one on resume.
|
|
27
|
+
*/
|
|
28
|
+
export function worktreeLost(record) {
|
|
29
|
+
return record.worktree !== undefined && record.worktreeResult?.hasChanges !== false;
|
|
30
|
+
}
|
|
31
|
+
/** Snapshot a finished record for {@link AGENT_RECORD_ENTRY_TYPE}. */
|
|
32
|
+
export function agentRecordEntryData(record) {
|
|
33
|
+
return {
|
|
34
|
+
id: record.id,
|
|
35
|
+
type: record.type,
|
|
36
|
+
description: record.description,
|
|
37
|
+
status: record.status,
|
|
38
|
+
result: record.result,
|
|
39
|
+
error: record.error,
|
|
40
|
+
startedAt: record.startedAt,
|
|
41
|
+
completedAt: record.completedAt,
|
|
42
|
+
sessionFile: record.sessionFile,
|
|
43
|
+
handle: record.handle,
|
|
44
|
+
alias: record.alias,
|
|
45
|
+
worktreeLost: worktreeLost(record) || undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function isAgentRecordEntryData(data) {
|
|
49
|
+
if (typeof data !== "object" || data === null)
|
|
50
|
+
return false;
|
|
51
|
+
const { id, type, status, startedAt } = data;
|
|
52
|
+
return typeof id === "string" && typeof type === "string" && typeof status === "string" && typeof startedAt === "number";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The latest persisted record of `id` on the current branch. A resumed agent
|
|
56
|
+
* appends a new entry on each finish, so the last one wins; an entry on an
|
|
57
|
+
* abandoned branch is not this conversation's.
|
|
58
|
+
*
|
|
59
|
+
* When `matchHandle` is set, the handle and alias match too, case-insensitively
|
|
60
|
+
* like `@handle`, and only if no entry's id is `id`. Resume leaves it off: it
|
|
61
|
+
* reopens whatever this returns, and a handle would reopen a saved session
|
|
62
|
+
* while that agent's live record still exists.
|
|
63
|
+
*/
|
|
64
|
+
export function findAgentRecordEntry(ctx, id, matchHandle = false) {
|
|
65
|
+
const entries = ctx.sessionManager.getBranch();
|
|
66
|
+
const wanted = id.toLowerCase();
|
|
67
|
+
let named;
|
|
68
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
69
|
+
const entry = entries[i];
|
|
70
|
+
if (entry.type !== "custom" || entry.customType !== AGENT_RECORD_ENTRY_TYPE)
|
|
71
|
+
continue;
|
|
72
|
+
if (!isAgentRecordEntryData(entry.data))
|
|
73
|
+
continue;
|
|
74
|
+
if (entry.data.id === id)
|
|
75
|
+
return entry.data;
|
|
76
|
+
if (matchHandle &&
|
|
77
|
+
named === undefined &&
|
|
78
|
+
(entry.data.handle?.toLowerCase() === wanted || entry.data.alias?.toLowerCase() === wanted)) {
|
|
79
|
+
named = entry.data;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return named;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether an evicted agent's saved conversation can be reopened, and the
|
|
86
|
+
* agent type to reopen it under. Shared by `@handle`, `Agent({ resume })` and
|
|
87
|
+
* a workflow's `resume`, so all three refuse the same cases in the same words;
|
|
88
|
+
* `reason` completes "Could not resume X —" and "cannot be resumed —".
|
|
89
|
+
*
|
|
90
|
+
* The file is checked here rather than left to SessionManager.open: that runs
|
|
91
|
+
* inside runAgent, whose rejection lands on the record as an agent error, not
|
|
92
|
+
* as a refusal. A `/new` in another pi window or a manual delete makes the
|
|
93
|
+
* conversation unrecoverable (Claude Code's `not_reachable`); `gone` marks
|
|
94
|
+
* that, and the no-file case, as refusals that can never clear.
|
|
95
|
+
*
|
|
96
|
+
* The Agent tool deliberately falls back to general-purpose for a type it
|
|
97
|
+
* cannot resolve (#183), which covers a deleted file AND a merely disabled
|
|
98
|
+
* one. A resume must not inherit that: reopening this conversation under a
|
|
99
|
+
* different agent's prompt and tools is not continuing it, and the new record
|
|
100
|
+
* would re-tombstone under the substitute, so the handle would never find its
|
|
101
|
+
* way back. `@handle` and the Agent tool reload the agent files first.
|
|
102
|
+
*
|
|
103
|
+
* `worktreeLost` refuses for good, as Claude Code does ("its worktree no
|
|
104
|
+
* longer exists"): the conversation's edits were made in a directory that is
|
|
105
|
+
* gone, and reopening it in the parent's checkout or a fresh worktree would
|
|
106
|
+
* continue that work somewhere it never happened.
|
|
107
|
+
*/
|
|
108
|
+
export function reopenTarget(type, sessionFile, worktreeLost) {
|
|
109
|
+
if (sessionFile === undefined) {
|
|
110
|
+
return { ok: false, reason: "its conversation was not saved (`rememberAgents` or `persist_session` was off)", gone: true };
|
|
111
|
+
}
|
|
112
|
+
if (!existsSync(sessionFile))
|
|
113
|
+
return { ok: false, reason: "its session is gone", gone: true };
|
|
114
|
+
if (worktreeLost)
|
|
115
|
+
return { ok: false, reason: "its worktree no longer exists", gone: true };
|
|
116
|
+
const dispatch = resolveSpawnType(type);
|
|
117
|
+
if (!dispatch.ok || dispatch.fellBackFrom !== undefined) {
|
|
118
|
+
return { ok: false, reason: `the ${type} agent is no longer available`, gone: false };
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, type: dispatch.type, sessionFile };
|
|
121
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
3
|
+
*/
|
|
4
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
5
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { type AgentSession, DefaultResourceLoader, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { type NestedAgentManager } from "./nested-tools.js";
|
|
8
|
+
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
9
|
+
import type { LifetimeUsage } from "./usage.js";
|
|
10
|
+
import type { CompiledSchema } from "./workflow/json-schema.js";
|
|
11
|
+
/**
|
|
12
|
+
* Tool names registered by THIS extension. Single source of truth so the
|
|
13
|
+
* registration sites (index.ts) and the subagent exclusion list below can't
|
|
14
|
+
* drift apart. These are our own tools, not pi built-ins, so they can't be
|
|
15
|
+
* derived from pi — but they only need defining once.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SUBAGENT_TOOL_NAMES: {
|
|
18
|
+
readonly AGENT: "Agent";
|
|
19
|
+
readonly WORKFLOW: "SubagentWorkflow";
|
|
20
|
+
readonly GET_RESULT: "get_subagent_result";
|
|
21
|
+
readonly STEER: "steer_subagent";
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Canonical name of an extension for `extensions: [...]` allowlist matching.
|
|
25
|
+
* Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
|
|
26
|
+
* resolves the same as `[mcp]`. Tool names within `ext:foo/bar` are not affected.
|
|
27
|
+
* Directory extensions (`foo/index.ts`) resolve to the parent directory name;
|
|
28
|
+
* single-file extensions to the basename minus `.ts`/`.js`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function extensionCanonicalName(extPath: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* All names an extension answers to for allowlist matching (lowercased): its
|
|
33
|
+
* path-derived {@link extensionCanonicalName} plus, when a pi package manifest
|
|
34
|
+
* declares this entry, that package's unscoped short name (`@scope/foo` → `foo`).
|
|
35
|
+
* #143: an extension installed via `pi.extensions: ["./src/index.ts"]` would
|
|
36
|
+
* otherwise only ever match as `src` (the source directory), never by its
|
|
37
|
+
* package name. The path-derived name is preserved, so it keeps matching too.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extensionCanonicalNames(extPath: string): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Classify `extensions: string[]` frontmatter entries for the loader-level filter.
|
|
42
|
+
*
|
|
43
|
+
* An entry is a PATH iff it contains a path separator or starts with `~`; otherwise
|
|
44
|
+
* it is a NAME. `"*"` sets the wildcard flag (keep all default-discovered extensions).
|
|
45
|
+
*
|
|
46
|
+
* Path entries are resolved (`~` expanded, made absolute against `cwd`) into `paths`
|
|
47
|
+
* — and their canonical name is also added to `names`. The loader override matches
|
|
48
|
+
* everything by canonical name, so path-loaded extensions are matched via their name
|
|
49
|
+
* rather than their post-staging `Extension.path`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseExtensionsSpec(entries: string[], cwd: string): {
|
|
52
|
+
names: Set<string>;
|
|
53
|
+
paths: string[];
|
|
54
|
+
wildcard: boolean;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Parse raw `ext:` selector strings (from the `tools:` CSV) into the set of
|
|
58
|
+
* extension names to keep loaded and a per-extension tool-narrowing map.
|
|
59
|
+
*
|
|
60
|
+
* `ext:foo` → `extNames` has `foo`, no narrowing entry (all of foo's tools).
|
|
61
|
+
* `ext:foo/bar` → `extNames` has `foo`, `narrowing.foo` has `bar` (only `bar`).
|
|
62
|
+
* A name lands in `narrowing` only when a `/tool` form is seen, so a bare
|
|
63
|
+
* `ext:foo` alongside `ext:foo/bar` leaves narrowing in effect (narrowing wins).
|
|
64
|
+
* The split is on the first `/`; extension canonical names never contain `/`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseExtSelectors(entries: string[]): {
|
|
67
|
+
extNames: Set<string>;
|
|
68
|
+
narrowing: Map<string, Set<string>>;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Keep a subagent's tool scope correct as extensions register tools over time.
|
|
72
|
+
*
|
|
73
|
+
* Extensions may call `registerTool` long after load — pi-mcp from `session_start`,
|
|
74
|
+
* context-mode from `before_agent_start` — so scope has to be re-derived rather than
|
|
75
|
+
* snapshotted. `registerTool` writes into the very `extension.tools` maps this reads,
|
|
76
|
+
* so `inScope()` sees late arrivals on the next call.
|
|
77
|
+
*
|
|
78
|
+
* Two enforcement points, because neither covers the whole picture:
|
|
79
|
+
*
|
|
80
|
+
* - `turn_end` re-narrows the ACTIVE set. pi emits `turn_end` immediately before
|
|
81
|
+
* `prepareNextTurn` re-snapshots `agent.state.tools`, and session listeners run
|
|
82
|
+
* synchronously, so the narrow lands in time for turns 2..N.
|
|
83
|
+
* - `beforeToolCall` blocks out-of-scope calls. Turn 1 cannot be narrowed at all:
|
|
84
|
+
* `before_agent_start` fires INSIDE `prompt()` and may widen the tool set, but
|
|
85
|
+
* `createContextSnapshot()` freezes that turn's tools immediately after — there
|
|
86
|
+
* is no hook in between. A call-time check is the only correct guard there.
|
|
87
|
+
*
|
|
88
|
+
* Both are installed on the session and deliberately NOT unsubscribed: they must
|
|
89
|
+
* outlive the `runAgent` call so resumed/steered turns stay scoped. pi's `dispose()`
|
|
90
|
+
* clears `_eventListeners`, so they die with the session rather than leaking.
|
|
91
|
+
*
|
|
92
|
+
* Only meaningful when extensions are loaded — under `noExtensions`/`isolated` the
|
|
93
|
+
* static `allowedToolNames` allowlist already gates the registry itself.
|
|
94
|
+
*/
|
|
95
|
+
export declare function installExtensionToolScope(session: AgentSession, ctx: {
|
|
96
|
+
loader: DefaultResourceLoader;
|
|
97
|
+
toolNames: string[];
|
|
98
|
+
disallowedSet: Set<string> | undefined;
|
|
99
|
+
extNames: Set<string>;
|
|
100
|
+
narrowing: Map<string, Set<string>>;
|
|
101
|
+
/** Bare `mcp__` entries from `tools:`; undefined leaves MCP tools to the rules above. */
|
|
102
|
+
mcpToolPatterns: string[] | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Injected `customTools` to keep active regardless of the built-in list.
|
|
105
|
+
*
|
|
106
|
+
* Two kinds arrive here and they are blocked for different reasons: opt-in
|
|
107
|
+
* nested-delegation tools share EXCLUDED_TOOL_NAMES' names, and
|
|
108
|
+
* StructuredOutput is simply not a built-in, so neither survives a `keep`
|
|
109
|
+
* seeded from `toolNames`.
|
|
110
|
+
*/
|
|
111
|
+
readmitToolNames: Set<string>;
|
|
112
|
+
/** Receives the scope check the child's nested-call gate extension applies. */
|
|
113
|
+
nestedCallGate: NestedCallGate;
|
|
114
|
+
}): void;
|
|
115
|
+
/** Set once the child's tool scope is installed; until then the gate admits everything. */
|
|
116
|
+
type NestedCallGate = {
|
|
117
|
+
allows?: (name: string) => boolean;
|
|
118
|
+
};
|
|
119
|
+
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
120
|
+
export declare function normalizeMaxTurns(n: number | undefined): number | undefined;
|
|
121
|
+
/** Get the default max turns value. undefined = unlimited. */
|
|
122
|
+
export declare function getDefaultMaxTurns(): number | undefined;
|
|
123
|
+
/** Set the default max turns value. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
124
|
+
export declare function setDefaultMaxTurns(n: number | undefined): void;
|
|
125
|
+
/**
|
|
126
|
+
* The turn limit a run of `type` will actually enforce: an explicit value if the
|
|
127
|
+
* caller supplied one, else the agent's own `max_turns`, else the project
|
|
128
|
+
* default. `undefined` = unlimited.
|
|
129
|
+
*
|
|
130
|
+
* Exported because the widget's turn counter (`↻3≤20`) has to predict this
|
|
131
|
+
* before the run starts, and a second copy of the expression would drift from
|
|
132
|
+
* the one below that enforces it.
|
|
133
|
+
*/
|
|
134
|
+
export declare function resolveEffectiveMaxTurns(type: string, explicit?: number): number | undefined;
|
|
135
|
+
/** Whether subagent sessions are persisted by default. */
|
|
136
|
+
export declare function getRememberAgents(): boolean;
|
|
137
|
+
/** Set whether subagent sessions are persisted by default. */
|
|
138
|
+
export declare function setRememberAgents(b: boolean): void;
|
|
139
|
+
/** Get the grace turns value. */
|
|
140
|
+
export declare function getGraceTurns(): number;
|
|
141
|
+
/** Set the grace turns value (minimum 1). */
|
|
142
|
+
export declare function setGraceTurns(n: number): void;
|
|
143
|
+
/**
|
|
144
|
+
* Try to find the right model for an agent type.
|
|
145
|
+
* Priority: explicit option > config.model > parent model.
|
|
146
|
+
*/
|
|
147
|
+
export declare function resolveDefaultModel(parentModel: Model<any> | undefined, registry: {
|
|
148
|
+
find(provider: string, modelId: string): Model<any> | undefined;
|
|
149
|
+
getAvailable?(): Model<any>[];
|
|
150
|
+
}, configModel?: string): Model<any> | undefined;
|
|
151
|
+
/** Info about a tool event in the subagent. */
|
|
152
|
+
export interface ToolActivity {
|
|
153
|
+
type: "start" | "end";
|
|
154
|
+
toolName: string;
|
|
155
|
+
}
|
|
156
|
+
export interface RunOptions {
|
|
157
|
+
/** ExtensionAPI instance — used for pi.exec() instead of execSync. */
|
|
158
|
+
pi: ExtensionAPI;
|
|
159
|
+
/** Manager-assigned id; suffixes session name to disambiguate parallel spawns (e.g. `Explore#a1b2c3d4`). */
|
|
160
|
+
agentId?: string;
|
|
161
|
+
model?: Model<any>;
|
|
162
|
+
maxTurns?: number;
|
|
163
|
+
signal?: AbortSignal;
|
|
164
|
+
/** Applies only when the agent file leaves `isolated:` unset — frontmatter is authoritative. */
|
|
165
|
+
isolated?: boolean;
|
|
166
|
+
inheritContext?: boolean;
|
|
167
|
+
thinkingLevel?: ThinkingLevel;
|
|
168
|
+
/**
|
|
169
|
+
* Reopen this pi session file rather than starting an empty conversation.
|
|
170
|
+
* `createAgentSession` seeds itself from whatever its SessionManager holds,
|
|
171
|
+
* so pointing it at an existing file rehydrates that agent's history and the
|
|
172
|
+
* prompt continues it. Everything else — tools, model, system prompt, turn
|
|
173
|
+
* caps — is still resolved from the agent type, so the continuation runs
|
|
174
|
+
* under the type's *current* definition, not the one the original run used.
|
|
175
|
+
*/
|
|
176
|
+
resumeSessionFile?: string;
|
|
177
|
+
/**
|
|
178
|
+
* True when another agent spawned this one. Only top-level agents get a
|
|
179
|
+
* handle, so only they can be reopened by name — which is the whole reason
|
|
180
|
+
* `rememberAgents` persists a session at all. A nested run's transcript would
|
|
181
|
+
* be unreachable by anything, so it stays in memory unless its own
|
|
182
|
+
* frontmatter asks otherwise.
|
|
183
|
+
*/
|
|
184
|
+
nested?: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* True when a workflow run spawned this agent. Its final text is the value
|
|
187
|
+
* `agent()` resolves to rather than a report a person reads, and the prompt
|
|
188
|
+
* says so — but only when `structuredOutput` is unset, since that child
|
|
189
|
+
* already has a `StructuredOutput` tool to answer through and two competing
|
|
190
|
+
* "this is how you return your answer" instructions is worse than one.
|
|
191
|
+
*/
|
|
192
|
+
workflow?: boolean;
|
|
193
|
+
/** Override working directory (e.g. for worktree isolation). */
|
|
194
|
+
cwd?: string;
|
|
195
|
+
/**
|
|
196
|
+
* Directory the worktree copy was created from. Set only when `cwd` points
|
|
197
|
+
* into a worktree — the prompt then tells the agent to stay in the copy
|
|
198
|
+
* instead of following the inherited parent prompt back to the main tree.
|
|
199
|
+
*/
|
|
200
|
+
worktreeBase?: string;
|
|
201
|
+
/**
|
|
202
|
+
* Where .pi config is discovered (project extensions, skills, pi settings,
|
|
203
|
+
* agent memory). Default: same as the working directory. The manager sets
|
|
204
|
+
* this to the parent session's cwd when `SpawnOptions.cwd` points the
|
|
205
|
+
* working directory elsewhere — the agent works *there* but carries the
|
|
206
|
+
* parent project's config (the target's `.pi` extensions never execute).
|
|
207
|
+
*
|
|
208
|
+
* WARNING for future callers: if you pass `cwd` pointing at a directory the
|
|
209
|
+
* user didn't open, you almost certainly must pass `configCwd` too —
|
|
210
|
+
* omitting it makes the target's `.pi` extensions execute in this process.
|
|
211
|
+
* (Worktree isolation is the one intentional exception: its copy IS the
|
|
212
|
+
* parent's repo, so config resolving inside it is correct.)
|
|
213
|
+
*/
|
|
214
|
+
configCwd?: string;
|
|
215
|
+
/** Called on tool start/end with activity info. */
|
|
216
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
217
|
+
/** Called on streaming text deltas from the assistant response. */
|
|
218
|
+
onTextDelta?: (delta: string, fullText: string) => void;
|
|
219
|
+
onSessionCreated?: (session: AgentSession) => void;
|
|
220
|
+
/** Called at the end of each agentic turn with the cumulative count. */
|
|
221
|
+
onTurnEnd?: (turnCount: number) => void;
|
|
222
|
+
/**
|
|
223
|
+
* Called once per assistant message_end with that message's usage delta.
|
|
224
|
+
* Lets callers maintain a lifetime accumulator that survives compaction
|
|
225
|
+
* (which replaces session.state.messages and resets stats-derived sums).
|
|
226
|
+
*
|
|
227
|
+
* `cost` is pi's own `usage.cost.total` for that message — priced from the
|
|
228
|
+
* model's rates, so it is 0 (not missing) for a model pi has no pricing for.
|
|
229
|
+
* We never price anything ourselves; every dollar figure this extension shows
|
|
230
|
+
* or reports traces back to this field.
|
|
231
|
+
*/
|
|
232
|
+
onAssistantUsage?: (usage: LifetimeUsage) => void;
|
|
233
|
+
/**
|
|
234
|
+
* Called when the session successfully compacts. `tokensBefore` is upstream's
|
|
235
|
+
* pre-compaction context size estimate. Aborted compactions don't fire.
|
|
236
|
+
*/
|
|
237
|
+
onCompaction?: (info: {
|
|
238
|
+
reason: "manual" | "threshold" | "overflow";
|
|
239
|
+
tokensBefore: number;
|
|
240
|
+
}) => void;
|
|
241
|
+
/**
|
|
242
|
+
* Make this child report through a `StructuredOutput` tool built from this
|
|
243
|
+
* schema, and put the validated payload on {@link RunResult.structuredJson}.
|
|
244
|
+
*
|
|
245
|
+
* Already compiled by the caller, so a schema this runtime cannot validate
|
|
246
|
+
* fails at the call that wrote it rather than inside the child.
|
|
247
|
+
*/
|
|
248
|
+
structuredOutput?: CompiledSchema;
|
|
249
|
+
/** Runtime bridge for opt-in child-safe nested delegation. */
|
|
250
|
+
nestedRuntime?: {
|
|
251
|
+
manager: NestedAgentManager;
|
|
252
|
+
parentAgentId: string;
|
|
253
|
+
depth: number;
|
|
254
|
+
maxSubagentDepth?: number;
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
export interface RunResult {
|
|
258
|
+
responseText: string;
|
|
259
|
+
session: AgentSession;
|
|
260
|
+
/** True if the agent was hard-aborted (max_turns + grace exceeded). */
|
|
261
|
+
aborted: boolean;
|
|
262
|
+
/** True if the agent was steered to wrap up (hit soft turn limit) but finished in time. */
|
|
263
|
+
steered: boolean;
|
|
264
|
+
/**
|
|
265
|
+
* A failure message for the run's FINAL assistant turn, when that turn failed:
|
|
266
|
+
* a provider error (stopReason "error"), or a "length" stop that produced no
|
|
267
|
+
* text (a silent max-token death). pi resolves an exhausted-retries failure
|
|
268
|
+
* normally instead of rejecting, so without this the manager would report such
|
|
269
|
+
* a run as completed — with an empty result, or worse, an earlier turn's text
|
|
270
|
+
* presented as the answer (#144). Undefined for a clean stop, or a "length"
|
|
271
|
+
* stop that produced text (a legitimate truncated answer).
|
|
272
|
+
*/
|
|
273
|
+
failure?: string;
|
|
274
|
+
/**
|
|
275
|
+
* The validated `StructuredOutput` payload as canonical JSON, when the caller
|
|
276
|
+
* asked for a schema and the child produced one.
|
|
277
|
+
*
|
|
278
|
+
* Deliberately not folded into {@link responseText}: `record.result` picks up
|
|
279
|
+
* a worktree branch note on the way out, which would leave the caller with
|
|
280
|
+
* unparseable JSON, and merging the two would make "produced structured
|
|
281
|
+
* output" indistinguishable from "happened to answer in JSON".
|
|
282
|
+
*/
|
|
283
|
+
structuredJson?: string;
|
|
284
|
+
/** Whether the extra structured-output prompt had to be sent. */
|
|
285
|
+
structuredRetried?: boolean;
|
|
286
|
+
}
|
|
287
|
+
export declare function runAgent(ctx: ExtensionContext, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult>;
|
|
288
|
+
/**
|
|
289
|
+
* Send a new prompt to an existing session (resume). A fresh run of the same
|
|
290
|
+
* agent: the turn limit is resolved as `runAgent` resolves it — an explicit
|
|
291
|
+
* `maxTurns`, else the agent type's own `max_turns`, else the project default
|
|
292
|
+
* — and counted from this prompt (#368).
|
|
293
|
+
*/
|
|
294
|
+
export declare function resumeAgent(session: AgentSession, prompt: string, options: Pick<RunOptions, "maxTurns" | "onTurnEnd" | "onToolActivity" | "onAssistantUsage" | "onCompaction" | "signal"> & {
|
|
295
|
+
/** The agent's type, for its own `max_turns`. */
|
|
296
|
+
type: SubagentType;
|
|
297
|
+
}): Promise<{
|
|
298
|
+
text: string;
|
|
299
|
+
failure?: string;
|
|
300
|
+
} & Pick<RunResult, "aborted" | "steered">>;
|
|
301
|
+
/**
|
|
302
|
+
* Send a steering message to a running subagent.
|
|
303
|
+
* The message will interrupt the agent after its current tool execution.
|
|
304
|
+
*/
|
|
305
|
+
export declare function steerAgent(session: AgentSession, message: string): Promise<void>;
|
|
306
|
+
/**
|
|
307
|
+
* Get the subagent's conversation messages as formatted text.
|
|
308
|
+
*/
|
|
309
|
+
export declare function getAgentConversation(session: AgentSession): string;
|
|
310
|
+
export {};
|