decorated-pi 0.7.1 → 0.7.3
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 +29 -8
- package/commands/dp-settings.ts +3 -3
- package/hooks/compaction.ts +115 -85
- package/hooks/externalize.ts +48 -35
- package/hooks/inject-agents-md.ts +20 -12
- package/hooks/mcp.ts +61 -64
- package/hooks/pi-tool-filter.ts +2 -1
- package/hooks/rtk.ts +24 -96
- package/hooks/skeleton.ts +86 -23
- package/hooks/smart-at.ts +59 -7
- package/hooks/wakatime.ts +16 -28
- package/index.ts +122 -4
- package/package.json +2 -4
- package/settings.ts +164 -4
- package/skills/pi-docs/SKILL.md +13 -0
- package/tools/ask/index.ts +0 -3
- package/tools/lsp/servers.ts +31 -15
- package/tools/lsp/tools.ts +0 -1
- package/tools/mcp/builtin/codegraph.ts +1 -1
- package/tools/mcp/client.ts +1 -1
- package/tools/mcp/config.ts +34 -15
- package/tools/patch/core.ts +10 -0
- package/tools/patch/index.ts +338 -226
- package/tsconfig.json +1 -0
- package/ui/ask.ts +72 -22
- package/ui/module-settings.ts +166 -16
- package/utils/which.ts +109 -0
package/README.md
CHANGED
|
@@ -14,25 +14,34 @@ pi install /path/to/decorated-pi
|
|
|
14
14
|
|
|
15
15
|
### 1. Token Efficiency
|
|
16
16
|
|
|
17
|
-
Multiple layers of token savings that compound across every session.
|
|
17
|
+
Multiple layers of token savings that compound across every session.
|
|
18
18
|
|
|
19
|
-
**RTK** — integrates [RTK](https://github.com/rtk-ai/rtk) to compress bash output into structured summaries, so the LLM never sees raw noise.
|
|
19
|
+
**RTK** — integrates [RTK](https://github.com/rtk-ai/rtk) to compress bash output into structured summaries, so the LLM never sees raw noise. **Just install the CLI, zero config**.
|
|
20
20
|
|
|
21
|
-
**
|
|
21
|
+
**Codegraph** — integrates [codegraph](https://github.com/colbymchenry/codegraph) to offer a code map of your project, so the LLM can navigate symbols and call graphs without chaining `ls` → `grep` → `read`. **You should manage the code source index yourself, see [codegraph doc](https://github.com/colbymchenry/codegraph/blob/main/README.md)**.
|
|
22
22
|
|
|
23
23
|
**Auxiliary Models** — offloads heavy-but-dumb tasks to cheaper models so your primary model only pays for the hard work:
|
|
24
24
|
|
|
25
|
-
- **Image
|
|
26
|
-
- **Compact
|
|
25
|
+
- **Image Read Fallback** — detects image type via magic bytes, calls a configured vision-capable model, and injects the analysis text, so your main model never touches image tokens
|
|
26
|
+
- **Compact Model** — handles context compaction with a smaller model instead of burning main-model capacity
|
|
27
27
|
|
|
28
|
-
Configured via `/dp-model`.
|
|
28
|
+
> Configured via `/dp-model`.
|
|
29
29
|
|
|
30
|
-
**Cache‑friendly
|
|
30
|
+
**Cache‑friendly Design** — stable system prompt prefix:
|
|
31
31
|
|
|
32
32
|
- tool definitions, guidelines, and skills are sorted alphabetically so the system prompt is identical across sessions
|
|
33
33
|
- volatile elements like `Current date: …` are stripped before prompt assembly
|
|
34
34
|
- MCP tool schemas are persisted to a local cache, so the tool list stays stable regardless of network conditions or server availability
|
|
35
35
|
|
|
36
|
+
**Pi Native Prompt Slimming**
|
|
37
|
+
|
|
38
|
+
- move the default Pi documentation block out of the system prompt and into a builtin `pi-docs` skill, so the docs reference loads on demand instead of sitting in every turn's prompt
|
|
39
|
+
- unregister the native `write` tool, since `bash` can handle it
|
|
40
|
+
|
|
41
|
+
**Large Result Externalization**
|
|
42
|
+
|
|
43
|
+
- a `tool_result` hook that catches ANY tool output (bash, read, MCP, etc.) exceeding 30 KB and saves the full content to `/tmp/decorated-pi-results/<tool>-<callId>.txt`. Pi's native truncation still keeps a chunk of the original content in the prompt; this replaces it entirely with a one‑line pointer (`[Output too long, saved to /tmp/…]`) so the LLM knows where the result is and reads it on demand — strictly fewer tokens per turn
|
|
44
|
+
|
|
36
45
|
### 2. Smarter Tools
|
|
37
46
|
|
|
38
47
|
Drop‑in replacements for Pi's built‑in tools, with better UX and fewer wasted turns.
|
|
@@ -145,7 +154,7 @@ Example redaction on a `read` / `bash` output:
|
|
|
145
154
|
|
|
146
155
|
## Configuration
|
|
147
156
|
|
|
148
|
-
Runtime settings in `~/.pi/agent/decorated-pi.json`.
|
|
157
|
+
Runtime settings in `~/.pi/agent/decorated-pi.json`. run `/dp-settings` to configure it.
|
|
149
158
|
|
|
150
159
|
```json
|
|
151
160
|
{
|
|
@@ -166,10 +175,22 @@ Runtime settings in `~/.pi/agent/decorated-pi.json`. Modules can be toggled via
|
|
|
166
175
|
"retry": true,
|
|
167
176
|
"usage": true
|
|
168
177
|
}
|
|
178
|
+
},
|
|
179
|
+
"dependencies": {
|
|
180
|
+
"rtk": {
|
|
181
|
+
"path": "/custom/bin/rtk",
|
|
182
|
+
"dontBother": false
|
|
183
|
+
},
|
|
184
|
+
"wakatime-cli": {
|
|
185
|
+
"dontBother": true
|
|
186
|
+
}
|
|
169
187
|
}
|
|
170
188
|
}
|
|
171
189
|
```
|
|
172
190
|
|
|
191
|
+
- `modules` can be toggled on/off to enable/disable features. All are enabled by default.
|
|
192
|
+
- `dependencies[binaryName].path` overrides the lookup location for a binary (file or directory). `dependencies[binaryName].dontBother` silences missing-dependency notifications for that binary. Both are optional
|
|
193
|
+
|
|
173
194
|
## License
|
|
174
195
|
|
|
175
196
|
MIT
|
package/commands/dp-settings.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* /dp-settings —
|
|
2
|
+
* /dp-settings — settings for decorated-pi.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -8,12 +8,12 @@ import { ModuleSettingsComponent } from "../ui/module-settings.js";
|
|
|
8
8
|
|
|
9
9
|
export function registerDpSettingsCommand(pi: ExtensionAPI): void {
|
|
10
10
|
pi.registerCommand("dp-settings", {
|
|
11
|
-
description: "
|
|
11
|
+
description: "Settings for decorated-pi",
|
|
12
12
|
handler: async (_args, ctx) => {
|
|
13
13
|
if (ctx.hasUI) {
|
|
14
14
|
await ctx.ui.custom<void>(
|
|
15
15
|
(tui, theme, _kb, done) =>
|
|
16
|
-
new ModuleSettingsComponent(tui, theme, () => done(undefined))
|
|
16
|
+
new ModuleSettingsComponent(tui, theme, ctx.ui, () => done(undefined))
|
|
17
17
|
);
|
|
18
18
|
// Only prompt for reload when the effective settings differ from
|
|
19
19
|
// the snapshot taken when pi loaded the extension.
|
package/hooks/compaction.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* compaction — custom compaction model + auto-resume.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* On `session_before_compact`, runs pi-coding-agent's `compact()` against
|
|
5
|
+
* the model configured in settings (rather than the agent's current model)
|
|
6
|
+
* and returns the result so pi uses our summary instead of running its
|
|
7
|
+
* default. If the configured model is missing, auth fails, or the call
|
|
8
|
+
* throws, we fall through (return undefined) and pi runs its own compaction.
|
|
9
|
+
*
|
|
10
|
+
* On `session_compact`, if the compaction was auto-triggered, sends a
|
|
11
|
+
* "continue" message to resume the agent loop.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
15
|
+
import { compact } from "@earendil-works/pi-coding-agent";
|
|
11
16
|
import type { Model } from "@earendil-works/pi-ai";
|
|
12
17
|
import { isContextOverflow } from "@earendil-works/pi-ai";
|
|
13
18
|
import * as fs from "node:fs";
|
|
@@ -19,7 +24,6 @@ import type { Module, Skeleton } from "./skeleton.js";
|
|
|
19
24
|
|
|
20
25
|
interface PiCompactionSettings {
|
|
21
26
|
enabled: boolean;
|
|
22
|
-
reserveTokens: number;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
interface AutoCompactionCandidate {
|
|
@@ -29,7 +33,6 @@ interface AutoCompactionCandidate {
|
|
|
29
33
|
|
|
30
34
|
const DEFAULT_PI_COMPACTION_SETTINGS: PiCompactionSettings = {
|
|
31
35
|
enabled: true,
|
|
32
|
-
reserveTokens: 16_384,
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
function readJsonObject(filePath: string): any | undefined {
|
|
@@ -52,7 +55,6 @@ function loadPiCompactionSettings(cwd: string): PiCompactionSettings {
|
|
|
52
55
|
};
|
|
53
56
|
return {
|
|
54
57
|
enabled: merged.enabled !== false,
|
|
55
|
-
reserveTokens: typeof merged.reserveTokens === "number" ? merged.reserveTokens : DEFAULT_PI_COMPACTION_SETTINGS.reserveTokens,
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -63,30 +65,24 @@ function getLastAssistantMessage(messages: any[]): any | undefined {
|
|
|
63
65
|
return undefined;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!lastAssistant) return false;
|
|
74
|
-
const contextWindow = usage?.contextWindow ?? 0;
|
|
75
|
-
if (contextWindow > 0 && isContextOverflow(lastAssistant, contextWindow)) return true;
|
|
76
|
-
if (!usage || usage.tokens === null) return false;
|
|
77
|
-
return usage.tokens > usage.contextWindow - settings.reserveTokens;
|
|
78
|
-
}
|
|
79
|
-
|
|
68
|
+
/** Mirror pi's _runAutoCompaction(reason, willRetry) decision:
|
|
69
|
+
* - overflow → willRetry=true → auto-resume the agent
|
|
70
|
+
* - threshold → willRetry=false → no auto-resume (user continues manually)
|
|
71
|
+
* - manual /compact → no resume
|
|
72
|
+
* Detected from the post-agent-end candidate: overflow shows up as a
|
|
73
|
+
* context-overflow error on the last assistant message; threshold is a
|
|
74
|
+
* pre-emptive compaction and we intentionally skip auto-resume for it. */
|
|
80
75
|
function shouldAutoResumeCompaction(
|
|
81
|
-
|
|
82
|
-
postAgentEndCandidate: AutoCompactionCandidate | null,
|
|
76
|
+
candidate: AutoCompactionCandidate | null,
|
|
83
77
|
settings: PiCompactionSettings,
|
|
84
78
|
customInstructions?: string,
|
|
85
79
|
): boolean {
|
|
86
80
|
if (customInstructions !== undefined) return false;
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
if (!candidate || !settings.enabled) return false;
|
|
82
|
+
const lastAssistant = getLastAssistantMessage(candidate.messages);
|
|
83
|
+
if (!lastAssistant) return false;
|
|
84
|
+
const contextWindow = candidate.usage?.contextWindow ?? 0;
|
|
85
|
+
return contextWindow > 0 && isContextOverflow(lastAssistant, contextWindow);
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
function getConfiguredCompactModel(registry: any): Model<any> | null {
|
|
@@ -97,92 +93,127 @@ function getConfiguredCompactModel(registry: any): Model<any> | null {
|
|
|
97
93
|
return registry.find(parsed.provider, parsed.modelId) ?? null;
|
|
98
94
|
}
|
|
99
95
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
): Promise<string> {
|
|
106
|
-
const { complete } = await import("@earendil-works/pi-ai");
|
|
107
|
-
const ct = serializeConversation(convertToLlm(messages));
|
|
108
|
-
const resp = await complete(model, {
|
|
109
|
-
systemPrompt: "You are a context summarization assistant. Produce a structured summary only.",
|
|
110
|
-
messages: [{ role: "user" as const, content: [{ type: "text" as const, text: `<conversation>\n${ct}\n</conversation>\n\n${TURN_PREFIX_PROMPT}` }], timestamp: Date.now() }],
|
|
111
|
-
}, { maxTokens: Math.floor(0.5 * reserveTokens), signal, apiKey, headers });
|
|
112
|
-
if (resp.stopReason === "error") throw new Error(resp.errorMessage ?? "Turn prefix summarization failed");
|
|
113
|
-
return resp.content.filter((c): c is { type: "text"; text: string } => c.type === "text").map(c => c.text).join("\n");
|
|
96
|
+
/** Per-session state. Indexed by sessionId (from ctx.sessionManager) so
|
|
97
|
+
* two concurrent pi sessions don't trample each other's flags. */
|
|
98
|
+
interface SessionState {
|
|
99
|
+
postAgentEndCandidate: AutoCompactionCandidate | null;
|
|
100
|
+
currentCompactionIsAuto: boolean;
|
|
114
101
|
}
|
|
115
102
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
103
|
+
const sessionStates = new Map<string, SessionState>();
|
|
104
|
+
|
|
105
|
+
function getSessionState(sessionId: string): SessionState {
|
|
106
|
+
let s = sessionStates.get(sessionId);
|
|
107
|
+
if (!s) {
|
|
108
|
+
s = { postAgentEndCandidate: null, currentCompactionIsAuto: false };
|
|
109
|
+
sessionStates.set(sessionId, s);
|
|
110
|
+
}
|
|
111
|
+
return s;
|
|
112
|
+
}
|
|
119
113
|
|
|
120
114
|
export const compactionModule: Module = {
|
|
121
115
|
name: "compaction",
|
|
122
116
|
hooks: {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
session_shutdown: [
|
|
118
|
+
(_event, ctx) => {
|
|
119
|
+
// Clean up state when the session ends so the Map doesn't grow.
|
|
120
|
+
sessionStates.delete(ctx.sessionManager.getSessionId());
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
input: [
|
|
124
|
+
(_event, ctx) => {
|
|
125
|
+
getSessionState(ctx.sessionManager.getSessionId()).postAgentEndCandidate = null;
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
before_agent_start: [
|
|
129
|
+
(_event, ctx) => {
|
|
130
|
+
getSessionState(ctx.sessionManager.getSessionId()).postAgentEndCandidate = null;
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
agent_start: [
|
|
134
|
+
(_event, ctx) => {
|
|
135
|
+
getSessionState(ctx.sessionManager.getSessionId()).postAgentEndCandidate = null;
|
|
136
|
+
},
|
|
137
|
+
],
|
|
126
138
|
agent_end: [
|
|
127
139
|
(event, ctx) => {
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
getSessionState(ctx.sessionManager.getSessionId()).postAgentEndCandidate = {
|
|
141
|
+
messages: event.messages,
|
|
142
|
+
usage: ctx.getContextUsage(),
|
|
143
|
+
};
|
|
130
144
|
},
|
|
131
145
|
],
|
|
132
146
|
session_before_compact: [
|
|
133
|
-
async (event, ctx) => {
|
|
147
|
+
async (event, ctx, pi) => {
|
|
148
|
+
const sessionState = getSessionState(ctx.sessionManager.getSessionId());
|
|
134
149
|
const compactionSettings = loadPiCompactionSettings(ctx.cwd);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
// Pi only auto-compacts after agent_end (see _checkCompaction in
|
|
151
|
+
// agent-session.js), so we detect "auto" via the post-agent-end
|
|
152
|
+
// overflow heuristic. Manual /compact carries customInstructions
|
|
153
|
+
// and skips auto-resume.
|
|
154
|
+
const isAutoResume = shouldAutoResumeCompaction(
|
|
155
|
+
sessionState.postAgentEndCandidate,
|
|
139
156
|
compactionSettings,
|
|
157
|
+
event.customInstructions,
|
|
140
158
|
);
|
|
141
|
-
|
|
142
|
-
// and no custom instructions given.
|
|
143
|
-
const isAutoResume = isAuto && !event.customInstructions;
|
|
144
|
-
currentCompactionIsAuto = isAutoResume;
|
|
145
|
-
prePromptCompactionPending = false;
|
|
146
|
-
postAgentEndCandidate = null;
|
|
159
|
+
sessionState.postAgentEndCandidate = null;
|
|
147
160
|
|
|
148
161
|
const model = getConfiguredCompactModel(ctx.modelRegistry);
|
|
149
|
-
if (!model) return;
|
|
162
|
+
if (!model) return; // No custom compact model configured → let pi run its default compaction.
|
|
150
163
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
151
164
|
if (!auth.ok) {
|
|
152
165
|
if (ctx.hasUI) ctx.ui.notify(`Compact model auth failed: ${auth.error}`, "warning");
|
|
153
|
-
return;
|
|
166
|
+
return; // Auth failed → fall through to default compaction; pi handles its own resume.
|
|
154
167
|
}
|
|
155
168
|
const { preparation, customInstructions, signal } = event;
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
if (ctx.hasUI) {
|
|
170
|
+
ctx.ui.notify(
|
|
171
|
+
`🗜️ Compacting with ${model.id} (${preparation.tokensBefore.toLocaleString()} tokens)...`,
|
|
172
|
+
"info",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
// Mirror pi's native behavior: compact() uses the agent session's
|
|
176
|
+
// current thinking level (agent-session.js passes this.thinkingLevel
|
|
177
|
+
// to compact() in both manual and auto compaction paths).
|
|
178
|
+
const thinkingLevel = pi.getThinkingLevel();
|
|
158
179
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
180
|
+
// Delegate to pi-coding-agent's compact() — it does both
|
|
181
|
+
// summarization (using our model) and file-op extraction, and
|
|
182
|
+
// returns the exact CompactionResult shape pi's runner expects.
|
|
183
|
+
const result = await compact(
|
|
184
|
+
preparation,
|
|
185
|
+
model,
|
|
186
|
+
auth.apiKey ?? "",
|
|
187
|
+
auth.headers,
|
|
188
|
+
customInstructions,
|
|
189
|
+
signal,
|
|
190
|
+
thinkingLevel,
|
|
191
|
+
);
|
|
192
|
+
// Only mark as auto-resumed if OUR hook did the compaction. If we
|
|
193
|
+
// had fallen through to pi's default path above, pi's own
|
|
194
|
+
// _runAutoCompaction would call agent.continue() on overflow;
|
|
195
|
+
// marking here would cause a duplicate resume message on top.
|
|
196
|
+
sessionState.currentCompactionIsAuto = isAutoResume;
|
|
197
|
+
return { compaction: result };
|
|
176
198
|
} catch (err) {
|
|
177
199
|
if (signal.aborted) return;
|
|
178
|
-
|
|
200
|
+
// Returning undefined lets pi run its default compaction with the
|
|
201
|
+
// active agent model. Surface the failure so the user knows their
|
|
202
|
+
// custom model didn't take effect.
|
|
203
|
+
if (ctx.hasUI) {
|
|
204
|
+
ctx.ui.notify(
|
|
205
|
+
`Custom compact failed (${err instanceof Error ? err.message : err}); using default model.`,
|
|
206
|
+
"warning",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
179
209
|
}
|
|
180
210
|
},
|
|
181
211
|
],
|
|
182
212
|
session_compact: [
|
|
183
|
-
(_event,
|
|
184
|
-
const
|
|
185
|
-
|
|
213
|
+
(_event, ctx, pi) => {
|
|
214
|
+
const sessionState = getSessionState(ctx.sessionManager.getSessionId());
|
|
215
|
+
const shouldResume = sessionState.currentCompactionIsAuto;
|
|
216
|
+
sessionState.currentCompactionIsAuto = false;
|
|
186
217
|
if (!shouldResume) return;
|
|
187
218
|
pi.sendMessage({
|
|
188
219
|
customType: "auto_compact_resume",
|
|
@@ -199,6 +230,5 @@ export function setupCompaction(sk: Skeleton): void {
|
|
|
199
230
|
}
|
|
200
231
|
|
|
201
232
|
export const __modelIntegrationTest = {
|
|
202
|
-
shouldExpectAutoCompaction,
|
|
203
233
|
shouldAutoResumeCompaction,
|
|
204
234
|
};
|
package/hooks/externalize.ts
CHANGED
|
@@ -14,7 +14,10 @@ import * as path from "node:path";
|
|
|
14
14
|
import { randomBytes } from "node:crypto";
|
|
15
15
|
import type { Module } from "./skeleton.js";
|
|
16
16
|
|
|
17
|
-
export const TOOL_OUTPUT_TEMP_DIR = path.join(
|
|
17
|
+
export const TOOL_OUTPUT_TEMP_DIR = path.join(
|
|
18
|
+
os.tmpdir(),
|
|
19
|
+
"decorated-pi-results",
|
|
20
|
+
);
|
|
18
21
|
export const OUTPUT_EXTERNALIZE_THRESHOLD = 30_000;
|
|
19
22
|
|
|
20
23
|
/** Write content to a temp file under TOOL_OUTPUT_TEMP_DIR.
|
|
@@ -22,49 +25,59 @@ export const OUTPUT_EXTERNALIZE_THRESHOLD = 30_000;
|
|
|
22
25
|
* Exported so other modules (e.g. tools/mcp/externalize.ts) can
|
|
23
26
|
* write to the same location. */
|
|
24
27
|
export function writeOutputToTemp(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
toolName: string,
|
|
29
|
+
toolCallId: string,
|
|
30
|
+
content: string,
|
|
28
31
|
): string | undefined {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(TOOL_OUTPUT_TEMP_DIR))
|
|
34
|
+
fs.mkdirSync(TOOL_OUTPUT_TEMP_DIR, { recursive: true });
|
|
35
|
+
const id = toolCallId
|
|
36
|
+
? toolCallId.slice(0, 12)
|
|
37
|
+
: randomBytes(8).toString("hex");
|
|
38
|
+
const filePath = path.join(
|
|
39
|
+
TOOL_OUTPUT_TEMP_DIR,
|
|
40
|
+
`${toolName}-${id}.txt`,
|
|
41
|
+
);
|
|
42
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
43
|
+
return filePath;
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
/** Externalize a tool_result event if content is above the threshold.
|
|
41
50
|
* Returns the modified event, or undefined to leave the original untouched. */
|
|
42
51
|
export function maybeExternalizeToolResult(event: any): any | undefined {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
if (!Array.isArray(event.content) || event.content.length === 0)
|
|
53
|
+
return undefined;
|
|
54
|
+
const first = event.content[0];
|
|
55
|
+
if (!first || first.type !== "text" || typeof first.text !== "string")
|
|
56
|
+
return undefined;
|
|
57
|
+
const text = first.text;
|
|
58
|
+
if (text.length <= OUTPUT_EXTERNALIZE_THRESHOLD) return undefined;
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
const filePath = writeOutputToTemp(event.toolName, event.toolCallId, text);
|
|
61
|
+
if (!filePath) return undefined;
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
return {
|
|
64
|
+
...event,
|
|
65
|
+
content: [
|
|
66
|
+
{
|
|
67
|
+
type: "text" as const,
|
|
68
|
+
text: `[Output too long, saved to ${filePath}.]`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
export const externalizeModule: Module = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
name: "externalize",
|
|
76
|
+
hooks: {
|
|
77
|
+
tool_result: [
|
|
78
|
+
(event) => {
|
|
79
|
+
return maybeExternalizeToolResult(event);
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
70
83
|
};
|
|
@@ -9,6 +9,17 @@ import { dirname, resolve, relative, join, normalize } from "node:path";
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import type { Module } from "./skeleton.js";
|
|
11
11
|
|
|
12
|
+
function isInsideSkillDir(filePath: string): boolean {
|
|
13
|
+
let dir = dirname(resolve(filePath));
|
|
14
|
+
while (true) {
|
|
15
|
+
if (existsSync(join(dir, "SKILL.md"))) return true;
|
|
16
|
+
const parent = dirname(dir);
|
|
17
|
+
if (parent === dir) break;
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
const CUSTOM_TYPE = "decorated-pi.subdir-agents";
|
|
13
24
|
const AGENTS_NAMES = ["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"];
|
|
14
25
|
|
|
@@ -86,7 +97,12 @@ function findNewAgents(
|
|
|
86
97
|
return results.reverse();
|
|
87
98
|
}
|
|
88
99
|
|
|
89
|
-
export const __subdirAgentsTest = { restoreFromBranch, findNewAgents };
|
|
100
|
+
export const __subdirAgentsTest = { restoreFromBranch, findNewAgents, isInsideSkillDir };
|
|
101
|
+
|
|
102
|
+
export const INJECT_AGENTS_MD_GUIDANCE = [
|
|
103
|
+
"### Context Loading, AGENTS.md / CLAUDE.md are auto-injected",
|
|
104
|
+
"- DO NOT read any **AGENTS.md** or **CLAUDE.md** files unless you're explicitly asked to, these files will loaded automatically if necessary.",
|
|
105
|
+
].join("\n");
|
|
90
106
|
|
|
91
107
|
export const injectAgentsMdModule: Module = {
|
|
92
108
|
name: "inject-agents-md",
|
|
@@ -111,7 +127,9 @@ export const injectAgentsMdModule: Module = {
|
|
|
111
127
|
if (event.toolName !== "read" && event.toolName !== "edit")
|
|
112
128
|
return;
|
|
113
129
|
const path = (event.input as { path?: string })?.path;
|
|
114
|
-
if (path)
|
|
130
|
+
if (!path) return;
|
|
131
|
+
if (isInsideSkillDir(resolve(path))) return;
|
|
132
|
+
pendingPaths.set(event.toolCallId, path);
|
|
115
133
|
},
|
|
116
134
|
],
|
|
117
135
|
tool_result: [
|
|
@@ -152,13 +170,3 @@ export const injectAgentsMdModule: Module = {
|
|
|
152
170
|
],
|
|
153
171
|
},
|
|
154
172
|
};
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* System-prompt guidance for the inject-agents-md hook — tells the
|
|
158
|
-
* LLM not to waste tool calls re-reading AGENTS.md / CLAUDE.md, since
|
|
159
|
-
* this hook already auto-injects them.
|
|
160
|
-
*/
|
|
161
|
-
export const INJECT_AGENTS_MD_GUIDANCE = [
|
|
162
|
-
"### Context Loading, AGENTS.md / CLAUDE.md are auto-injected",
|
|
163
|
-
"- DO NOT read any **AGENTS.md** or **CLAUDE.md** files unless you're explicitly asked to, these files will loaded automatically if necessary.",
|
|
164
|
-
].join("\n");
|