@pi-kaush/pi-agent-mode 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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/package.json +54 -0
- package/src/agent-discovery.ts +168 -0
- package/src/index.ts +337 -0
- package/src/model-spec.ts +41 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Add the `/agent` command for activating a configured Pi agent as a persistent mode in the current session.
|
|
6
|
+
- Support `/agent <name>`, an interactive selector, and `/agent none` / `/agent off` restoration of the pre-activation baseline.
|
|
7
|
+
- Capture model, thinking level, and active tools as one baseline on first activation; keep it across agent switches.
|
|
8
|
+
- Append agent prompts to Pi's base prompt, apply tool allowlists with missing-tool reporting, and support model thinking suffixes (`:low`, `:high`, `:max`).
|
|
9
|
+
- Restore the last active agent across session resume and reload, reading both the namespaced `pi-agent-mode/state` entry and legacy `active-agent-state` entries (newest recognized entry on the active branch wins).
|
|
10
|
+
- Keep the package standalone with zero runtime dependencies: discovery and display helpers are self-contained and no delegated-subagent runner is required.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kaushik Gopal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# pi-agent-mode
|
|
2
|
+
|
|
3
|
+
Activates a configured Pi agent as a persistent mode in the current session. It switches
|
|
4
|
+
the session to the agent's model, thinking level, and tool set, appends the agent's
|
|
5
|
+
prompt to Pi's base instructions, and restores the pre-activation baseline when cleared.
|
|
6
|
+
It does not spawn children and provides no delegated subagents.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pi install npm:@pi-kaush/pi-agent-mode
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Restart Pi or run `/reload`. To pin a specific release, append its version, such as
|
|
15
|
+
`@0.1.0`.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
/agent open an agent selector
|
|
21
|
+
/agent <name> activate the named user or project agent
|
|
22
|
+
/agent none restore the pre-activation model, thinking level, and tools
|
|
23
|
+
/agent off same as /agent none
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Agent definitions come from Markdown files in Pi's user agent directory
|
|
27
|
+
(`~/.pi/agent/agents/*.md`) or the nearest project `agents/` directory (project
|
|
28
|
+
agents take precedence by name and are confirmed before activation unless the
|
|
29
|
+
agent sets `confirmProjectAgents: false`).
|
|
30
|
+
|
|
31
|
+
Supported frontmatter:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
---
|
|
35
|
+
name: reviewer # required
|
|
36
|
+
description: ... # required
|
|
37
|
+
emoji: 🔍 # optional, shown in the footer status
|
|
38
|
+
tools: read, grep # optional allowlist; unavailable tools are reported and omitted
|
|
39
|
+
model: provider/model:high # optional; thinking suffix may be :off/:low/:medium/:high/:max
|
|
40
|
+
confirmProjectAgents: false # project agents default to requiring confirmation
|
|
41
|
+
---
|
|
42
|
+
Prompt body appended to Pi's base instructions while the agent is active.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Behavior
|
|
46
|
+
|
|
47
|
+
- The first activation captures the current model, thinking level, and active
|
|
48
|
+
tools as one baseline. Switching agents keeps that original baseline, so
|
|
49
|
+
clearing always returns to the pre-`/agent` session state.
|
|
50
|
+
- Missing models or failed authentication abort activation without corrupting
|
|
51
|
+
the previous state.
|
|
52
|
+
- The active agent survives session resume and reload; the footer status shows
|
|
53
|
+
the active agent, including its emoji when configured.
|
|
54
|
+
- Durable state is written as a namespaced `pi-agent-mode/state` session entry.
|
|
55
|
+
Legacy `active-agent-state` entries from earlier local implementations are
|
|
56
|
+
still read on resume; the newest recognized entry on the active branch wins.
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
npm run check
|
|
62
|
+
npm pack --workspace @pi-kaush/pi-agent-mode --dry-run
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Load the live source in isolation:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
pi --no-extensions -e ~/dev/oss/pi-kaush/extensions/pi-agent-mode/src/index.ts
|
|
69
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-kaush/pi-agent-mode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Activate a configured Pi agent as a persistent mode in the current session.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Kaushik Gopal",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": "./src/index.ts",
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pi-package",
|
|
12
|
+
"pi-extension",
|
|
13
|
+
"agent",
|
|
14
|
+
"mode"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/kaushikgopal/pi-kaush/tree/main/extensions/pi-agent-mode#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/kaushikgopal/pi-kaush/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/kaushikgopal/pi-kaush.git",
|
|
23
|
+
"directory": "extensions/pi-agent-mode"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"CHANGELOG.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"pi": {
|
|
32
|
+
"extensions": [
|
|
33
|
+
"./src/index.ts"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-coding-agent": ">=0.80.6"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"@earendil-works/pi-coding-agent": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"package:check": "node ./scripts/check-package.mjs"
|
|
52
|
+
},
|
|
53
|
+
"sideEffects": false
|
|
54
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery for `/agent` mode activation.
|
|
3
|
+
*
|
|
4
|
+
* Discovery reads Markdown agent definitions from Pi's user agent directory
|
|
5
|
+
* and the nearest project `agents/` directory. Agent Markdown files remain
|
|
6
|
+
* the source of persona/model/tool definitions; this module never spawns
|
|
7
|
+
* children or runs delegated subagents.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
CONFIG_DIR_NAME,
|
|
14
|
+
getAgentDir,
|
|
15
|
+
parseFrontmatter,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
export type AgentScope = "user" | "project" | "both";
|
|
19
|
+
|
|
20
|
+
export interface AgentConfig {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
emoji?: string;
|
|
24
|
+
tools?: string[];
|
|
25
|
+
model?: string;
|
|
26
|
+
confirmProjectAgents?: boolean;
|
|
27
|
+
systemPrompt: string;
|
|
28
|
+
source: "user" | "project";
|
|
29
|
+
filePath: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface AgentFrontmatter extends Record<string, unknown> {
|
|
33
|
+
name?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
emoji?: string;
|
|
36
|
+
tools?: string;
|
|
37
|
+
model?: string;
|
|
38
|
+
confirmProjectAgents?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AgentDiscoveryResult {
|
|
42
|
+
agents: AgentConfig[];
|
|
43
|
+
projectAgentsDir: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AgentIdentity {
|
|
47
|
+
name: string;
|
|
48
|
+
emoji?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatAgentDisplayName(agent: AgentIdentity): string {
|
|
52
|
+
const emoji = agent.emoji?.trim();
|
|
53
|
+
return emoji ? `${emoji} ${agent.name}` : agent.name;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadAgentsFromDir(
|
|
57
|
+
dir: string,
|
|
58
|
+
source: "user" | "project",
|
|
59
|
+
): AgentConfig[] {
|
|
60
|
+
const agents: AgentConfig[] = [];
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(dir)) {
|
|
63
|
+
return agents;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let entries: fs.Dirent[];
|
|
67
|
+
try {
|
|
68
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
69
|
+
} catch {
|
|
70
|
+
return agents;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
75
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
76
|
+
|
|
77
|
+
const filePath = path.join(dir, entry.name);
|
|
78
|
+
let content: string;
|
|
79
|
+
try {
|
|
80
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { frontmatter, body } = parseFrontmatter<AgentFrontmatter>(content);
|
|
86
|
+
|
|
87
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const tools = frontmatter.tools
|
|
92
|
+
?.split(",")
|
|
93
|
+
.map((t: string) => t.trim())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
|
|
96
|
+
const emoji = frontmatter.emoji?.trim();
|
|
97
|
+
agents.push({
|
|
98
|
+
name: frontmatter.name,
|
|
99
|
+
description: frontmatter.description,
|
|
100
|
+
...(emoji ? { emoji } : {}),
|
|
101
|
+
...(tools && tools.length > 0 ? { tools } : {}),
|
|
102
|
+
...(frontmatter.model ? { model: frontmatter.model } : {}),
|
|
103
|
+
...(typeof frontmatter.confirmProjectAgents === "boolean"
|
|
104
|
+
? { confirmProjectAgents: frontmatter.confirmProjectAgents }
|
|
105
|
+
: {}),
|
|
106
|
+
systemPrompt: body,
|
|
107
|
+
source,
|
|
108
|
+
filePath,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return agents;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isDirectory(p: string): boolean {
|
|
116
|
+
try {
|
|
117
|
+
return fs.statSync(p).isDirectory();
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
124
|
+
let currentDir = cwd;
|
|
125
|
+
while (true) {
|
|
126
|
+
const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents");
|
|
127
|
+
if (isDirectory(candidate)) return candidate;
|
|
128
|
+
|
|
129
|
+
const parentDir = path.dirname(currentDir);
|
|
130
|
+
if (parentDir === currentDir) return null;
|
|
131
|
+
currentDir = parentDir;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function discoverAgents(
|
|
136
|
+
cwd: string,
|
|
137
|
+
scope: AgentScope,
|
|
138
|
+
): AgentDiscoveryResult {
|
|
139
|
+
const userDir = path.join(getAgentDir(), "agents");
|
|
140
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
141
|
+
|
|
142
|
+
const userAgents =
|
|
143
|
+
scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
144
|
+
const projectAgents =
|
|
145
|
+
scope === "user" || !projectAgentsDir
|
|
146
|
+
? []
|
|
147
|
+
: loadAgentsFromDir(projectAgentsDir, "project");
|
|
148
|
+
|
|
149
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
150
|
+
|
|
151
|
+
if (scope === "both") {
|
|
152
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
153
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
154
|
+
} else if (scope === "user") {
|
|
155
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
156
|
+
} else {
|
|
157
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function shouldConfirmProjectAgent(
|
|
164
|
+
agent: AgentConfig,
|
|
165
|
+
invocationSetting?: boolean,
|
|
166
|
+
): boolean {
|
|
167
|
+
return invocationSetting ?? agent.confirmProjectAgents ?? true;
|
|
168
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/agent` — activate a configured Pi agent as a persistent mode in the
|
|
3
|
+
* current session.
|
|
4
|
+
*
|
|
5
|
+
* This package is a mode on the current agent session: it switches model,
|
|
6
|
+
* thinking level, and active tools, appends the agent prompt to the base
|
|
7
|
+
* prompt, and restores the pre-activation baseline on `/agent none` or
|
|
8
|
+
* `/agent off`. It does not spawn children and provides no delegated
|
|
9
|
+
* subagents.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
CustomEntry,
|
|
14
|
+
ExtensionAPI,
|
|
15
|
+
ExtensionContext,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import {
|
|
18
|
+
discoverAgents,
|
|
19
|
+
formatAgentDisplayName,
|
|
20
|
+
shouldConfirmProjectAgent,
|
|
21
|
+
type AgentConfig,
|
|
22
|
+
} from "./agent-discovery.ts";
|
|
23
|
+
import { parseAgentModelSpec, type ThinkingLevel } from "./model-spec.ts";
|
|
24
|
+
|
|
25
|
+
interface ModelReference {
|
|
26
|
+
provider: string;
|
|
27
|
+
id: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AgentModelSpec {
|
|
31
|
+
model: string;
|
|
32
|
+
thinkingLevel?: ThinkingLevel;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { parseAgentModelSpec };
|
|
36
|
+
|
|
37
|
+
// Durable session state is namespaced so it cannot collide with other
|
|
38
|
+
// packages. Legacy Aikado sessions wrote `active-agent-state`; restore reads
|
|
39
|
+
// both types and prefers the newest recognized entry on the active branch so
|
|
40
|
+
// sessions activated by either implementation resume correctly.
|
|
41
|
+
const PI_AGENT_MODE_STATE_TYPE = "pi-agent-mode/state";
|
|
42
|
+
const LEGACY_ACTIVE_AGENT_STATE_TYPE = "active-agent-state";
|
|
43
|
+
const ACTIVE_AGENT_STATE_TYPES = [
|
|
44
|
+
LEGACY_ACTIVE_AGENT_STATE_TYPE,
|
|
45
|
+
PI_AGENT_MODE_STATE_TYPE,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
interface AgentBaseline {
|
|
49
|
+
model?: ModelReference;
|
|
50
|
+
thinkingLevel: ThinkingLevel;
|
|
51
|
+
tools: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ActiveAgent {
|
|
55
|
+
agent: AgentConfig;
|
|
56
|
+
baseline: AgentBaseline;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ActiveAgentState {
|
|
60
|
+
active: boolean;
|
|
61
|
+
name?: string;
|
|
62
|
+
baseline?: AgentBaseline;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function registerAgentMode(pi: ExtensionAPI) {
|
|
66
|
+
let activeAgent: ActiveAgent | undefined;
|
|
67
|
+
|
|
68
|
+
function getBaseline(ctx: ExtensionContext): AgentBaseline {
|
|
69
|
+
return {
|
|
70
|
+
...(ctx.model
|
|
71
|
+
? { model: { provider: ctx.model.provider, id: ctx.model.id } }
|
|
72
|
+
: {}),
|
|
73
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
74
|
+
tools: pi.getActiveTools(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateAgentStatus(ctx: ExtensionContext) {
|
|
79
|
+
if (!activeAgent) {
|
|
80
|
+
ctx.ui.setStatus("active-agent", undefined);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ctx.ui.setStatus(
|
|
85
|
+
"active-agent",
|
|
86
|
+
ctx.ui.theme.bold(
|
|
87
|
+
ctx.ui.theme.fg("warning", formatAgentDisplayName(activeAgent.agent)),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function persistActiveAgent() {
|
|
93
|
+
const state: ActiveAgentState = activeAgent
|
|
94
|
+
? {
|
|
95
|
+
active: true,
|
|
96
|
+
name: activeAgent.agent.name,
|
|
97
|
+
baseline: activeAgent.baseline,
|
|
98
|
+
}
|
|
99
|
+
: { active: false };
|
|
100
|
+
pi.appendEntry(PI_AGENT_MODE_STATE_TYPE, state);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findModel(modelSpec: string, ctx: ExtensionContext) {
|
|
104
|
+
const spec = modelSpec.trim();
|
|
105
|
+
const separator = spec.indexOf("/");
|
|
106
|
+
if (separator > 0 && separator < spec.length - 1) {
|
|
107
|
+
return ctx.modelRegistry.find(
|
|
108
|
+
spec.slice(0, separator),
|
|
109
|
+
spec.slice(separator + 1),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const matches = ctx.modelRegistry
|
|
114
|
+
.getAll()
|
|
115
|
+
.filter((model) => model.id === spec || model.name === spec);
|
|
116
|
+
return (
|
|
117
|
+
matches.find((model) => model.provider === ctx.model?.provider) ??
|
|
118
|
+
matches[0]
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function restoreModel(
|
|
123
|
+
model: ModelReference | undefined,
|
|
124
|
+
ctx: ExtensionContext,
|
|
125
|
+
): Promise<boolean> {
|
|
126
|
+
if (!model) return true;
|
|
127
|
+
if (ctx.model?.provider === model.provider && ctx.model.id === model.id)
|
|
128
|
+
return true;
|
|
129
|
+
|
|
130
|
+
const resolved = ctx.modelRegistry.find(model.provider, model.id);
|
|
131
|
+
if (!resolved) {
|
|
132
|
+
ctx.ui.notify(
|
|
133
|
+
`Cannot restore model ${model.provider}/${model.id}: it is no longer available.`,
|
|
134
|
+
"warning",
|
|
135
|
+
);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (await pi.setModel(resolved)) return true;
|
|
139
|
+
|
|
140
|
+
ctx.ui.notify(
|
|
141
|
+
`Cannot restore model ${model.provider}/${model.id}: authentication is unavailable.`,
|
|
142
|
+
"warning",
|
|
143
|
+
);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function applyAgentTools(
|
|
148
|
+
agent: AgentConfig,
|
|
149
|
+
baseline: AgentBaseline,
|
|
150
|
+
ctx: ExtensionContext,
|
|
151
|
+
) {
|
|
152
|
+
const requested = agent.tools?.length ? agent.tools : baseline.tools;
|
|
153
|
+
const available = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
154
|
+
const valid = requested.filter((tool) => available.has(tool));
|
|
155
|
+
const missing = requested.filter((tool) => !available.has(tool));
|
|
156
|
+
|
|
157
|
+
if (missing.length > 0) {
|
|
158
|
+
ctx.ui.notify(
|
|
159
|
+
`Agent ${formatAgentDisplayName(agent)}: unavailable tools ignored: ${missing.join(", ")}`,
|
|
160
|
+
"warning",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
pi.setActiveTools(valid);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function activateAgent(
|
|
167
|
+
agent: AgentConfig,
|
|
168
|
+
ctx: ExtensionContext,
|
|
169
|
+
baseline = activeAgent?.baseline ?? getBaseline(ctx),
|
|
170
|
+
persist = true,
|
|
171
|
+
): Promise<boolean> {
|
|
172
|
+
baseline = {
|
|
173
|
+
...baseline,
|
|
174
|
+
thinkingLevel: baseline.thinkingLevel ?? pi.getThinkingLevel(),
|
|
175
|
+
};
|
|
176
|
+
let thinkingLevel = baseline.thinkingLevel;
|
|
177
|
+
if (agent.model) {
|
|
178
|
+
const modelSpec = parseAgentModelSpec(agent.model);
|
|
179
|
+
const model = findModel(modelSpec.model, ctx);
|
|
180
|
+
if (!model) {
|
|
181
|
+
ctx.ui.notify(
|
|
182
|
+
`Agent ${formatAgentDisplayName(agent)}: configured model "${agent.model}" was not found.`,
|
|
183
|
+
"error",
|
|
184
|
+
);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (!(await pi.setModel(model))) {
|
|
188
|
+
ctx.ui.notify(
|
|
189
|
+
`Agent ${formatAgentDisplayName(agent)}: authentication is unavailable for ${agent.model}.`,
|
|
190
|
+
"error",
|
|
191
|
+
);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
thinkingLevel = modelSpec.thinkingLevel ?? baseline.thinkingLevel;
|
|
195
|
+
} else {
|
|
196
|
+
await restoreModel(baseline.model, ctx);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pi.setThinkingLevel(thinkingLevel);
|
|
200
|
+
applyAgentTools(agent, baseline, ctx);
|
|
201
|
+
activeAgent = { agent, baseline };
|
|
202
|
+
updateAgentStatus(ctx);
|
|
203
|
+
if (persist) persistActiveAgent();
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function clearActiveAgent(ctx: ExtensionContext) {
|
|
208
|
+
if (!activeAgent) {
|
|
209
|
+
ctx.ui.notify("No active agent.", "info");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await restoreModel(activeAgent.baseline.model, ctx);
|
|
214
|
+
pi.setThinkingLevel(activeAgent.baseline.thinkingLevel);
|
|
215
|
+
pi.setActiveTools(activeAgent.baseline.tools);
|
|
216
|
+
activeAgent = undefined;
|
|
217
|
+
updateAgentStatus(ctx);
|
|
218
|
+
persistActiveAgent();
|
|
219
|
+
ctx.ui.notify(
|
|
220
|
+
"Active agent cleared; previous model and tools restored.",
|
|
221
|
+
"info",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function confirmProjectAgent(
|
|
226
|
+
agent: AgentConfig,
|
|
227
|
+
ctx: ExtensionContext,
|
|
228
|
+
): Promise<boolean> {
|
|
229
|
+
if (
|
|
230
|
+
agent.source !== "project" ||
|
|
231
|
+
!ctx.hasUI ||
|
|
232
|
+
!shouldConfirmProjectAgent(agent)
|
|
233
|
+
)
|
|
234
|
+
return true;
|
|
235
|
+
return ctx.ui.confirm(
|
|
236
|
+
"Activate project-local agent?",
|
|
237
|
+
`Agent: ${formatAgentDisplayName(agent)}\nSource: ${agent.filePath}\n\nProject agents are repo-controlled. Only continue for trusted projects.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
pi.registerCommand("agent", {
|
|
242
|
+
description:
|
|
243
|
+
"Activate a named agent in this session; use /agent none to restore defaults",
|
|
244
|
+
handler: async (args, ctx) => {
|
|
245
|
+
const requested = args.trim();
|
|
246
|
+
if (requested === "none" || requested === "off") {
|
|
247
|
+
await clearActiveAgent(ctx);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const agents = discoverAgents(ctx.cwd, "both").agents;
|
|
252
|
+
if (agents.length === 0) {
|
|
253
|
+
ctx.ui.notify("No agents found.", "warning");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let agent: AgentConfig | undefined;
|
|
258
|
+
if (requested) {
|
|
259
|
+
agent = agents.find((candidate) => candidate.name === requested);
|
|
260
|
+
if (!agent) {
|
|
261
|
+
ctx.ui.notify(
|
|
262
|
+
`Unknown agent "${requested}". Available: ${agents.map((candidate) => formatAgentDisplayName(candidate)).join(", ")}`,
|
|
263
|
+
"warning",
|
|
264
|
+
);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
if (!ctx.hasUI) {
|
|
269
|
+
ctx.ui.notify(
|
|
270
|
+
`Usage: /agent <name|none>. Available: ${agents.map((agent) => formatAgentDisplayName(agent)).join(", ")}`,
|
|
271
|
+
"info",
|
|
272
|
+
);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const choices = agents.map(
|
|
276
|
+
(candidate) =>
|
|
277
|
+
`${formatAgentDisplayName(candidate)} — ${candidate.description}`,
|
|
278
|
+
);
|
|
279
|
+
const choice = await ctx.ui.select("Activate agent:", choices);
|
|
280
|
+
if (!choice) return;
|
|
281
|
+
agent = agents[choices.indexOf(choice)];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!agent || !(await confirmProjectAgent(agent, ctx))) return;
|
|
285
|
+
if (await activateAgent(agent, ctx)) {
|
|
286
|
+
ctx.ui.notify(`Active agent: ${formatAgentDisplayName(agent)}`, "info");
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
pi.on("before_agent_start", (event) => {
|
|
292
|
+
if (!activeAgent) return;
|
|
293
|
+
return {
|
|
294
|
+
systemPrompt: `${event.systemPrompt}\n\n## Active agent: ${formatAgentDisplayName(activeAgent.agent)}\nThe following agent definition is active for this session. Follow it in addition to Pi's base instructions and loaded project context.\n\n${activeAgent.agent.systemPrompt}`,
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
299
|
+
activeAgent = undefined;
|
|
300
|
+
const stateEntry = ctx.sessionManager
|
|
301
|
+
.getBranch()
|
|
302
|
+
.filter(
|
|
303
|
+
(entry): entry is CustomEntry<ActiveAgentState> =>
|
|
304
|
+
entry.type === "custom" &&
|
|
305
|
+
ACTIVE_AGENT_STATE_TYPES.includes(entry.customType),
|
|
306
|
+
)
|
|
307
|
+
.pop();
|
|
308
|
+
const state = stateEntry?.data;
|
|
309
|
+
if (!state?.active || !state.name) {
|
|
310
|
+
updateAgentStatus(ctx);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const agent = discoverAgents(ctx.cwd, "both").agents.find(
|
|
315
|
+
(candidate) => candidate.name === state.name,
|
|
316
|
+
);
|
|
317
|
+
if (!agent) {
|
|
318
|
+
ctx.ui.notify(
|
|
319
|
+
`Active agent "${state.name}" is no longer available.`,
|
|
320
|
+
"warning",
|
|
321
|
+
);
|
|
322
|
+
updateAgentStatus(ctx);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await activateAgent(agent, ctx, state.baseline ?? getBaseline(ctx), false);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
330
|
+
ctx.ui.setStatus("active-agent", undefined);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Pi loads the package as an extension through this default factory.
|
|
335
|
+
export default async function (pi: ExtensionAPI) {
|
|
336
|
+
registerAgentMode(pi);
|
|
337
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing of agent-configured model specifications.
|
|
3
|
+
*
|
|
4
|
+
* Agent frontmatter `model` values may carry a thinking suffix such as
|
|
5
|
+
* `:low`, `:high`, or `:max`. Unknown suffixes are left attached to the
|
|
6
|
+
* model id so lookup can decide whether the id as a whole exists.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type ThinkingLevel =
|
|
10
|
+
| "off"
|
|
11
|
+
| "minimal"
|
|
12
|
+
| "low"
|
|
13
|
+
| "medium"
|
|
14
|
+
| "high"
|
|
15
|
+
| "xhigh"
|
|
16
|
+
| "max";
|
|
17
|
+
|
|
18
|
+
const THINKING_LEVELS = new Set<ThinkingLevel>([
|
|
19
|
+
"off",
|
|
20
|
+
"minimal",
|
|
21
|
+
"low",
|
|
22
|
+
"medium",
|
|
23
|
+
"high",
|
|
24
|
+
"xhigh",
|
|
25
|
+
"max",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export interface AgentModelSpec {
|
|
29
|
+
model: string;
|
|
30
|
+
thinkingLevel?: ThinkingLevel;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseAgentModelSpec(modelSpec: string): AgentModelSpec {
|
|
34
|
+
const spec = modelSpec.trim();
|
|
35
|
+
const separator = spec.lastIndexOf(":");
|
|
36
|
+
if (separator <= 0 || separator === spec.length - 1) return { model: spec };
|
|
37
|
+
|
|
38
|
+
const suffix = spec.slice(separator + 1) as ThinkingLevel;
|
|
39
|
+
if (!THINKING_LEVELS.has(suffix)) return { model: spec };
|
|
40
|
+
return { model: spec.slice(0, separator), thinkingLevel: suffix };
|
|
41
|
+
}
|