@oh-my-pi/pi-coding-agent 16.4.1 → 16.4.2
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 +8 -0
- package/dist/cli.js +3390 -3288
- package/dist/types/mcp/manager.d.ts +1 -1
- package/dist/types/mcp/oauth-flow.d.ts +1 -0
- package/dist/types/system-prompt.d.ts +8 -2
- package/dist/types/task/prompt-policy.d.ts +2 -0
- package/package.json +13 -13
- package/src/mcp/manager.ts +67 -64
- package/src/mcp/oauth-flow.ts +2 -0
- package/src/prompts/system/system-prompt.md +18 -5
- package/src/prompts/tools/task.md +2 -7
- package/src/sdk.ts +5 -1
- package/src/session/agent-session.ts +6 -7
- package/src/session/session-manager.ts +2 -1
- package/src/system-prompt.ts +17 -3
- package/src/task/index.ts +1 -5
- package/src/task/prompt-policy.ts +8 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type TSchema } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import type { SourceMeta } from "../capability/types.js";
|
|
3
3
|
import type { CustomTool } from "../extensibility/custom-tools/types.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type AuthStorage } from "../session/auth-storage.js";
|
|
5
5
|
import type { McpConnectionStatusEvent } from "./startup-events.js";
|
|
6
6
|
import type { MCPToolDetails } from "./tool-bridge.js";
|
|
7
7
|
import type { MCPToolCache } from "./tool-cache.js";
|
|
@@ -125,6 +125,7 @@ export declare class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
125
125
|
*/
|
|
126
126
|
export interface RefreshMCPOAuthTokenOptions {
|
|
127
127
|
fetch?: FetchImpl;
|
|
128
|
+
signal?: AbortSignal;
|
|
128
129
|
/**
|
|
129
130
|
* Authorization-server URL the original grant was minted against. Used to
|
|
130
131
|
* filter same-origin resource indicators on refresh. Defaults to `tokenUrl`'s
|
|
@@ -97,8 +97,12 @@ export interface BuildSystemPromptOptions {
|
|
|
97
97
|
eagerTasks?: boolean;
|
|
98
98
|
/** When true, the Eager Tasks section uses the hard MUST/ONLY wording (`task.eager: always`) rather than the softer `preferred` nudge. */
|
|
99
99
|
eagerTasksAlways?: boolean;
|
|
100
|
-
/** Whether `task.batch` is enabled;
|
|
100
|
+
/** Whether `task.batch` is enabled; selects the centralized delegation guidance's call shape. */
|
|
101
101
|
taskBatch?: boolean;
|
|
102
|
+
/** Effective task concurrency limit displayed in centralized delegation guidance. Zero means unlimited. */
|
|
103
|
+
taskMaxConcurrency?: number;
|
|
104
|
+
/** Whether IRC-backed parallel coordination can be included in delegation policy. */
|
|
105
|
+
taskIrcEnabled?: boolean;
|
|
102
106
|
/** Rules with alwaysApply=true — their full content is injected into the prompt. */
|
|
103
107
|
alwaysApplyRules?: AlwaysApplyRule[];
|
|
104
108
|
/** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
|
|
@@ -107,8 +111,10 @@ export interface BuildSystemPromptOptions {
|
|
|
107
111
|
workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
|
|
108
112
|
/** Whether the local memory://root summary is active. */
|
|
109
113
|
memoryRootEnabled?: boolean;
|
|
110
|
-
/** Active model identifier (e.g. "anthropic/claude-opus-4")
|
|
114
|
+
/** Active model identifier (e.g. "anthropic/claude-opus-4") used by prompt policy and optionally surfaced. */
|
|
111
115
|
model?: string;
|
|
116
|
+
/** Whether to surface `model` in the workstation block. Model-specific prompt policy still uses it. Default: true. */
|
|
117
|
+
includeModelInPrompt?: boolean;
|
|
112
118
|
/** Personality preset rendered into the default system prompt. "none" omits the block. Default: "default" */
|
|
113
119
|
personality?: Personality;
|
|
114
120
|
/** Whether to include the workspace directory tree in the system prompt. Default: false */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.4.
|
|
4
|
+
"version": "16.4.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -56,17 +56,17 @@
|
|
|
56
56
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
57
57
|
"@babel/parser": "^7.29.7",
|
|
58
58
|
"@mozilla/readability": "^0.6.0",
|
|
59
|
-
"@oh-my-pi/hashline": "16.4.
|
|
60
|
-
"@oh-my-pi/omp-stats": "16.4.
|
|
61
|
-
"@oh-my-pi/pi-agent-core": "16.4.
|
|
62
|
-
"@oh-my-pi/pi-ai": "16.4.
|
|
63
|
-
"@oh-my-pi/pi-catalog": "16.4.
|
|
64
|
-
"@oh-my-pi/pi-mnemopi": "16.4.
|
|
65
|
-
"@oh-my-pi/pi-natives": "16.4.
|
|
66
|
-
"@oh-my-pi/pi-tui": "16.4.
|
|
67
|
-
"@oh-my-pi/pi-utils": "16.4.
|
|
68
|
-
"@oh-my-pi/pi-wire": "16.4.
|
|
69
|
-
"@oh-my-pi/snapcompact": "16.4.
|
|
59
|
+
"@oh-my-pi/hashline": "16.4.2",
|
|
60
|
+
"@oh-my-pi/omp-stats": "16.4.2",
|
|
61
|
+
"@oh-my-pi/pi-agent-core": "16.4.2",
|
|
62
|
+
"@oh-my-pi/pi-ai": "16.4.2",
|
|
63
|
+
"@oh-my-pi/pi-catalog": "16.4.2",
|
|
64
|
+
"@oh-my-pi/pi-mnemopi": "16.4.2",
|
|
65
|
+
"@oh-my-pi/pi-natives": "16.4.2",
|
|
66
|
+
"@oh-my-pi/pi-tui": "16.4.2",
|
|
67
|
+
"@oh-my-pi/pi-utils": "16.4.2",
|
|
68
|
+
"@oh-my-pi/pi-wire": "16.4.2",
|
|
69
|
+
"@oh-my-pi/snapcompact": "16.4.2",
|
|
70
70
|
"@opentelemetry/api": "^1.9.1",
|
|
71
71
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
72
72
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"@puppeteer/browsers": "^3.0.4",
|
|
77
77
|
"@types/turndown": "5.0.6",
|
|
78
78
|
"@xterm/headless": "^6.0.0",
|
|
79
|
-
"arktype": "
|
|
79
|
+
"arktype": "2.2.2",
|
|
80
80
|
"chalk": "^5.6.2",
|
|
81
81
|
"diff": "^9.0.0",
|
|
82
82
|
"fast-xml-parser": "^5.9.0",
|
package/src/mcp/manager.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
11
11
|
import type { SourceMeta } from "../capability/types";
|
|
12
12
|
import { resolveConfigValue } from "../config/resolve-config-value";
|
|
13
13
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
14
|
-
import type
|
|
14
|
+
import { type AuthStorage, REMOTE_REFRESH_SENTINEL } from "../session/auth-storage";
|
|
15
15
|
import {
|
|
16
16
|
connectToServer,
|
|
17
17
|
disconnectServer,
|
|
@@ -1226,76 +1226,79 @@ export class MCPManager {
|
|
|
1226
1226
|
const { credentialId } = lookup;
|
|
1227
1227
|
try {
|
|
1228
1228
|
let credential: MCPStoredOAuthCredential | undefined = lookup.credential;
|
|
1229
|
-
// Refresh material comes from ONE source: the credential's embedded
|
|
1230
|
-
// fields (written atomically with the tokens they minted — tokenUrl
|
|
1231
|
-
// always present) or, for legacy rows that predate embedding, the
|
|
1232
|
-
// config auth block. Never mix the two: a shared file's auth block
|
|
1233
|
-
// can belong to another profile, whose client the grant is NOT
|
|
1234
|
-
// bound to.
|
|
1235
|
-
const material = selectMcpOAuthRefreshMaterial(credential, auth);
|
|
1236
|
-
const tokenUrl = material?.tokenUrl;
|
|
1237
|
-
const clientId = material?.clientId;
|
|
1238
|
-
const clientSecret = material?.clientSecret;
|
|
1239
|
-
// `authorizationUrl` only lives on the embedded credential form;
|
|
1240
|
-
// legacy `MCPAuthConfig` rows never carried it. Required to filter
|
|
1241
|
-
// same-origin resource indicators on refresh when the authorize and
|
|
1242
|
-
// token endpoints sit on different origins (issue #3502 review
|
|
1243
|
-
// follow-up).
|
|
1244
|
-
const authorizationUrl = material && "authorizationUrl" in material ? material.authorizationUrl : undefined;
|
|
1245
|
-
const resourceIsFallback =
|
|
1246
|
-
!material?.resource && (config.type === "http" || config.type === "sse") && Boolean(config.url);
|
|
1247
|
-
const resource = material?.resource ?? (resourceIsFallback ? config.url : undefined);
|
|
1248
|
-
// Proactive refresh: 5-minute buffer before expiry
|
|
1249
|
-
// Force refresh: on 401/403 auth errors (revoked tokens, clock skew, missing expires)
|
|
1250
1229
|
const REFRESH_BUFFER_MS = 5 * 60_000;
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
)
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
tokenUrl
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
// next connect attempt surfaces a clean "needs reauth" failure and
|
|
1283
|
-
// the user can recover with `/mcp reauth <server>` (or `/mcp unauth`
|
|
1284
|
-
// to forget the server entirely).
|
|
1285
|
-
logger.warn("MCP OAuth refresh failed definitively; cleared credential", {
|
|
1286
|
-
credentialId,
|
|
1287
|
-
error: errorMsg,
|
|
1230
|
+
const refreshResult = await this.#authStorage.refreshStoredOAuthCredential<MCPStoredOAuthCredential>(
|
|
1231
|
+
credentialId,
|
|
1232
|
+
{
|
|
1233
|
+
observedCredential: credential,
|
|
1234
|
+
credentialFromRow: row => row,
|
|
1235
|
+
forceRefresh: opts?.forceRefresh,
|
|
1236
|
+
refreshSkewMs: REFRESH_BUFFER_MS,
|
|
1237
|
+
canRefresh: current => {
|
|
1238
|
+
const material = selectMcpOAuthRefreshMaterial(current, auth);
|
|
1239
|
+
return Boolean(current.refresh && material?.tokenUrl);
|
|
1240
|
+
},
|
|
1241
|
+
refresh: (current, signal) => {
|
|
1242
|
+
if (current.refresh === REMOTE_REFRESH_SENTINEL) {
|
|
1243
|
+
throw new Error("MCP OAuth refresh token is broker-redacted; local refresh is unavailable");
|
|
1244
|
+
}
|
|
1245
|
+
const material = selectMcpOAuthRefreshMaterial(current, auth);
|
|
1246
|
+
const tokenUrl = material?.tokenUrl;
|
|
1247
|
+
if (!current.refresh || !tokenUrl) {
|
|
1248
|
+
throw new Error("MCP OAuth credential is missing refresh material");
|
|
1249
|
+
}
|
|
1250
|
+
const clientId = material?.clientId;
|
|
1251
|
+
const clientSecret = material?.clientSecret;
|
|
1252
|
+
const authorizationUrl =
|
|
1253
|
+
material && "authorizationUrl" in material ? material.authorizationUrl : undefined;
|
|
1254
|
+
const resourceIsFallback =
|
|
1255
|
+
!material?.resource && (config.type === "http" || config.type === "sse") && Boolean(config.url);
|
|
1256
|
+
const resource = material?.resource ?? (resourceIsFallback ? config.url : undefined);
|
|
1257
|
+
return refreshMCPOAuthToken(tokenUrl, current.refresh, clientId, clientSecret, resource, {
|
|
1258
|
+
authorizationUrl,
|
|
1259
|
+
stripSameOriginResource: resourceIsFallback,
|
|
1260
|
+
signal,
|
|
1288
1261
|
});
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1262
|
+
},
|
|
1263
|
+
mergeRefreshedCredential: (current, refreshed) => {
|
|
1264
|
+
const material = selectMcpOAuthRefreshMaterial(current, auth);
|
|
1265
|
+
const tokenUrl = material?.tokenUrl;
|
|
1266
|
+
const clientId = material?.clientId;
|
|
1267
|
+
const clientSecret = material?.clientSecret;
|
|
1268
|
+
const authorizationUrl =
|
|
1269
|
+
material && "authorizationUrl" in material ? material.authorizationUrl : undefined;
|
|
1270
|
+
const resourceIsFallback =
|
|
1271
|
+
!material?.resource && (config.type === "http" || config.type === "sse") && Boolean(config.url);
|
|
1272
|
+
const resource = material?.resource ?? (resourceIsFallback ? config.url : undefined);
|
|
1273
|
+
return {
|
|
1274
|
+
...current,
|
|
1275
|
+
...refreshed,
|
|
1276
|
+
tokenUrl,
|
|
1277
|
+
clientId,
|
|
1278
|
+
clientSecret,
|
|
1279
|
+
resource: resourceIsFallback ? undefined : resource,
|
|
1280
|
+
authorizationUrl,
|
|
1281
|
+
};
|
|
1282
|
+
},
|
|
1283
|
+
isDefinitiveFailure: error =>
|
|
1284
|
+
isDefinitiveOAuthFailure(error instanceof Error ? error.message : String(error)),
|
|
1285
|
+
disabledCause: error =>
|
|
1286
|
+
`oauth refresh failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1287
|
+
keepCredentialOnRefreshFailure: error =>
|
|
1288
|
+
!(error instanceof Error && error.message.includes("broker-redacted")),
|
|
1289
|
+
onRefreshFailure: refreshError => {
|
|
1290
|
+
if (refreshError instanceof Error && refreshError.message.includes("broker-redacted")) return;
|
|
1292
1291
|
logger.warn("MCP OAuth refresh failed, using existing token", {
|
|
1293
1292
|
credentialId,
|
|
1294
1293
|
error: refreshError,
|
|
1295
1294
|
});
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
);
|
|
1298
|
+
if (refreshResult.removed) {
|
|
1299
|
+
logger.warn("MCP OAuth refresh failed definitively; cleared credential", { credentialId });
|
|
1298
1300
|
}
|
|
1301
|
+
credential = refreshResult.credential;
|
|
1299
1302
|
|
|
1300
1303
|
if (credential) {
|
|
1301
1304
|
if (resolved.type === "http" || resolved.type === "sse") {
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -724,6 +724,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
724
724
|
*/
|
|
725
725
|
export interface RefreshMCPOAuthTokenOptions {
|
|
726
726
|
fetch?: FetchImpl;
|
|
727
|
+
signal?: AbortSignal;
|
|
727
728
|
/**
|
|
728
729
|
* Authorization-server URL the original grant was minted against. Used to
|
|
729
730
|
* filter same-origin resource indicators on refresh. Defaults to `tokenUrl`'s
|
|
@@ -775,6 +776,7 @@ export async function refreshMCPOAuthToken(
|
|
|
775
776
|
method: "POST",
|
|
776
777
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
777
778
|
body: params.toString(),
|
|
779
|
+
signal: optsFromTrailing?.signal,
|
|
778
780
|
});
|
|
779
781
|
|
|
780
782
|
if (!response.ok) {
|
|
@@ -125,7 +125,6 @@ You NEVER open a file hoping. Hope is not a strategy.
|
|
|
125
125
|
{{#has tools "grep"}}- Use `{{toolRefs.grep}}` to locate targets.{{/has}}
|
|
126
126
|
{{#has tools "glob"}}- Use `{{toolRefs.glob}}` to map structure.{{/has}}
|
|
127
127
|
{{#has tools "read"}}- Use `{{toolRefs.read}}` with offset/limit instead of whole-file reads.{{/has}}
|
|
128
|
-
{{#has tools "task"}}- Use `{{toolRefs.task}}` to map unknown code instead of reading file after file yourself.{{/has}}
|
|
129
128
|
|
|
130
129
|
{{#has tools "lsp"}}
|
|
131
130
|
# LSP
|
|
@@ -142,9 +141,16 @@ You SHOULD use syntax-aware tools before text hacks:
|
|
|
142
141
|
- Use `grep` only for plain-text lookup when structure is irrelevant.
|
|
143
142
|
{{/ifAny}}
|
|
144
143
|
|
|
144
|
+
{{#has tools "task"}}
|
|
145
145
|
# Delegation
|
|
146
|
+
{{#if useCodexTaskPrompt}}
|
|
147
|
+
{{#if eagerTasks}}
|
|
148
|
+
Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.
|
|
149
|
+
{{else}}
|
|
150
|
+
Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work.
|
|
151
|
+
{{/if}}
|
|
152
|
+
{{else}}
|
|
146
153
|
{{#if eagerTasks}}
|
|
147
|
-
{{#has tools "task"}}
|
|
148
154
|
{{#if eagerTasksAlways}}
|
|
149
155
|
Delegation is the default here, not the exception. Once the design is settled, you MUST fan the work out to `{{toolRefs.task}}` subagents rather than doing it yourself. Work alone ONLY when one of these is unambiguously true:
|
|
150
156
|
- A single-file edit under approximately 30 lines
|
|
@@ -153,8 +159,17 @@ Delegation is the default here, not the exception. Once the design is settled, y
|
|
|
153
159
|
|
|
154
160
|
Everything else—multi-file changes, refactors, new features, tests, investigations—MUST be decomposed and delegated.{{#if taskBatch}} Batch independent slices into one parallel `{{toolRefs.task}}` call; never serialize what can run concurrently.{{/if}}{{else}}Delegation is preferred here. Once the design is settled, you SHOULD fan substantial work out to `{{toolRefs.task}}` subagents instead of doing everything yourself. Multi-file changes, refactors, new features, tests, and investigations are strong candidates. Use your judgment for small, single-file, or interactive work.{{#if taskBatch}} When you delegate independent slices, batch them into one parallel `{{toolRefs.task}}` call rather than serializing them.{{/if}}
|
|
155
161
|
{{/if}}
|
|
156
|
-
{{/has}}
|
|
157
162
|
{{/if}}
|
|
163
|
+
- Use `{{toolRefs.task}}` to map unknown code instead of reading file after file yourself.
|
|
164
|
+
- NEVER abandon phases under scope pressure—delegate, don't shrink.
|
|
165
|
+
- Default to parallel for complex changes. Delegate via `{{toolRefs.task}}` for non-importing file edits, multi-subsystem investigation, and decomposable work.
|
|
166
|
+
- **Maximize parallelism:** Break work into the widest possible {{#if taskBatch}}array of `tasks[]`{{else}}set of parallel `task` calls{{/if}}. NEVER serialize work that can run concurrently. Tasks touching different files or independent refactors should run in parallel; agents resolve their own file collisions live.
|
|
167
|
+
{{#when MAX_CONCURRENCY ">" 0}}
|
|
168
|
+
- **Concurrency cap:** At most {{pluralize MAX_CONCURRENCY "subagent" "subagents"}} run at once in this session — anything beyond that just queues, so a {{#if taskBatch}}`tasks[]` batch{{else}}set of parallel `task` calls{{/if}} larger than {{MAX_CONCURRENCY}} only delays results. Keep the fan-out at or under the cap.
|
|
169
|
+
{{/when}}
|
|
170
|
+
- **Sequence only when necessary:** The only reason to run A before B is if B strictly requires A's output to function (e.g., a core API contract or schema migration). {{#if taskIrcEnabled}}If the missing piece is small, run them in parallel and have B ask A via `irc`!{{/if}}
|
|
171
|
+
{{/if}}
|
|
172
|
+
{{/has}}
|
|
158
173
|
|
|
159
174
|
EXECUTION WORKFLOW
|
|
160
175
|
==============
|
|
@@ -170,8 +185,6 @@ EXECUTION WORKFLOW
|
|
|
170
185
|
|
|
171
186
|
# 3. Decompose
|
|
172
187
|
- Update todos as you go; skip them for trivial requests. Marking a todo done is a transition: start the next in the same turn.
|
|
173
|
-
- NEVER abandon phases under scope pressure—delegate, don't shrink.
|
|
174
|
-
{{#has tools "task"}}- Default to parallel for complex changes. Delegate via `{{toolRefs.task}}` for non-importing file edits, multi-subsystem investigation, and decomposable work.{{/has}}
|
|
175
188
|
- Plan only what makes the request work. Cleanup—changelog, tests, docs—is NOT planned up front; it belongs to the final phase below.
|
|
176
189
|
|
|
177
190
|
# 4. Implement
|
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
Execution does not block your turn: you receive agent and job IDs immediately, and the final results deliver themselves when the subagents finish.{{else}}{{#if batchEnabled}}Run subagents synchronously by passing items in a `tasks[]` batch.{{else}}Run ONE subagent synchronously per call.{{/if}}
|
|
3
3
|
Execution blocks your turn: the call only returns once the work is completely finished.{{/if}}
|
|
4
4
|
|
|
5
|
-
#
|
|
6
|
-
- **Maximize parallelism:** Break work into the widest possible {{#if batchEnabled}}array of `tasks[]`{{else}}set of parallel `task` calls{{/if}}. NEVER serialize work that can run concurrently. Tasks touching different files or independent refactors should run in parallel; agents resolve their own file collisions live.
|
|
7
|
-
{{#when MAX_CONCURRENCY ">" 0}}
|
|
8
|
-
- **Concurrency cap:** At most {{pluralize MAX_CONCURRENCY "subagent" "subagents"}} run at once in this session — anything beyond that just queues, so a {{#if batchEnabled}}`tasks[]` batch{{else}}set of parallel `task` calls{{/if}} larger than {{MAX_CONCURRENCY}} only delays results. Keep the fan-out at or under the cap.
|
|
9
|
-
{{/when}}
|
|
10
|
-
- **Sequence only when necessary:** The only reason to run A before B is if B strictly requires A's output to function (e.g., a core API contract or schema migration). {{#if ircEnabled}}If the missing piece is small, run them in parallel and have B ask A via `irc`!{{/if}}
|
|
11
|
-
{{#if ircEnabled}}- **Steering delivery:** Parent-to-subagent IRC is delivered immediately as steering; subagents blocked in `job poll` / `irc wait` do not need to poll separately for it.{{/if}}
|
|
5
|
+
# Assignment Design
|
|
12
6
|
- **Role matching:** Assign each subagent a specific `role` (e.g. "Security Reviewer", "DB Migrator"). Do not spawn generic workers.
|
|
13
7
|
- **No overhead:** Each assignment MUST instruct its agent to skip formatters, linters, and project-wide test suites. You will run those once at the end.
|
|
14
8
|
- **One-pass agents:** Prefer agents that investigate **and** edit in a single pass; only spin a read-only discovery step (e.g. `scout`) when the affected files are genuinely unknown.
|
|
@@ -37,6 +31,7 @@ Execution blocks your turn: the call only returns once the work is completely fi
|
|
|
37
31
|
|
|
38
32
|
# Context and Communication
|
|
39
33
|
Subagents start blank. They have no access to your conversation history.
|
|
34
|
+
{{#if ircEnabled}}- **Steering delivery:** Parent-to-subagent IRC is delivered immediately as steering; subagents blocked in `job poll` / `irc wait` do not need to poll separately for it.{{/if}}
|
|
40
35
|
{{#if batchEnabled}}
|
|
41
36
|
- Pass large payloads using `local://<path>` URIs, never inline text.
|
|
42
37
|
{{else}}
|
package/src/sdk.ts
CHANGED
|
@@ -191,6 +191,7 @@ import {
|
|
|
191
191
|
import { normalizeToolName, normalizeToolNames } from "./tools/builtin-names";
|
|
192
192
|
import { ToolContextStore } from "./tools/context";
|
|
193
193
|
import { getImageGenTools } from "./tools/image-gen";
|
|
194
|
+
import { isIrcEnabled } from "./tools/irc";
|
|
194
195
|
import { wrapToolWithMetaNotice } from "./tools/output-meta";
|
|
195
196
|
import { queueResolveHandler } from "./tools/resolve";
|
|
196
197
|
import { ttsTool } from "./tools/tts";
|
|
@@ -2444,11 +2445,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2444
2445
|
eagerTasks,
|
|
2445
2446
|
eagerTasksAlways,
|
|
2446
2447
|
taskBatch: settings.get("task.batch"),
|
|
2448
|
+
taskMaxConcurrency: settings.get("task.maxConcurrency"),
|
|
2449
|
+
taskIrcEnabled: isIrcEnabled(settings, options.taskDepth ?? 0),
|
|
2447
2450
|
secretsEnabled,
|
|
2448
2451
|
workspaceTree: workspaceTreePromise,
|
|
2449
2452
|
includeWorkspaceTree,
|
|
2450
2453
|
memoryRootEnabled: memoryBackend.id === "local",
|
|
2451
|
-
model:
|
|
2454
|
+
model: getActiveModelString(),
|
|
2455
|
+
includeModelInPrompt: settings.get("includeModelInPrompt"),
|
|
2452
2456
|
personality: agentKind === "sub" ? "none" : settings.get("personality"),
|
|
2453
2457
|
renderMermaid: settings.get("tui.renderMermaid"),
|
|
2454
2458
|
activeRepoContext,
|
|
@@ -286,6 +286,7 @@ import {
|
|
|
286
286
|
type SecretObfuscator,
|
|
287
287
|
} from "../secrets/obfuscator";
|
|
288
288
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
289
|
+
import { usesCodexTaskPrompt } from "../task/prompt-policy";
|
|
289
290
|
import {
|
|
290
291
|
AUTO_THINKING,
|
|
291
292
|
type ConfiguredThinkingLevel,
|
|
@@ -6123,19 +6124,17 @@ export class AgentSession {
|
|
|
6123
6124
|
return resolveEditMode(this.#getEditModeSession());
|
|
6124
6125
|
}
|
|
6125
6126
|
|
|
6126
|
-
/**
|
|
6127
|
-
* Model key (`provider/id`) currently surfaced in the system prompt, or
|
|
6128
|
-
* undefined when the model is unset or `includeModelInPrompt` is disabled.
|
|
6129
|
-
*/
|
|
6127
|
+
/** Cache key for model-dependent prompt content: displayed id or hidden-policy cohort. */
|
|
6130
6128
|
#currentPromptModelKey(): string | undefined {
|
|
6131
|
-
|
|
6132
|
-
|
|
6129
|
+
const model = this.model ? formatModelString(this.model) : undefined;
|
|
6130
|
+
if (!model || this.settings.get("includeModelInPrompt")) return model;
|
|
6131
|
+
return usesCodexTaskPrompt(model) ? "task-policy:gpt-5.6" : "task-policy:default";
|
|
6133
6132
|
}
|
|
6134
6133
|
|
|
6135
6134
|
async #syncAfterModelChange(previousEditMode: EditMode): Promise<void> {
|
|
6136
6135
|
const currentEditMode = this.#resolveActiveEditMode();
|
|
6137
6136
|
const editModeChanged = previousEditMode !== currentEditMode && this.getActiveToolNames().includes("edit");
|
|
6138
|
-
// The system prompt
|
|
6137
|
+
// The system prompt selects model-specific policy even when it does not display the model id.
|
|
6139
6138
|
const modelChanged = this.#currentPromptModelKey() !== this.#promptModelKey;
|
|
6140
6139
|
if (editModeChanged || modelChanged) {
|
|
6141
6140
|
await this.refreshBaseSystemPrompt();
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getSessionsDir,
|
|
16
16
|
isEnoent,
|
|
17
17
|
logger,
|
|
18
|
+
stringifyJson,
|
|
18
19
|
toError,
|
|
19
20
|
} from "@oh-my-pi/pi-utils";
|
|
20
21
|
import { ArtifactManager } from "./artifacts";
|
|
@@ -539,7 +540,7 @@ export class SessionManager {
|
|
|
539
540
|
}
|
|
540
541
|
|
|
541
542
|
#lineFor(entry: FileEntry): string {
|
|
542
|
-
return `${
|
|
543
|
+
return `${stringifyJson(prepareEntryForPersistence(entry, this.#blobs)) ?? "null"}\n`;
|
|
543
544
|
}
|
|
544
545
|
|
|
545
546
|
#titleSlotLine(): string {
|
package/src/system-prompt.ts
CHANGED
|
@@ -22,6 +22,8 @@ import friendlyPersonality from "./prompts/system/personalities/friendly.md" wit
|
|
|
22
22
|
import pragmaticPersonality from "./prompts/system/personalities/pragmatic.md" with { type: "text" };
|
|
23
23
|
import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
|
|
24
24
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
25
|
+
import { normalizeConcurrencyLimit } from "./task/parallel";
|
|
26
|
+
import { usesCodexTaskPrompt } from "./task/prompt-policy";
|
|
25
27
|
import { shortenPath } from "./tools/render-utils";
|
|
26
28
|
import { type ActiveRepoContext, resolveActiveRepoContext } from "./utils/active-repo-context";
|
|
27
29
|
import { formatLocalCalendarDate } from "./utils/local-date";
|
|
@@ -481,8 +483,12 @@ export interface BuildSystemPromptOptions {
|
|
|
481
483
|
eagerTasks?: boolean;
|
|
482
484
|
/** When true, the Eager Tasks section uses the hard MUST/ONLY wording (`task.eager: always`) rather than the softer `preferred` nudge. */
|
|
483
485
|
eagerTasksAlways?: boolean;
|
|
484
|
-
/** Whether `task.batch` is enabled;
|
|
486
|
+
/** Whether `task.batch` is enabled; selects the centralized delegation guidance's call shape. */
|
|
485
487
|
taskBatch?: boolean;
|
|
488
|
+
/** Effective task concurrency limit displayed in centralized delegation guidance. Zero means unlimited. */
|
|
489
|
+
taskMaxConcurrency?: number;
|
|
490
|
+
/** Whether IRC-backed parallel coordination can be included in delegation policy. */
|
|
491
|
+
taskIrcEnabled?: boolean;
|
|
486
492
|
/** Rules with alwaysApply=true — their full content is injected into the prompt. */
|
|
487
493
|
alwaysApplyRules?: AlwaysApplyRule[];
|
|
488
494
|
/** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
|
|
@@ -491,8 +497,10 @@ export interface BuildSystemPromptOptions {
|
|
|
491
497
|
workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
|
|
492
498
|
/** Whether the local memory://root summary is active. */
|
|
493
499
|
memoryRootEnabled?: boolean;
|
|
494
|
-
/** Active model identifier (e.g. "anthropic/claude-opus-4")
|
|
500
|
+
/** Active model identifier (e.g. "anthropic/claude-opus-4") used by prompt policy and optionally surfaced. */
|
|
495
501
|
model?: string;
|
|
502
|
+
/** Whether to surface `model` in the workstation block. Model-specific prompt policy still uses it. Default: true. */
|
|
503
|
+
includeModelInPrompt?: boolean;
|
|
496
504
|
/** Personality preset rendered into the default system prompt. "none" omits the block. Default: "default" */
|
|
497
505
|
personality?: Personality;
|
|
498
506
|
/** Whether to include the workspace directory tree in the system prompt. Default: false */
|
|
@@ -536,10 +544,13 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
536
544
|
eagerTasks = false,
|
|
537
545
|
eagerTasksAlways = false,
|
|
538
546
|
taskBatch = true,
|
|
547
|
+
taskMaxConcurrency = 0,
|
|
548
|
+
taskIrcEnabled = false,
|
|
539
549
|
secretsEnabled = false,
|
|
540
550
|
workspaceTree: providedWorkspaceTree,
|
|
541
551
|
memoryRootEnabled = false,
|
|
542
552
|
model,
|
|
553
|
+
includeModelInPrompt = true,
|
|
543
554
|
personality = "default",
|
|
544
555
|
includeWorkspaceTree = false,
|
|
545
556
|
renderMermaid = true,
|
|
@@ -770,7 +781,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
770
781
|
date,
|
|
771
782
|
dateTime,
|
|
772
783
|
cwd: promptCwd,
|
|
773
|
-
model: model ?? "",
|
|
784
|
+
model: includeModelInPrompt ? (model ?? "") : "",
|
|
785
|
+
useCodexTaskPrompt: usesCodexTaskPrompt(model),
|
|
774
786
|
personality: personality === "none" ? "" : PERSONALITY_SPECS[personality].trim(),
|
|
775
787
|
intentTracing: !!intentField,
|
|
776
788
|
intentField: intentField ?? "",
|
|
@@ -780,6 +792,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
780
792
|
eagerTasks,
|
|
781
793
|
eagerTasksAlways,
|
|
782
794
|
taskBatch,
|
|
795
|
+
MAX_CONCURRENCY: normalizeConcurrencyLimit(taskMaxConcurrency),
|
|
796
|
+
taskIrcEnabled,
|
|
783
797
|
secretsEnabled,
|
|
784
798
|
hasMemoryRoot: memoryRootEnabled,
|
|
785
799
|
hasObsidian: hasObsidian(),
|
package/src/task/index.ts
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
} from "./isolation-runner";
|
|
61
61
|
import { generateTaskName } from "./name-generator";
|
|
62
62
|
import { AgentOutputManager } from "./output-manager";
|
|
63
|
-
import { mapWithConcurrencyLimit,
|
|
63
|
+
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
64
64
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
65
65
|
import { repairTaskParams } from "./repair-args";
|
|
66
66
|
import { parseIsolationMode } from "./worktree";
|
|
@@ -180,7 +180,6 @@ export function formatResultOutputFallback(result: Pick<SingleResult, "output" |
|
|
|
180
180
|
*/
|
|
181
181
|
function renderDescription(
|
|
182
182
|
agents: AgentDefinition[],
|
|
183
|
-
maxConcurrency: number,
|
|
184
183
|
isolationEnabled: boolean,
|
|
185
184
|
disabledAgents: string[],
|
|
186
185
|
batchEnabled: boolean,
|
|
@@ -208,7 +207,6 @@ function renderDescription(
|
|
|
208
207
|
defaultAgent: spawnPolicy.defaultAgent,
|
|
209
208
|
defaultAgentIsGeneric: spawnPolicy.defaultAgent === DEFAULT_SPAWN_AGENT,
|
|
210
209
|
allowedAgentsText: spawnPolicy.allowedPromptText,
|
|
211
|
-
MAX_CONCURRENCY: normalizeConcurrencyLimit(maxConcurrency),
|
|
212
210
|
isolationEnabled,
|
|
213
211
|
batchEnabled,
|
|
214
212
|
asyncEnabled,
|
|
@@ -521,11 +519,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
521
519
|
/** Dynamic description that reflects current disabled-agent settings */
|
|
522
520
|
get description(): string {
|
|
523
521
|
const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
|
|
524
|
-
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
|
|
525
522
|
const isolationMode = this.session.settings.get("task.isolation.mode");
|
|
526
523
|
return renderDescription(
|
|
527
524
|
this.#discoveredAgents,
|
|
528
|
-
maxConcurrency,
|
|
529
525
|
isolationMode !== "none",
|
|
530
526
|
disabledAgents,
|
|
531
527
|
this.#isBatchEnabled(),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { bareModelId, parseOpenAIModel, semverEqual } from "@oh-my-pi/pi-catalog/identity";
|
|
2
|
+
|
|
3
|
+
/** Whether task guidance should follow Codex's GPT-5.6-specific delegation policy. */
|
|
4
|
+
export function usesCodexTaskPrompt(modelId: string | undefined): boolean {
|
|
5
|
+
if (!modelId) return false;
|
|
6
|
+
const parsed = parseOpenAIModel(bareModelId(modelId));
|
|
7
|
+
return parsed !== null && semverEqual(parsed.version, "5.6");
|
|
8
|
+
}
|