@mystilleef/pi-subagent 0.10.2 → 0.12.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/README.md +84 -10
- package/package.json +8 -8
- package/src/agent/agents.ts +75 -9
- package/src/child/complete-extension.ts +36 -0
- package/src/child/complete-outcome.ts +35 -0
- package/src/child/model-resolution.ts +81 -0
- package/src/child/process-utils.ts +114 -0
- package/src/child/process.ts +225 -471
- package/src/child/prompt-contract.ts +22 -10
- package/src/child/prompt-setup.ts +36 -0
- package/src/child/result-builder.ts +142 -0
- package/src/child/sampling-extension.ts +49 -0
- package/src/child/streaming-progress.ts +138 -0
- package/src/orchestration/subagent-orchestrator.ts +20 -16
- package/src/output/normalize.ts +1 -1
- package/src/output/summary.ts +22 -6
- package/src/output/ui.ts +19 -21
- package/src/progress/progress-state.ts +10 -12
- package/src/progress/result-details.ts +54 -45
- package/src/shared/limits.ts +81 -0
- package/src/shared/message-utils.ts +56 -0
- package/src/shared/resource-resolution.ts +289 -0
- package/src/shared/sampling.ts +56 -0
- package/src/shared/types.ts +1 -0
- package/src/shared/utils.ts +31 -204
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ pi -e npm:@mystilleef/pi-subagent
|
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
28
|
-
- **Asynchronous:** Agents run in the background.
|
|
28
|
+
- **Asynchronous:** Agents always run in the background.
|
|
29
29
|
- **Parallel:** Run many agents simultaneously.
|
|
30
30
|
- **Isolated:** Each delegated task receives a separate context window.
|
|
31
31
|
- **Nested:** `Subagents` can spawn other `subagents`.
|
|
@@ -108,9 +108,54 @@ YAML `frontmatter` and a Markdown system prompt body.
|
|
|
108
108
|
|
|
109
109
|
### Discovery locations
|
|
110
110
|
|
|
111
|
-
- User-global agents: `~/.pi/agents/*.md`
|
|
111
|
+
- User-global agents: `~/.pi/agent/agents/*.md`
|
|
112
112
|
- Project-local agents: nearest `.pi/agents/*.md`
|
|
113
113
|
|
|
114
|
+
### Example of an agent file
|
|
115
|
+
|
|
116
|
+
**PATH:** _~/.pi/agent/agents/lifehacks.md_
|
|
117
|
+
|
|
118
|
+
<!-- prettier-ignore-start -->
|
|
119
|
+
```md
|
|
120
|
+
---
|
|
121
|
+
name: lifehacks
|
|
122
|
+
description: Daily lifehacks
|
|
123
|
+
replace_prompt: true
|
|
124
|
+
context: false
|
|
125
|
+
thinking: xhigh
|
|
126
|
+
skills: false
|
|
127
|
+
extensions: pi-mcp-adapter
|
|
128
|
+
tools: read, grep, find, ls, mcp
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
# Role
|
|
132
|
+
|
|
133
|
+
Embody an expert philosopher and life coach. Your specialize in
|
|
134
|
+
_lifehacks_.
|
|
135
|
+
|
|
136
|
+
## Workflow
|
|
137
|
+
|
|
138
|
+
- Research and deliver 3 _profound_ and _transformative lifehacks_.
|
|
139
|
+
- Expound upon each of them.
|
|
140
|
+
- Emit unmodified result to the calling agent.
|
|
141
|
+
|
|
142
|
+
## Directives
|
|
143
|
+
|
|
144
|
+
- Forbid `copular` verb forms.
|
|
145
|
+
- **Minimum** words. **Maximum** signal.
|
|
146
|
+
- Keep prose vivid but terse.
|
|
147
|
+
- Optimize prose for token and context efficiency.
|
|
148
|
+
- Use lists and sub-lists over paragraphs and long sentences.
|
|
149
|
+
- Use elegant, well-structured, idiomatic markdown.
|
|
150
|
+
|
|
151
|
+
## Constraints
|
|
152
|
+
|
|
153
|
+
- Operate in read-only mode.
|
|
154
|
+
- Forbid all write operations.
|
|
155
|
+
- **NEVER** wrap the entire result in a code block.
|
|
156
|
+
```
|
|
157
|
+
<!-- prettier-ignore-end -->
|
|
158
|
+
|
|
114
159
|
### Required front matter
|
|
115
160
|
|
|
116
161
|
```yaml
|
|
@@ -123,19 +168,48 @@ description: Review code for correctness and maintainability.
|
|
|
123
168
|
```yaml
|
|
124
169
|
tools: read, bash, edit
|
|
125
170
|
skills: code-review
|
|
171
|
+
extensions: git-summary
|
|
172
|
+
context: false
|
|
126
173
|
thinking: medium
|
|
127
174
|
provider: deepseek
|
|
128
175
|
model: deepseek-v4-flash
|
|
176
|
+
temperature: 0.7
|
|
177
|
+
top_p: 0.9
|
|
178
|
+
replace_prompt: true
|
|
129
179
|
```
|
|
130
180
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
- `
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
- `
|
|
137
|
-
|
|
138
|
-
|
|
181
|
+
- `tools`: Specifies a comma-separated list of enabled tools. Omission
|
|
182
|
+
defaults to the child process default `toolset`.
|
|
183
|
+
- `skills`: Specifies a comma-separated list of enabled skills. Setting
|
|
184
|
+
`false` disables all skills. Omission inherits the active workspace
|
|
185
|
+
skills.
|
|
186
|
+
- `extensions`: Specifies a comma-separated list of enabled extensions.
|
|
187
|
+
Setting `false` disables all extensions except core subagent
|
|
188
|
+
extensions. Omission inherits the active workspace extensions.
|
|
189
|
+
- `context`: Controls the inclusion of project context files,
|
|
190
|
+
`AGENTS.md`. Setting `false` excludes default workspace files from the
|
|
191
|
+
child context window.
|
|
192
|
+
- `thinking`: Controls the model thinking level (values: `off`,
|
|
193
|
+
`minimal`, `low`, `medium`, `high`, `xhigh`). The child runner clamps
|
|
194
|
+
unsupported levels to supported values and prints warnings.
|
|
195
|
+
- `provider`: Specifies the model provider. Requires setting the `model`
|
|
196
|
+
field. Omission of the `model` field when defining a `provider`
|
|
197
|
+
invalidates the agent configuration.
|
|
198
|
+
- `model`: Specifies the model identifier. Agent-level model settings
|
|
199
|
+
override parent settings.
|
|
200
|
+
- `temperature`: Sets the sampling temperature. Accepts numeric values
|
|
201
|
+
between `0.0` and `1.0` inclusive. Incorrect values trigger warnings
|
|
202
|
+
during discovery and the system ignores them.
|
|
203
|
+
- `top_p`: Sets the sampling top-p value. Accepts numeric values between
|
|
204
|
+
`0.0` and `1.0` inclusive. Incorrect values trigger warnings during
|
|
205
|
+
discovery and the system ignores them.
|
|
206
|
+
- `replace_prompt`: Set `true` to replace the child system prompt base
|
|
207
|
+
with the agent body. Omitted or `false` appends the body to the
|
|
208
|
+
existing system prompt, `SYSTEM.md`, preserving current behavior.
|
|
209
|
+
Requires a non-empty agent body; `replace_prompt: true` with an empty
|
|
210
|
+
or whitespace-only body fails discovery. **Warning:** `true` replaces
|
|
211
|
+
pi built-in defaults and any project/global `SYSTEM.md`, including
|
|
212
|
+
their safety and behavior guardrails.
|
|
139
213
|
|
|
140
214
|
---
|
|
141
215
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mystilleef/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Pi subagent for the SPAE Framework",
|
|
5
5
|
"author": "Lateef Alabi-Oki <mystilleef@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,14 +64,14 @@
|
|
|
64
64
|
"typebox": "*"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@biomejs/biome": "^2.5.
|
|
68
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
69
|
-
"@earendil-works/pi-ai": "^0.
|
|
70
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
71
|
-
"@earendil-works/pi-tui": "^0.
|
|
67
|
+
"@biomejs/biome": "^2.5.2",
|
|
68
|
+
"@earendil-works/pi-agent-core": "^0.80.3",
|
|
69
|
+
"@earendil-works/pi-ai": "^0.80.3",
|
|
70
|
+
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
71
|
+
"@earendil-works/pi-tui": "^0.80.3",
|
|
72
72
|
"@types/bun": "^1.3.14",
|
|
73
|
-
"@types/node": "^
|
|
74
|
-
"typebox": "^1.
|
|
73
|
+
"@types/node": "^26.1.0",
|
|
74
|
+
"typebox": "^1.3.4",
|
|
75
75
|
"typescript": "^6.0.3"
|
|
76
76
|
}
|
|
77
77
|
}
|
package/src/agent/agents.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Dirent } from "node:fs";
|
|
|
2
2
|
import * as fsPromises from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { isValidSamplingValue } from "../shared/sampling.js";
|
|
5
6
|
|
|
6
7
|
export type AgentSource = "user" | "project";
|
|
7
8
|
export type AgentScope = AgentSource | "both";
|
|
@@ -20,11 +21,16 @@ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
|
20
21
|
export interface AgentConfig {
|
|
21
22
|
name: string;
|
|
22
23
|
description: string;
|
|
24
|
+
context?: false | undefined;
|
|
23
25
|
tools?: string[] | undefined;
|
|
24
|
-
skills?: string[] | undefined;
|
|
26
|
+
skills?: string[] | false | undefined;
|
|
27
|
+
extensions?: string[] | undefined;
|
|
25
28
|
thinking?: ThinkingLevel | undefined;
|
|
26
29
|
model?: string | undefined;
|
|
27
30
|
provider?: string | undefined;
|
|
31
|
+
temperature?: number | undefined;
|
|
32
|
+
topP?: number | undefined;
|
|
33
|
+
replacePrompt?: true | undefined;
|
|
28
34
|
systemPrompt: string;
|
|
29
35
|
source: AgentSource;
|
|
30
36
|
filePath: string;
|
|
@@ -68,6 +74,21 @@ function parseCommaList(raw: unknown): string[] | undefined {
|
|
|
68
74
|
return items.length > 0 ? items : undefined;
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
function parseExtensions(raw: unknown): string[] | undefined {
|
|
78
|
+
if (raw === undefined) return undefined;
|
|
79
|
+
if (raw === false || Array.isArray(raw)) return [];
|
|
80
|
+
if (typeof raw === "string") {
|
|
81
|
+
const trimmed = raw.trim();
|
|
82
|
+
if (trimmed.length === 0) return [];
|
|
83
|
+
const items = trimmed
|
|
84
|
+
.split(",")
|
|
85
|
+
.map((s) => s.trim())
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
return items.length > 0 ? items : [];
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
function parseThinkingLevel(raw: unknown): ThinkingLevel | undefined {
|
|
72
93
|
if (typeof raw !== "string") return undefined;
|
|
73
94
|
const normalized = raw.trim().toLowerCase();
|
|
@@ -82,6 +103,23 @@ function parseOptionalString(raw: unknown): string | undefined {
|
|
|
82
103
|
return normalized.length > 0 ? normalized : undefined;
|
|
83
104
|
}
|
|
84
105
|
|
|
106
|
+
function isNonStringOptional(raw: unknown): boolean {
|
|
107
|
+
return raw != null && typeof raw !== "string";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseSamplingValue(
|
|
111
|
+
raw: unknown,
|
|
112
|
+
field: string,
|
|
113
|
+
agentName: string,
|
|
114
|
+
): number | undefined {
|
|
115
|
+
if (raw === undefined) return undefined;
|
|
116
|
+
if (isValidSamplingValue(raw)) return raw;
|
|
117
|
+
console.warn(
|
|
118
|
+
`Warning: Agent '${agentName}' has invalid '${field}' value: ${raw}`,
|
|
119
|
+
);
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
function parseAgentConfig(
|
|
86
124
|
content: string,
|
|
87
125
|
source: AgentSource,
|
|
@@ -104,26 +142,49 @@ function parseAgentConfig(
|
|
|
104
142
|
const {
|
|
105
143
|
name,
|
|
106
144
|
description,
|
|
145
|
+
context: rawContext,
|
|
107
146
|
tools: rawTools,
|
|
108
147
|
skills: rawSkills,
|
|
148
|
+
extensions: rawExtensions,
|
|
109
149
|
thinking: rawThinking,
|
|
110
150
|
model: rawModel,
|
|
111
151
|
provider: rawProvider,
|
|
152
|
+
temperature: rawTemperature,
|
|
153
|
+
top_p: rawTopP,
|
|
154
|
+
replace_prompt: rawReplacePrompt,
|
|
112
155
|
} = frontmatter;
|
|
113
156
|
if (typeof name !== "string" || typeof description !== "string") return null;
|
|
114
|
-
if (
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
if (
|
|
157
|
+
if (rawContext !== undefined && typeof rawContext !== "boolean") return null;
|
|
158
|
+
if (rawReplacePrompt !== undefined && typeof rawReplacePrompt !== "boolean")
|
|
159
|
+
return null;
|
|
160
|
+
if (isNonStringOptional(rawTools)) return null;
|
|
161
|
+
if (rawSkills != null && typeof rawSkills !== "string" && rawSkills !== false)
|
|
162
|
+
return null;
|
|
163
|
+
if (
|
|
164
|
+
rawExtensions !== undefined &&
|
|
165
|
+
rawExtensions !== false &&
|
|
166
|
+
typeof rawExtensions !== "string" &&
|
|
167
|
+
!(Array.isArray(rawExtensions) && rawExtensions.length === 0)
|
|
168
|
+
)
|
|
169
|
+
return null;
|
|
170
|
+
if (isNonStringOptional(rawThinking)) return null;
|
|
171
|
+
if (isNonStringOptional(rawModel)) return null;
|
|
172
|
+
if (isNonStringOptional(rawProvider)) return null;
|
|
119
173
|
const tools = parseCommaList(rawTools);
|
|
120
|
-
const skills =
|
|
121
|
-
|
|
122
|
-
|
|
174
|
+
const skills =
|
|
175
|
+
rawSkills === false
|
|
176
|
+
? false
|
|
177
|
+
: rawSkills !== undefined
|
|
178
|
+
? (parseCommaList(rawSkills) ?? [])
|
|
179
|
+
: undefined;
|
|
123
180
|
const thinking = parseThinkingLevel(rawThinking);
|
|
124
181
|
const model = parseOptionalString(rawModel);
|
|
125
182
|
const provider = parseOptionalString(rawProvider);
|
|
126
183
|
if (provider !== undefined && model === undefined) return null;
|
|
184
|
+
const temperature = parseSamplingValue(rawTemperature, "temperature", name);
|
|
185
|
+
const topP = parseSamplingValue(rawTopP, "top_p", name);
|
|
186
|
+
const extensions = parseExtensions(rawExtensions);
|
|
187
|
+
if (rawReplacePrompt === true && body.trim().length === 0) return null;
|
|
127
188
|
return {
|
|
128
189
|
name,
|
|
129
190
|
description,
|
|
@@ -135,6 +196,11 @@ function parseAgentConfig(
|
|
|
135
196
|
systemPrompt: body,
|
|
136
197
|
source,
|
|
137
198
|
filePath,
|
|
199
|
+
...(rawContext === false && { context: false }),
|
|
200
|
+
...(temperature !== undefined && { temperature }),
|
|
201
|
+
...(topP !== undefined && { topP }),
|
|
202
|
+
...(extensions !== undefined && { extensions }),
|
|
203
|
+
...(rawReplacePrompt === true && { replacePrompt: true }),
|
|
138
204
|
};
|
|
139
205
|
}
|
|
140
206
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
export const completeParams = Type.Object({
|
|
5
|
+
outcome: Type.String({
|
|
6
|
+
description:
|
|
7
|
+
"A short, single-sentence summary of the task outcome. Keep it concise, brief, and under 100 characters.",
|
|
8
|
+
minLength: 1,
|
|
9
|
+
pattern: "^[\\s\\S]*\\S[\\s\\S]*$",
|
|
10
|
+
}),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const completeTool = defineTool({
|
|
14
|
+
name: "complete",
|
|
15
|
+
label: "Complete",
|
|
16
|
+
description:
|
|
17
|
+
"Complete the task and report the structured outcome. Call this as your final action.",
|
|
18
|
+
promptSnippet: "Complete the task and report the structured outcome.",
|
|
19
|
+
promptGuidelines: [
|
|
20
|
+
"Call complete as your final action to report the outcome after completing the task.",
|
|
21
|
+
],
|
|
22
|
+
parameters: completeParams,
|
|
23
|
+
async execute(_toolCallId, params) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: params.outcome }],
|
|
26
|
+
details: {
|
|
27
|
+
outcome: params.outcome,
|
|
28
|
+
},
|
|
29
|
+
terminate: true,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export default function (pi: ExtensionAPI) {
|
|
35
|
+
pi.registerTool(completeTool);
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Value } from "typebox/value";
|
|
3
|
+
import { completeParams } from "./complete-extension.js";
|
|
4
|
+
|
|
5
|
+
export function getOutcomeString(source: unknown): string | undefined {
|
|
6
|
+
if (Value.Check(completeParams, source)) {
|
|
7
|
+
return source.outcome.trim();
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts the latest valid outcome from complete tool call arguments in messages.
|
|
14
|
+
*
|
|
15
|
+
* Reads from call arguments (message_end) rather than tool results (tool_result_end)
|
|
16
|
+
* because tool_result_end is unreliable when terminate: true causes pi to exit before
|
|
17
|
+
* delivering it. Call arguments are always delivered via message_end.
|
|
18
|
+
*/
|
|
19
|
+
export function getLatestOutcomeFromMessages(
|
|
20
|
+
messages: Message[] | undefined,
|
|
21
|
+
): string | undefined {
|
|
22
|
+
if (!messages?.length) return undefined;
|
|
23
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
24
|
+
const msg = messages[i];
|
|
25
|
+
if (msg?.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
26
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
27
|
+
const part = msg.content[j];
|
|
28
|
+
if (part?.type !== "toolCall") continue;
|
|
29
|
+
if (part.name !== "complete" || !part.id) continue;
|
|
30
|
+
const outcome = getOutcomeString(part.arguments);
|
|
31
|
+
if (outcome) return outcome;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model and thinking level resolution for subagent child processes.
|
|
3
|
+
* Handles provider/model selection and thinking level clamping.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
clampThinkingLevel,
|
|
8
|
+
getSupportedThinkingLevels,
|
|
9
|
+
type ModelThinkingLevel,
|
|
10
|
+
} from "@earendil-works/pi-ai";
|
|
11
|
+
import { getModel } from "@earendil-works/pi-ai/compat";
|
|
12
|
+
import type { ThinkingLevel } from "../agent/agents.js";
|
|
13
|
+
|
|
14
|
+
export type ChildModelSettings = {
|
|
15
|
+
provider?: string | undefined;
|
|
16
|
+
id?: string | undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the effective thinking level for a model, clamping to supported levels.
|
|
21
|
+
* Returns a warning message if the requested level differs from the effective level.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveThinkingLevel(
|
|
24
|
+
requested: ThinkingLevel,
|
|
25
|
+
provider: string,
|
|
26
|
+
modelId: string,
|
|
27
|
+
): { level: ThinkingLevel; warning?: string } {
|
|
28
|
+
const model = getModel(provider as never, modelId as never);
|
|
29
|
+
if (!model) return { level: requested };
|
|
30
|
+
const mkWarning = (effective: ThinkingLevel) =>
|
|
31
|
+
`Thinking level "${requested}" not supported by model "${provider}/${modelId}"; using "${effective}" instead`;
|
|
32
|
+
if (model.reasoning === false) {
|
|
33
|
+
return { level: "off", warning: mkWarning("off") };
|
|
34
|
+
}
|
|
35
|
+
if (!model.thinkingLevelMap) return { level: requested };
|
|
36
|
+
const supported = getSupportedThinkingLevels(model);
|
|
37
|
+
if (supported.length === 0) return { level: requested };
|
|
38
|
+
const clamped = clampThinkingLevel(
|
|
39
|
+
model,
|
|
40
|
+
requested as ModelThinkingLevel,
|
|
41
|
+
) as ThinkingLevel;
|
|
42
|
+
if (clamped === requested) return { level: requested };
|
|
43
|
+
return { level: clamped, warning: mkWarning(clamped) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolves effective child model settings by merging agent config with parent settings.
|
|
48
|
+
* Agent-specific settings take precedence over parent settings.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveEffectiveChildModelSettings(
|
|
51
|
+
agent: { provider?: string | undefined; model?: string | undefined },
|
|
52
|
+
parentModel: ChildModelSettings | undefined,
|
|
53
|
+
): ChildModelSettings {
|
|
54
|
+
return {
|
|
55
|
+
provider: agent.provider ?? parentModel?.provider,
|
|
56
|
+
id:
|
|
57
|
+
agent.model ??
|
|
58
|
+
(agent.provider === undefined ? parentModel?.id : undefined),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds a display string for the model and thinking level.
|
|
64
|
+
* Returns undefined if no parts are available.
|
|
65
|
+
*/
|
|
66
|
+
export function buildModelDisplay(
|
|
67
|
+
effectiveModel: ChildModelSettings,
|
|
68
|
+
thinking: ThinkingLevel,
|
|
69
|
+
): string | undefined {
|
|
70
|
+
const parts: string[] = [];
|
|
71
|
+
if (effectiveModel.provider) {
|
|
72
|
+
parts.push(effectiveModel.provider);
|
|
73
|
+
}
|
|
74
|
+
if (effectiveModel.id) {
|
|
75
|
+
parts.push(effectiveModel.id);
|
|
76
|
+
}
|
|
77
|
+
if (thinking) {
|
|
78
|
+
parts.push(thinking);
|
|
79
|
+
}
|
|
80
|
+
return parts.length > 0 ? parts.join(" ・ ") : undefined;
|
|
81
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for child process management.
|
|
3
|
+
* Handles byte-limited string appending, UTF-8 truncation, and context window resolution.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
10
|
+
import { getModel } from "@earendil-works/pi-ai/compat";
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
function resolveExtensionPath(baseName: string, dir?: string): string {
|
|
16
|
+
const resolvedDir = dir ?? __dirname;
|
|
17
|
+
const ext = __filename.endsWith(".ts") ? ".ts" : ".js";
|
|
18
|
+
const primary = path.join(resolvedDir, `${baseName}${ext}`);
|
|
19
|
+
if (fs.existsSync(primary)) return primary;
|
|
20
|
+
const altExt = ext === ".ts" ? ".js" : ".ts";
|
|
21
|
+
const fallback = path.join(resolvedDir, `${baseName}${altExt}`);
|
|
22
|
+
if (fs.existsSync(fallback)) return fallback;
|
|
23
|
+
throw new Error(`${baseName} not found at ${primary} or ${fallback}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the absolute path to the complete-extension file.
|
|
28
|
+
* Prefers the extension matching the current runtime, then falls back to the
|
|
29
|
+
* alternate extension. Throws if neither exists.
|
|
30
|
+
* Pass `dir` in tests to use a controlled directory.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveCompleteExtensionPath(dir?: string): string {
|
|
33
|
+
return resolveExtensionPath("complete-extension", dir);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolves the absolute path to the sampling-extension file.
|
|
38
|
+
* Prefers the extension matching the current runtime, then falls back to the
|
|
39
|
+
* alternate extension. Throws if neither exists.
|
|
40
|
+
* Pass `dir` in tests to use a controlled directory.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveSamplingExtensionPath(dir?: string): string {
|
|
43
|
+
return resolveExtensionPath("sampling-extension", dir);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolves the absolute path to the pi-subagent package entry extension file.
|
|
48
|
+
* This is the main index that registers the subagent tool and commands.
|
|
49
|
+
* Prefers the extension matching the current runtime, then falls back to the
|
|
50
|
+
* alternate extension. Throws if neither exists.
|
|
51
|
+
* Pass `dir` in tests to use a controlled directory.
|
|
52
|
+
*/
|
|
53
|
+
export function resolvePackageExtensionPath(dir?: string): string {
|
|
54
|
+
return resolveExtensionPath("../index", dir);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Appends data to a string while enforcing a maximum byte limit.
|
|
59
|
+
* Handles both string and Buffer inputs, truncating at valid UTF-8 boundaries.
|
|
60
|
+
*/
|
|
61
|
+
export function appendWithByteLimit(
|
|
62
|
+
current: string,
|
|
63
|
+
data: string | Buffer,
|
|
64
|
+
max: number,
|
|
65
|
+
): string {
|
|
66
|
+
const currentBytes = Buffer.from(current, "utf-8");
|
|
67
|
+
if (currentBytes.length >= max) return current;
|
|
68
|
+
const incomingBytes = Buffer.isBuffer(data)
|
|
69
|
+
? data
|
|
70
|
+
: Buffer.from(data, "utf-8");
|
|
71
|
+
const combined = Buffer.concat([currentBytes, incomingBytes]);
|
|
72
|
+
if (combined.length <= max) return combined.toString("utf-8");
|
|
73
|
+
return truncateValidUtf8(combined, max);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Truncates a buffer to the specified byte limit while preserving valid UTF-8 sequences.
|
|
78
|
+
* Returns an empty string if no valid truncation point is found.
|
|
79
|
+
*/
|
|
80
|
+
export function truncateValidUtf8(buffer: Buffer, max: number): string {
|
|
81
|
+
let end = Math.min(max, buffer.length);
|
|
82
|
+
while (end > 0) {
|
|
83
|
+
const candidate = buffer.subarray(0, end).toString("utf-8");
|
|
84
|
+
if (!candidate.endsWith("�")) return candidate;
|
|
85
|
+
end -= 1;
|
|
86
|
+
}
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolves the context window size in tokens for a given message.
|
|
92
|
+
* Returns undefined if the message doesn't have valid provider/model info
|
|
93
|
+
* or if the model lookup fails.
|
|
94
|
+
*
|
|
95
|
+
* Rationale: Subagent usage reporting needs context window awareness to provide
|
|
96
|
+
* meaningful "context full" indicators to the parent.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveContextWindowTokens(msg: Message): number | undefined {
|
|
99
|
+
const m = msg as unknown as Record<string, unknown>;
|
|
100
|
+
if (typeof m["provider"] !== "string" || typeof m["model"] !== "string")
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
const contextWindow = getModel(
|
|
104
|
+
m["provider"] as never,
|
|
105
|
+
m["model"] as never,
|
|
106
|
+
)?.contextWindow;
|
|
107
|
+
return Number.isFinite(contextWindow) && contextWindow > 0
|
|
108
|
+
? contextWindow
|
|
109
|
+
: undefined;
|
|
110
|
+
} catch {
|
|
111
|
+
/* model lookup failures return undefined to skip context window tracking */
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|