@signalridge/pi-subagents 1.10.3 → 1.11.1
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 +53 -0
- package/README.md +34 -11
- package/examples/agent-tool-description.md +1 -1
- package/package.json +9 -8
- package/src/agent-manager.ts +1405 -486
- package/src/agent-runner.ts +538 -268
- package/src/agent-types.ts +6 -0
- package/src/ask-tools.ts +31 -21
- package/src/context-boundary.ts +54 -0
- package/src/context.ts +121 -36
- package/src/custom-agents.ts +19 -4
- package/src/enabled-models.ts +84 -83
- package/src/gate.ts +0 -0
- package/src/index.ts +1118 -861
- package/src/mention-clone.ts +129 -83
- package/src/model-runtime-bridge.ts +68 -0
- package/src/model-scope.ts +8 -3
- package/src/nested-tools.ts +13 -5
- package/src/project-trust.ts +146 -0
- package/src/settings.ts +7 -4
- package/src/skill-loader.ts +22 -10
- package/src/types.ts +6 -4
package/src/index.ts
CHANGED
|
@@ -119,6 +119,7 @@ import {
|
|
|
119
119
|
setFallbackSubagent,
|
|
120
120
|
} from "./agent-types.js";
|
|
121
121
|
import { inChildSessionContext } from "./child-context.js";
|
|
122
|
+
import { INHERIT_CONTEXT_UNAVAILABLE } from "./context-boundary.js";
|
|
122
123
|
import {
|
|
123
124
|
PROTOCOL_CAPABILITIES,
|
|
124
125
|
PROTOCOL_VERSION,
|
|
@@ -143,8 +144,13 @@ import {
|
|
|
143
144
|
resolveHandleToType,
|
|
144
145
|
stripAgentPrefix,
|
|
145
146
|
} from "./mention.js";
|
|
146
|
-
import { runMentionClone } from "./mention-clone.js";
|
|
147
|
-
import {
|
|
147
|
+
import { MENTION_SPAWNED, runMentionClone } from "./mention-clone.js";
|
|
148
|
+
import {
|
|
149
|
+
type ModelRegistry,
|
|
150
|
+
resolveModel,
|
|
151
|
+
shortModelLabel,
|
|
152
|
+
} from "./model-resolver.js";
|
|
153
|
+
import { parentModelSessionOptions } from "./model-runtime-bridge.js";
|
|
148
154
|
import {
|
|
149
155
|
checkModelScope,
|
|
150
156
|
isScopeModelsEnabled,
|
|
@@ -159,6 +165,7 @@ import {
|
|
|
159
165
|
streamToOutputFile,
|
|
160
166
|
writeInitialEntry,
|
|
161
167
|
} from "./output-file.js";
|
|
168
|
+
import { captureProjectTrust, isProjectResource } from "./project-trust.js";
|
|
162
169
|
import { SubagentScheduler } from "./schedule.js";
|
|
163
170
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
164
171
|
import {
|
|
@@ -227,7 +234,10 @@ import {
|
|
|
227
234
|
getSessionContextPercent,
|
|
228
235
|
type LifetimeUsage,
|
|
229
236
|
} from "./usage.js";
|
|
230
|
-
import {
|
|
237
|
+
import {
|
|
238
|
+
isWorktreeIsolationEnabled,
|
|
239
|
+
setWorktreeIsolationEnabled,
|
|
240
|
+
} from "./worktree.js";
|
|
231
241
|
|
|
232
242
|
// ---- Shared helpers ----
|
|
233
243
|
|
|
@@ -573,16 +583,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
573
583
|
if (inert) return;
|
|
574
584
|
if (!rootRuntime) {
|
|
575
585
|
try {
|
|
576
|
-
const
|
|
586
|
+
const trust = captureProjectTrust(ctx);
|
|
587
|
+
const startupSettings = loadSettings(trust.cwd, trust.trusted);
|
|
577
588
|
const startupAgents = loadCustomAgents(
|
|
578
|
-
|
|
589
|
+
trust.cwd,
|
|
579
590
|
startupSettings.strictAgentFiles === true,
|
|
591
|
+
trust.trusted,
|
|
580
592
|
);
|
|
581
593
|
rootRuntime = activateRootRuntime(
|
|
582
594
|
pi,
|
|
583
595
|
startupSettings,
|
|
584
596
|
startupAgents,
|
|
585
|
-
ctx
|
|
597
|
+
ctx,
|
|
586
598
|
unsubscribeRootContext,
|
|
587
599
|
);
|
|
588
600
|
} catch (error: unknown) {
|
|
@@ -602,10 +614,12 @@ function activateRootRuntime(
|
|
|
602
614
|
pi: ExtensionAPI,
|
|
603
615
|
startupSettings: SubagentsSettings,
|
|
604
616
|
startupAgents: Map<string, AgentConfig>,
|
|
605
|
-
|
|
617
|
+
initialContext: ExtensionContext,
|
|
606
618
|
unsubscribeRootContext: () => void,
|
|
607
619
|
): RootRuntime {
|
|
608
|
-
let
|
|
620
|
+
let configurationOwner = initialContext;
|
|
621
|
+
let activeTrust = captureProjectTrust(configurationOwner);
|
|
622
|
+
let sessionCwd = activeTrust.cwd;
|
|
609
623
|
let strictAgentFiles = startupSettings.strictAgentFiles === true;
|
|
610
624
|
|
|
611
625
|
// ---- Register custom notification renderer ----
|
|
@@ -659,7 +673,12 @@ function activateRootRuntime(
|
|
|
659
673
|
|
|
660
674
|
/** Reload agents from project/global custom agent dirs and merge with defaults (called on init and each Agent invocation). */
|
|
661
675
|
const reloadCustomAgents = (strict = false) => {
|
|
662
|
-
const
|
|
676
|
+
const trust = captureProjectTrust(configurationOwner);
|
|
677
|
+
const changed = trust.cwd !== activeTrust.cwd || trust.trusted !== activeTrust.trusted;
|
|
678
|
+
activeTrust = trust;
|
|
679
|
+
sessionCwd = trust.cwd;
|
|
680
|
+
if (changed) applyRuntimeSettings(loadSettings(trust.cwd, trust.trusted));
|
|
681
|
+
const userAgents = loadCustomAgents(trust.cwd, strict, trust.trusted);
|
|
663
682
|
registerAgents(userAgents);
|
|
664
683
|
};
|
|
665
684
|
|
|
@@ -804,8 +823,12 @@ function activateRootRuntime(
|
|
|
804
823
|
durationMs,
|
|
805
824
|
tokens,
|
|
806
825
|
...(record.outputFile ? { outputFile: record.outputFile } : {}),
|
|
807
|
-
...(record.invocation?.agentTier === undefined
|
|
808
|
-
|
|
826
|
+
...(record.invocation?.agentTier === undefined
|
|
827
|
+
? {}
|
|
828
|
+
: { tier: record.invocation.agentTier }),
|
|
829
|
+
...(record.invocation?.agentTierSnapshot
|
|
830
|
+
? { tierSnapshot: record.invocation.agentTierSnapshot }
|
|
831
|
+
: {}),
|
|
809
832
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
810
833
|
};
|
|
811
834
|
}
|
|
@@ -843,7 +866,9 @@ function activateRootRuntime(
|
|
|
843
866
|
? { tokens: { total: terminal.tokenCount } }
|
|
844
867
|
: {}),
|
|
845
868
|
...(tombstone.tier === undefined ? {} : { tier: tombstone.tier }),
|
|
846
|
-
...(tombstone.tierSnapshot
|
|
869
|
+
...(tombstone.tierSnapshot
|
|
870
|
+
? { tierSnapshot: tombstone.tierSnapshot }
|
|
871
|
+
: {}),
|
|
847
872
|
owner: snapshotOwner(tombstone.owner),
|
|
848
873
|
};
|
|
849
874
|
}
|
|
@@ -868,7 +893,9 @@ function activateRootRuntime(
|
|
|
868
893
|
...(terminal.outputFile ? { outputFile: terminal.outputFile } : {}),
|
|
869
894
|
...(terminal.tokenCount ? { tokens: terminal.tokenCount } : {}),
|
|
870
895
|
...(tombstone.tier === undefined ? {} : { tier: tombstone.tier }),
|
|
871
|
-
...(tombstone.tierSnapshot
|
|
896
|
+
...(tombstone.tierSnapshot
|
|
897
|
+
? { tierSnapshot: tombstone.tierSnapshot }
|
|
898
|
+
: {}),
|
|
872
899
|
owner: snapshotOwner(tombstone.owner),
|
|
873
900
|
});
|
|
874
901
|
}
|
|
@@ -934,8 +961,12 @@ function activateRootRuntime(
|
|
|
934
961
|
...(record.lifetimeUsage.input + record.lifetimeUsage.output > 0
|
|
935
962
|
? { tokens: record.lifetimeUsage.input + record.lifetimeUsage.output }
|
|
936
963
|
: {}),
|
|
937
|
-
...(record.invocation?.agentTier === undefined
|
|
938
|
-
|
|
964
|
+
...(record.invocation?.agentTier === undefined
|
|
965
|
+
? {}
|
|
966
|
+
: { tier: record.invocation.agentTier }),
|
|
967
|
+
...(record.invocation?.agentTierSnapshot
|
|
968
|
+
? { tierSnapshot: record.invocation.agentTierSnapshot }
|
|
969
|
+
: {}),
|
|
939
970
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
940
971
|
});
|
|
941
972
|
|
|
@@ -980,7 +1011,9 @@ function activateRootRuntime(
|
|
|
980
1011
|
id: record.id,
|
|
981
1012
|
type: record.type,
|
|
982
1013
|
description: record.description,
|
|
983
|
-
...(record.invocation?.agentTier === undefined
|
|
1014
|
+
...(record.invocation?.agentTier === undefined
|
|
1015
|
+
? {}
|
|
1016
|
+
: { tier: record.invocation.agentTier }),
|
|
984
1017
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
985
1018
|
});
|
|
986
1019
|
},
|
|
@@ -994,8 +1027,12 @@ function activateRootRuntime(
|
|
|
994
1027
|
reason: info.reason,
|
|
995
1028
|
tokensBefore: info.tokensBefore,
|
|
996
1029
|
compactionCount: record.compactionCount,
|
|
997
|
-
...(record.invocation?.agentTier === undefined
|
|
998
|
-
|
|
1030
|
+
...(record.invocation?.agentTier === undefined
|
|
1031
|
+
? {}
|
|
1032
|
+
: { tier: record.invocation.agentTier }),
|
|
1033
|
+
...(record.invocation?.agentTierSnapshot
|
|
1034
|
+
? { tierSnapshot: record.invocation.agentTierSnapshot }
|
|
1035
|
+
: {}),
|
|
999
1036
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
1000
1037
|
});
|
|
1001
1038
|
},
|
|
@@ -1009,7 +1046,9 @@ function activateRootRuntime(
|
|
|
1009
1046
|
type: record.type,
|
|
1010
1047
|
description: record.description,
|
|
1011
1048
|
isBackground: record.isBackground,
|
|
1012
|
-
...(record.invocation?.agentTier === undefined
|
|
1049
|
+
...(record.invocation?.agentTier === undefined
|
|
1050
|
+
? {}
|
|
1051
|
+
: { tier: record.invocation.agentTier }),
|
|
1013
1052
|
owner: snapshotOwner(record.owner),
|
|
1014
1053
|
});
|
|
1015
1054
|
},
|
|
@@ -1067,8 +1106,7 @@ function activateRootRuntime(
|
|
|
1067
1106
|
const globalRegistry = globalThis as any;
|
|
1068
1107
|
const predecessor =
|
|
1069
1108
|
(globalRegistry[MANAGER_TAKEOVER_LOCK_KEY] as
|
|
1070
|
-
|
|
1071
|
-
| undefined) ?? Promise.resolve();
|
|
1109
|
+
Promise<void> | undefined) ?? Promise.resolve();
|
|
1072
1110
|
let release!: () => void;
|
|
1073
1111
|
const gate = new Promise<void>((resolve) => {
|
|
1074
1112
|
release = resolve;
|
|
@@ -1152,6 +1190,33 @@ function activateRootRuntime(
|
|
|
1152
1190
|
|
|
1153
1191
|
// --- Cross-extension RPC via pi.events ---
|
|
1154
1192
|
let currentCtx: ExtensionContext | undefined;
|
|
1193
|
+
// A session ID alone cannot distinguish a branch change or a resume of the
|
|
1194
|
+
// same session. Invalidate pending off-screen mention turns only when the
|
|
1195
|
+
// replacement commits: before-switch/tree handlers can still cancel it.
|
|
1196
|
+
let sessionGeneration = 0;
|
|
1197
|
+
// Public synchronous bus preflight works in either extension load order. A
|
|
1198
|
+
// filtered-out activation has no bound context and must not veto navigation.
|
|
1199
|
+
pi.events.on("pi:navigation-preflight", (raw: unknown) => {
|
|
1200
|
+
if (!currentCtx || !manager.hasUnsettledWork()) return;
|
|
1201
|
+
const report = (raw as { report?: unknown } | null)?.report;
|
|
1202
|
+
if (typeof report === "function") report("subagents");
|
|
1203
|
+
});
|
|
1204
|
+
const navigationPreflight = (
|
|
1205
|
+
ctx: ExtensionContext,
|
|
1206
|
+
): { cancel: true } | undefined => {
|
|
1207
|
+
let busy = false;
|
|
1208
|
+
pi.events.emit("pi:navigation-preflight", {
|
|
1209
|
+
report: () => {
|
|
1210
|
+
busy = true;
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
if (!busy) return undefined;
|
|
1214
|
+
const notice =
|
|
1215
|
+
"Navigation blocked while agents or workflows are active or cleaning up. Wait for them to finish, or stop them explicitly and retry.";
|
|
1216
|
+
if (ctx.hasUI) ctx.ui.notify(notice, "warning");
|
|
1217
|
+
else console.warn(`[pi-subagents] ${notice}`);
|
|
1218
|
+
return { cancel: true };
|
|
1219
|
+
};
|
|
1155
1220
|
// RPC handlers + the `subagents:ready` broadcast are wired on `session_start`
|
|
1156
1221
|
// (a bound lifecycle event), not at factory time. pi runs every extension
|
|
1157
1222
|
// factory before the `extensions:` filter and only fires lifecycle events for
|
|
@@ -1192,7 +1257,23 @@ function activateRootRuntime(
|
|
|
1192
1257
|
event: { reason?: string },
|
|
1193
1258
|
ctx: ExtensionContext,
|
|
1194
1259
|
): Promise<void> {
|
|
1195
|
-
|
|
1260
|
+
// A successful session_start is the commit point for a reused runtime. A
|
|
1261
|
+
// cancelled session_before_switch leaves the old context and scheduler live.
|
|
1262
|
+
if (currentCtx) {
|
|
1263
|
+
scheduler.stop();
|
|
1264
|
+
manager.detachForBranchChange();
|
|
1265
|
+
manager.resetManagedSpawns();
|
|
1266
|
+
}
|
|
1267
|
+
sessionGeneration++;
|
|
1268
|
+
const replacingConfiguration = currentCtx !== undefined;
|
|
1269
|
+
configurationOwner = ctx;
|
|
1270
|
+
const trust = captureProjectTrust(ctx);
|
|
1271
|
+
sessionCwd = trust.cwd;
|
|
1272
|
+
if (replacingConfiguration) {
|
|
1273
|
+
activeTrust = trust;
|
|
1274
|
+
applyRuntimeSettings(loadSettings(trust.cwd, trust.trusted));
|
|
1275
|
+
}
|
|
1276
|
+
reloadCustomAgents();
|
|
1196
1277
|
boundSessionId = ctx.sessionManager?.getSessionId?.();
|
|
1197
1278
|
currentCtx = ctx;
|
|
1198
1279
|
await claimRpcRegistry(ctx, event.reason === "reload");
|
|
@@ -1233,7 +1314,8 @@ function activateRootRuntime(
|
|
|
1233
1314
|
abortOwned: (id, owner) => manager.abortOwned(id, owner),
|
|
1234
1315
|
quiesceOwned: (runId, agentIds, timeoutMs, owners) =>
|
|
1235
1316
|
manager.quiesceOwned(runId, agentIds, timeoutMs, owners),
|
|
1236
|
-
reconcileManaged: (spawnKey, owner) =>
|
|
1317
|
+
reconcileManaged: (spawnKey, owner) =>
|
|
1318
|
+
manager.reconcileManaged(spawnKey, owner),
|
|
1237
1319
|
// The live pool size, not a snapshot: `/subagents` can change it
|
|
1238
1320
|
// mid-session, and a peer that sizes its fan-out from this must see
|
|
1239
1321
|
// the value that is actually throttling it now.
|
|
@@ -1297,38 +1379,34 @@ function activateRootRuntime(
|
|
|
1297
1379
|
startScheduler(ctx);
|
|
1298
1380
|
}
|
|
1299
1381
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
//
|
|
1312
|
-
// records
|
|
1313
|
-
// the
|
|
1314
|
-
pi.on("
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
pi.events.emit("subagents:session_before_tree", {});
|
|
1318
|
-
const quiesced = await manager.quiesceAll(5_000);
|
|
1319
|
-
if (!quiesced.settled) {
|
|
1320
|
-
// waitForTerminalRecords quarantines timed-out records, and this second
|
|
1321
|
-
// guard covers handler-order races where tree preparation is delivered
|
|
1322
|
-
// after an abort but before the provider promise has settled.
|
|
1323
|
-
manager.detachForBranchChange();
|
|
1324
|
-
// The manager retains pending records and recovery state; avoid writing
|
|
1325
|
-
// directly to the teardown event loop.
|
|
1326
|
-
}
|
|
1382
|
+
// Both before-events are cancellable. A later handler may veto, and tree
|
|
1383
|
+
// summarization can still abort after every handler has returned. Never stop
|
|
1384
|
+
// live work on an attempted navigation.
|
|
1385
|
+
pi.on("session_before_switch", (_event, ctx) => navigationPreflight(ctx));
|
|
1386
|
+
pi.on("session_before_tree", (_event, ctx) => navigationPreflight(ctx));
|
|
1387
|
+
let detachedTreeEvent: unknown;
|
|
1388
|
+
const detachCommittedTree = (event: unknown): void => {
|
|
1389
|
+
if (!currentCtx || detachedTreeEvent === event) return;
|
|
1390
|
+
detachedTreeEvent = event;
|
|
1391
|
+
manager.detachForBranchChange();
|
|
1392
|
+
};
|
|
1393
|
+
// If workflows' committed-tree handler runs first, it must detach our old
|
|
1394
|
+
// records before aborting its controller: abort listeners can synchronously
|
|
1395
|
+
// issue stop-owned RPC and otherwise append a managed tombstone on the new leaf.
|
|
1396
|
+
pi.events.on("pi-workflows:session_tree_committed", (raw: unknown) => {
|
|
1397
|
+
const event = (raw as { event?: unknown } | null)?.event;
|
|
1398
|
+
if (event) detachCommittedTree(event);
|
|
1327
1399
|
});
|
|
1328
|
-
pi.on("session_tree", (
|
|
1400
|
+
pi.on("session_tree", (event, ctx) => {
|
|
1401
|
+
sessionGeneration++;
|
|
1402
|
+
detachCommittedTree(event);
|
|
1403
|
+
// Pi has already changed the leaf. Fence workflow listeners synchronously
|
|
1404
|
+
// even when subagents' session_tree handler runs first.
|
|
1405
|
+
pi.events.emit("subagents:session_tree_committed", { event });
|
|
1329
1406
|
currentCtx = ctx;
|
|
1330
|
-
|
|
1331
|
-
|
|
1407
|
+
configurationOwner = ctx;
|
|
1408
|
+
reloadCustomAgents();
|
|
1409
|
+
scheduler.stop();
|
|
1332
1410
|
manager.clearCompleted(true);
|
|
1333
1411
|
manager.resetManagedSpawns();
|
|
1334
1412
|
const entries =
|
|
@@ -1350,6 +1428,52 @@ function activateRootRuntime(
|
|
|
1350
1428
|
let runtimeShutdown: Promise<void> | undefined;
|
|
1351
1429
|
const shutdownRuntime = (): Promise<void> => {
|
|
1352
1430
|
runtimeShutdown ??= (async () => {
|
|
1431
|
+
// session_shutdown is confirmed, unlike either before-event. Start the
|
|
1432
|
+
// workflow's owned-agent cleanup while its RPC responder and the old leaf
|
|
1433
|
+
// are still available, regardless of which extension loads first.
|
|
1434
|
+
const pending: Array<
|
|
1435
|
+
Promise<{ settled: boolean; pending: string[]; diagnostic?: string }>
|
|
1436
|
+
> = [];
|
|
1437
|
+
pi.events.emit("pi-workflows:shutdown-quiesce", {
|
|
1438
|
+
respond: (
|
|
1439
|
+
operation: Promise<{
|
|
1440
|
+
settled: boolean;
|
|
1441
|
+
pending: string[];
|
|
1442
|
+
diagnostic?: string;
|
|
1443
|
+
}>,
|
|
1444
|
+
) => {
|
|
1445
|
+
pending.push(operation);
|
|
1446
|
+
},
|
|
1447
|
+
});
|
|
1448
|
+
scheduler.stop();
|
|
1449
|
+
currentCtx = undefined; // reject new spawns; owned cleanup RPC remains bound
|
|
1450
|
+
if (pending.length > 0) {
|
|
1451
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1452
|
+
const results = await Promise.race([
|
|
1453
|
+
Promise.allSettled(pending),
|
|
1454
|
+
new Promise<undefined>((resolve) => {
|
|
1455
|
+
timer = setTimeout(() => resolve(undefined), 6_000);
|
|
1456
|
+
}),
|
|
1457
|
+
]);
|
|
1458
|
+
if (timer) clearTimeout(timer);
|
|
1459
|
+
if (
|
|
1460
|
+
!results ||
|
|
1461
|
+
results.some(
|
|
1462
|
+
(result) => result.status === "rejected" || !result.value.settled,
|
|
1463
|
+
)
|
|
1464
|
+
) {
|
|
1465
|
+
console.warn(
|
|
1466
|
+
"[pi-subagents] workflow shutdown quiescence timed out or failed; pending work will be quarantined",
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
const quiesced = await manager.quiesceAll(5_000);
|
|
1471
|
+
if (!quiesced.settled) {
|
|
1472
|
+
console.warn(
|
|
1473
|
+
`[pi-subagents] shutdown quiescence timed out; quarantined: ${quiesced.pending.join(", ")}`,
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
sessionGeneration++;
|
|
1353
1477
|
unsubscribeRootContext();
|
|
1354
1478
|
rpcHandle?.unsubSpawn();
|
|
1355
1479
|
rpcHandle?.unsubSpawnManaged();
|
|
@@ -1474,7 +1598,11 @@ function activateRootRuntime(
|
|
|
1474
1598
|
const effectiveTier = (() => {
|
|
1475
1599
|
try {
|
|
1476
1600
|
return selectAgentTier(
|
|
1477
|
-
{
|
|
1601
|
+
{
|
|
1602
|
+
requestedTier: request.tier,
|
|
1603
|
+
requireTier: true,
|
|
1604
|
+
agentConfig: customConfig,
|
|
1605
|
+
},
|
|
1478
1606
|
agentTiers,
|
|
1479
1607
|
)?.tier;
|
|
1480
1608
|
} catch {
|
|
@@ -1496,7 +1624,12 @@ function activateRootRuntime(
|
|
|
1496
1624
|
resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
|
|
1497
1625
|
);
|
|
1498
1626
|
const effectiveIsolation = request.isolation ?? resolvedConfig.isolation;
|
|
1499
|
-
const effectiveExcludeTools = [
|
|
1627
|
+
const effectiveExcludeTools = [
|
|
1628
|
+
...new Set([
|
|
1629
|
+
...(customConfig?.disallowedTools ?? []),
|
|
1630
|
+
...(request.excludeTools ?? []),
|
|
1631
|
+
]),
|
|
1632
|
+
];
|
|
1500
1633
|
const managedPolicy: ManagedSpawnPolicy = {
|
|
1501
1634
|
maxTurns: effectiveMaxTurns,
|
|
1502
1635
|
isolated: resolvedConfig.isolated,
|
|
@@ -1747,8 +1880,8 @@ function activateRootRuntime(
|
|
|
1747
1880
|
|
|
1748
1881
|
// ---- Agent tool description mode ----
|
|
1749
1882
|
// "full" (default) keeps the rich Claude Code-style description; "compact"
|
|
1750
|
-
// swaps in a ~75% smaller one for small/local models (#91).
|
|
1751
|
-
//
|
|
1883
|
+
// swaps in a ~75% smaller one for small/local models (#91). The description
|
|
1884
|
+
// getter rechecks configuration authority so replacements cannot retain denied text.
|
|
1752
1885
|
let toolDescriptionMode: ToolDescriptionMode = "full";
|
|
1753
1886
|
function getToolDescriptionMode(): ToolDescriptionMode {
|
|
1754
1887
|
return toolDescriptionMode;
|
|
@@ -1848,40 +1981,47 @@ function activateRootRuntime(
|
|
|
1848
1981
|
|
|
1849
1982
|
// Apply the already-validated session settings and emit the same lifecycle
|
|
1850
1983
|
// event that the normal startup path exposes. Do not re-read process.cwd().
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1984
|
+
const baselineSettings: SubagentsSettings = { ...snapshotSettings(), strictAgentFiles: false, agentTiers: {} };
|
|
1985
|
+
function applyRuntimeSettings(settings: SubagentsSettings): void {
|
|
1986
|
+
// Clear optional policies as well: absence must not retain the old project's values.
|
|
1987
|
+
setDefaultModel(settings.defaultModel ?? baselineSettings.defaultModel);
|
|
1988
|
+
setFallbackSubagent(settings.fallbackSubagent ?? baselineSettings.fallbackSubagent);
|
|
1989
|
+
applySettings({ ...baselineSettings, ...settings }, {
|
|
1990
|
+
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
1991
|
+
setDefaultMaxTurns,
|
|
1992
|
+
setGraceTurns,
|
|
1993
|
+
setDefaultMaxTokens,
|
|
1994
|
+
setDefaultMaxToolCalls,
|
|
1995
|
+
setDefaultToolTimeout: (ms) => setDefaultToolTimeoutMs(ms),
|
|
1996
|
+
setDefaultJoinMode,
|
|
1997
|
+
setDefaultModel,
|
|
1998
|
+
setSchedulingEnabled,
|
|
1999
|
+
setScopeModels: setScopeModelsEnabled,
|
|
2000
|
+
setStrictAgentFiles: (enabled) => {
|
|
2001
|
+
strictAgentFiles = enabled;
|
|
2002
|
+
},
|
|
2003
|
+
setDisableDefaultAgents,
|
|
2004
|
+
setToolDescriptionMode,
|
|
2005
|
+
setFleetView: setFleetViewEnabled,
|
|
2006
|
+
setOutputTranscript: setOutputTranscriptDefault,
|
|
2007
|
+
setRememberAgents: (enabled) => manager.setRememberAgents(enabled),
|
|
2008
|
+
setAgentMentions: (enabled) => {
|
|
2009
|
+
agentMentionMode = enabled;
|
|
2010
|
+
},
|
|
2011
|
+
setSupervisorQuestions: (enabled) => {
|
|
2012
|
+
supervisorQuestionsEnabled = enabled;
|
|
2013
|
+
manager.setSupervisorQuestions(enabled);
|
|
2014
|
+
},
|
|
2015
|
+
setMaxSubagentDepth,
|
|
2016
|
+
setMaxSubagentSpawnsPerBranch: (n) =>
|
|
2017
|
+
manager.setMaxSubagentSpawnsPerBranch(n),
|
|
2018
|
+
setFallbackSubagent,
|
|
2019
|
+
setWorktreeIsolation: setWorktreeIsolationEnabled,
|
|
2020
|
+
setAgentTiers: setAgentTiersSettings,
|
|
2021
|
+
});
|
|
2022
|
+
pi.events.emit("subagents:settings_loaded", { settings });
|
|
2023
|
+
}
|
|
2024
|
+
applyRuntimeSettings(startupSettings);
|
|
1885
2025
|
|
|
1886
2026
|
// A tier that nothing defines is a typo, and the resolver would only reach it
|
|
1887
2027
|
// on the first spawn that needs it — possibly minutes in, mid-task. Report it
|
|
@@ -1963,18 +2103,25 @@ function activateRootRuntime(
|
|
|
1963
2103
|
}
|
|
1964
2104
|
|
|
1965
2105
|
if (record.session) {
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2106
|
+
try {
|
|
2107
|
+
const resumed = await manager.resume(
|
|
2108
|
+
record.id,
|
|
2109
|
+
mention.message,
|
|
2110
|
+
undefined,
|
|
2111
|
+
{ isBackground: true },
|
|
2112
|
+
);
|
|
2113
|
+
ctx.ui.notify(
|
|
2114
|
+
resumed
|
|
2115
|
+
? `Resuming ${target}`
|
|
2116
|
+
: `Could not resume ${target} — it is still running.`,
|
|
2117
|
+
resumed ? "info" : "warning",
|
|
2118
|
+
);
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
ctx.ui.notify(
|
|
2121
|
+
`Could not resume ${target}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2122
|
+
"warning",
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
1978
2125
|
return { action: "handled" };
|
|
1979
2126
|
}
|
|
1980
2127
|
// A live record with no session never got far enough to continue, so it
|
|
@@ -2053,37 +2200,82 @@ function activateRootRuntime(
|
|
|
2053
2200
|
if (!type) return { action: "continue" };
|
|
2054
2201
|
|
|
2055
2202
|
const label = `@${handleBase(type)}`;
|
|
2203
|
+
const originGeneration = sessionGeneration;
|
|
2204
|
+
const originSessionId = ctx.sessionManager.getSessionId();
|
|
2205
|
+
const isOriginCurrent = (): boolean =>
|
|
2206
|
+
currentCtx !== undefined &&
|
|
2207
|
+
sessionGeneration === originGeneration &&
|
|
2208
|
+
boundSessionId === originSessionId &&
|
|
2209
|
+
ctx.sessionManager.getSessionId() === originSessionId;
|
|
2056
2210
|
const startDirectly = (): void => {
|
|
2057
|
-
|
|
2211
|
+
// The hidden model turn may outlive a change to agent files or settings.
|
|
2212
|
+
// Revalidate the handle rather than bypassing Agent's dispatch policy
|
|
2213
|
+
// with a type that has since been disabled or removed.
|
|
2214
|
+
reloadCustomAgents();
|
|
2215
|
+
const current = resolveSpawnType(type);
|
|
2216
|
+
if (
|
|
2217
|
+
!current.ok ||
|
|
2218
|
+
current.fellBackFrom !== undefined ||
|
|
2219
|
+
current.type !== type
|
|
2220
|
+
) {
|
|
2221
|
+
throw new Error(`Agent type "${type}" is no longer available`);
|
|
2222
|
+
}
|
|
2223
|
+
spawnMention(ctx, current.type, mention.message, {
|
|
2058
2224
|
description: describeMention(mention.message),
|
|
2059
2225
|
});
|
|
2060
2226
|
};
|
|
2061
2227
|
|
|
2062
|
-
// In `model` mode
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2065
|
-
|
|
2066
|
-
const registeredAgentTool = agentToolRef;
|
|
2228
|
+
// In `model` mode an off-screen mention-only turn rewrites the typed task
|
|
2229
|
+
// without copying parent history or its request-local context hooks.
|
|
2230
|
+
// Nothing reaches the chat, and it starts an ordinary top-level agent.
|
|
2231
|
+
const registeredAgentTool = agentToolRef ? freshAgentToolDefinition() : undefined;
|
|
2067
2232
|
if (getAgentMentionMode() === "model" && registeredAgentTool) {
|
|
2233
|
+
// The hidden turn is itself a provider call. Refuse before saying
|
|
2234
|
+
// "Starting" or spending that call when its inherited parent model is
|
|
2235
|
+
// unavailable. A shape-only check would let the hidden clone refuse, then
|
|
2236
|
+
// let its direct fallback allocate an ID and announce an agent that fails.
|
|
2237
|
+
try {
|
|
2238
|
+
parentModelSessionOptions(ctx, ctx.model);
|
|
2239
|
+
} catch (err) {
|
|
2240
|
+
ctx.ui.notify(
|
|
2241
|
+
`Could not start ${label}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2242
|
+
"error",
|
|
2243
|
+
);
|
|
2244
|
+
return { action: "handled" };
|
|
2245
|
+
}
|
|
2068
2246
|
ctx.ui.notify(`Starting ${label}…`, "info");
|
|
2069
2247
|
// Not awaited: the clone runs a full model turn and `prompt()` is blocked
|
|
2070
2248
|
// until this hook returns. The user gets their prompt back immediately.
|
|
2071
|
-
void runMentionClone({
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2249
|
+
void runMentionClone({
|
|
2250
|
+
ctx,
|
|
2251
|
+
type,
|
|
2252
|
+
message: mention.message,
|
|
2253
|
+
agentTool: registeredAgentTool,
|
|
2254
|
+
isOriginCurrent,
|
|
2255
|
+
}).then((result) => {
|
|
2256
|
+
if (result.spawned || !isOriginCurrent()) return;
|
|
2257
|
+
if (result.refused) {
|
|
2258
|
+
ctx.ui.notify(
|
|
2259
|
+
`Could not start ${label} — ${result.error}`,
|
|
2260
|
+
"warning",
|
|
2261
|
+
);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
// A clone that could not run must not swallow the mention: start the
|
|
2265
|
+
// agent the direct way rather than leaving a toast and nothing running.
|
|
2266
|
+
try {
|
|
2267
|
+
startDirectly();
|
|
2268
|
+
ctx.ui.notify(
|
|
2269
|
+
`Started ${label} directly — ${result.error}`,
|
|
2270
|
+
"warning",
|
|
2271
|
+
);
|
|
2272
|
+
} catch (err) {
|
|
2273
|
+
ctx.ui.notify(
|
|
2274
|
+
`Could not start ${label}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2275
|
+
"error",
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2087
2279
|
return { action: "handled" };
|
|
2088
2280
|
}
|
|
2089
2281
|
|
|
@@ -2129,15 +2321,13 @@ function activateRootRuntime(
|
|
|
2129
2321
|
// The catalogue is injected into the description rather than left to a lookup
|
|
2130
2322
|
// tool: the host has to pick a tier on its first call, and a tool it must
|
|
2131
2323
|
// remember to call first is a tool it will skip.
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
? `\n\n${compactTierListText}`
|
|
2137
|
-
: "";
|
|
2324
|
+
const tierSection = (compact = false): string => {
|
|
2325
|
+
const text = compact ? buildCompactAgentTierListText() : buildAgentTierListText();
|
|
2326
|
+
return text ? `\n\n${text}` : "";
|
|
2327
|
+
};
|
|
2138
2328
|
|
|
2139
|
-
const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
|
|
2140
|
-
${buildCompactTypeListText()}${
|
|
2329
|
+
const compactAgentToolDescription = () => `Launch an autonomous agent for complex, multi-step tasks. Agent types:
|
|
2330
|
+
${buildCompactTypeListText()}${tierSection(true)}
|
|
2141
2331
|
|
|
2142
2332
|
Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global).
|
|
2143
2333
|
|
|
@@ -2146,14 +2336,15 @@ Notes:
|
|
|
2146
2336
|
- Parallel work: one message, multiple Agent calls, run_in_background: true on each. You are notified when background agents finish — never poll or sleep.
|
|
2147
2337
|
- The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
|
|
2148
2338
|
- resume continues a previous agent by ID; steer_subagent messages a running or queued one.
|
|
2149
|
-
- isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch
|
|
2339
|
+
- isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.
|
|
2340
|
+
- Raw inherit_context is unavailable; put only an explicitly sanitized summary in the task prompt.`;
|
|
2150
2341
|
|
|
2151
|
-
const fullAgentToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
2342
|
+
const fullAgentToolDescription = () => `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
2152
2343
|
|
|
2153
2344
|
Available agent types and the tools they have access to:
|
|
2154
2345
|
${buildTypeListText()}
|
|
2155
2346
|
|
|
2156
|
-
Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.${tierSection}
|
|
2347
|
+
Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.${tierSection()}
|
|
2157
2348
|
|
|
2158
2349
|
When using the Agent tool, specify a subagent_type parameter to select which agent type to use.
|
|
2159
2350
|
|
|
@@ -2174,7 +2365,7 @@ If the target is already known, use a direct tool — \`read\` for a known path,
|
|
|
2174
2365
|
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
|
|
2175
2366
|
- If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
|
|
2176
2367
|
- Use tier to pick the model profile for this spawn, by name. A tier overrides the agent's own default tier. Model and thinking are not callable parameters — they are what a tier resolves to.
|
|
2177
|
-
-
|
|
2368
|
+
- Raw inherit_context is unavailable: put only an explicitly sanitized summary in the task prompt, or persist a context_edit before starting a new agent.
|
|
2178
2369
|
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications). The worktree is automatically cleaned up if the agent makes no changes; otherwise the path and branch are returned in the result.${scheduleGuideline}
|
|
2179
2370
|
|
|
2180
2371
|
## Writing the prompt
|
|
@@ -2201,8 +2392,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2201
2392
|
// Both carry their own leading blank line, so a template that drops the
|
|
2202
2393
|
// placeholder inline renders byte-identically to the built-in description
|
|
2203
2394
|
// whether or not any tier is configured.
|
|
2204
|
-
tierList: () => tierSection,
|
|
2205
|
-
compactTierList: () =>
|
|
2395
|
+
tierList: () => tierSection(),
|
|
2396
|
+
compactTierList: () => tierSection(true),
|
|
2206
2397
|
defaultTier: getDefaultAgentTierText,
|
|
2207
2398
|
agentDir: getAgentDir,
|
|
2208
2399
|
scheduleGuideline: () => scheduleGuideline,
|
|
@@ -2218,10 +2409,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2218
2409
|
};
|
|
2219
2410
|
|
|
2220
2411
|
const loadCustomToolDescription = (): string | undefined => {
|
|
2412
|
+
const trust = captureProjectTrust(configurationOwner);
|
|
2221
2413
|
for (const path of [
|
|
2222
|
-
join(
|
|
2414
|
+
...(trust.trusted ? [join(trust.cwd, ".pi", "agent-tool-description.md")] : []),
|
|
2223
2415
|
join(getAgentDir(), "agent-tool-description.md"),
|
|
2224
2416
|
]) {
|
|
2417
|
+
if (!trust.trusted && isProjectResource(path, trust.cwd)) continue;
|
|
2225
2418
|
try {
|
|
2226
2419
|
if (!existsSync(path)) continue;
|
|
2227
2420
|
const text = readFileSync(path, "utf-8").trim();
|
|
@@ -2236,9 +2429,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2236
2429
|
return undefined;
|
|
2237
2430
|
};
|
|
2238
2431
|
|
|
2239
|
-
const agentToolDescription = (
|
|
2432
|
+
const agentToolDescription = () => {
|
|
2433
|
+
const trust = captureProjectTrust(configurationOwner);
|
|
2434
|
+
if (trust.cwd !== activeTrust.cwd || trust.trusted !== activeTrust.trusted) reloadCustomAgents();
|
|
2240
2435
|
const mode = getToolDescriptionMode();
|
|
2241
|
-
if (mode === "compact") return compactAgentToolDescription;
|
|
2436
|
+
if (mode === "compact") return compactAgentToolDescription();
|
|
2242
2437
|
if (mode === "custom") {
|
|
2243
2438
|
const custom = loadCustomToolDescription();
|
|
2244
2439
|
if (custom) return custom;
|
|
@@ -2246,798 +2441,850 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2246
2441
|
'[pi-subagents] toolDescriptionMode is "custom" but no agent-tool-description.md found — using "full"',
|
|
2247
2442
|
);
|
|
2248
2443
|
}
|
|
2249
|
-
return fullAgentToolDescription;
|
|
2250
|
-
}
|
|
2444
|
+
return fullAgentToolDescription();
|
|
2445
|
+
};
|
|
2251
2446
|
|
|
2252
2447
|
// Captured so the mention clone can hand the copy the REAL registered tool:
|
|
2253
2448
|
// its handler closes over this activation, which is what makes a clone-driven
|
|
2254
2449
|
// spawn an ordinary top-level agent rather than something the fork owns.
|
|
2450
|
+
const agentTypeParameterDescription = () =>
|
|
2451
|
+
`The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`;
|
|
2255
2452
|
const agentTool = defineTool({
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2453
|
+
name: SUBAGENT_TOOL_NAMES.AGENT,
|
|
2454
|
+
label: "Agent",
|
|
2455
|
+
get description() { return agentToolDescription(); },
|
|
2456
|
+
promptSnippet: "Launch autonomous sub-agents for complex multi-step tasks",
|
|
2457
|
+
promptGuidelines: [
|
|
2458
|
+
"Use Agent with specialized agents when the task matches an agent type's description. Subagents are valuable for parallelizing independent queries or for protecting the main context window from excessive results, but should not be used excessively when not needed. Importantly, avoid duplicating work that subagents are already doing — if you delegate research to a subagent, do not also perform the same searches yourself.",
|
|
2459
|
+
"For broad codebase exploration or research, spawn Agent with an appropriate subagent_type (e.g. Explore). Otherwise use direct tools (read, grep, find) when the target is already known.",
|
|
2460
|
+
"When an agent runs in the background, you will be notified on completion — do not poll or sleep waiting for it. Continue with other work instead.",
|
|
2461
|
+
"Trust but verify: an agent's summary describes intent, not outcome. When an agent writes or edits code, check the actual changes before reporting work as done.",
|
|
2462
|
+
],
|
|
2463
|
+
parameters: Type.Object({
|
|
2464
|
+
prompt: Type.String({
|
|
2465
|
+
description: "The task for the agent to perform.",
|
|
2466
|
+
}),
|
|
2467
|
+
description: Type.String({
|
|
2468
|
+
description:
|
|
2469
|
+
"A short (3-5 word) description of the task (shown in UI).",
|
|
2470
|
+
}),
|
|
2471
|
+
subagent_type: Type.String({
|
|
2472
|
+
description: agentTypeParameterDescription(),
|
|
2473
|
+
}),
|
|
2474
|
+
tier: Type.Optional(
|
|
2475
|
+
Type.String({
|
|
2476
|
+
description: buildAgentTierParameterDescription(),
|
|
2270
2477
|
}),
|
|
2271
|
-
|
|
2478
|
+
),
|
|
2479
|
+
max_turns: Type.Optional(
|
|
2480
|
+
Type.Number({
|
|
2272
2481
|
description:
|
|
2273
|
-
"
|
|
2482
|
+
"Maximum number of agentic turns before stopping. Omit for unlimited (default).",
|
|
2483
|
+
minimum: 1,
|
|
2274
2484
|
}),
|
|
2275
|
-
|
|
2276
|
-
|
|
2485
|
+
),
|
|
2486
|
+
run_in_background: Type.Optional(
|
|
2487
|
+
Type.Boolean({
|
|
2488
|
+
description:
|
|
2489
|
+
"Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
|
|
2277
2490
|
}),
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
),
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
),
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2491
|
+
),
|
|
2492
|
+
resume: Type.Optional(
|
|
2493
|
+
Type.String({
|
|
2494
|
+
description:
|
|
2495
|
+
"Optional agent ID to resume from. Continues from previous context.",
|
|
2496
|
+
}),
|
|
2497
|
+
),
|
|
2498
|
+
isolated: Type.Optional(
|
|
2499
|
+
Type.Boolean({
|
|
2500
|
+
description:
|
|
2501
|
+
"If true, agent gets no extension/MCP tools — only built-in tools.",
|
|
2502
|
+
}),
|
|
2503
|
+
),
|
|
2504
|
+
inherit_context: Type.Optional(
|
|
2505
|
+
Type.Boolean({
|
|
2506
|
+
description:
|
|
2507
|
+
"Unavailable on current Pi hosts: true is rejected before child dispatch. Omit or set false; provide an explicitly sanitized summary in the task prompt instead.",
|
|
2508
|
+
}),
|
|
2509
|
+
),
|
|
2510
|
+
isolation: Type.Optional(
|
|
2511
|
+
Type.Union(
|
|
2512
|
+
[
|
|
2513
|
+
Type.Literal("worktree", {
|
|
2514
|
+
description:
|
|
2515
|
+
"Run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.",
|
|
2516
|
+
}),
|
|
2517
|
+
Type.Literal("off", {
|
|
2518
|
+
description:
|
|
2519
|
+
"Explicitly disable worktree isolation for this agent.",
|
|
2520
|
+
}),
|
|
2521
|
+
],
|
|
2522
|
+
{
|
|
2310
2523
|
description:
|
|
2311
|
-
|
|
2312
|
-
}
|
|
2313
|
-
),
|
|
2314
|
-
isolation: Type.Optional(
|
|
2315
|
-
Type.Union(
|
|
2316
|
-
[
|
|
2317
|
-
Type.Literal("worktree", {
|
|
2318
|
-
description:
|
|
2319
|
-
'Run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
|
|
2320
|
-
}),
|
|
2321
|
-
Type.Literal("off", {
|
|
2322
|
-
description: 'Explicitly disable worktree isolation for this agent.',
|
|
2323
|
-
}),
|
|
2324
|
-
],
|
|
2325
|
-
{
|
|
2326
|
-
description: 'Isolation mode: "worktree" for isolated git worktree, "off" to explicitly disable.',
|
|
2327
|
-
},
|
|
2328
|
-
),
|
|
2524
|
+
'Isolation mode: "worktree" for isolated git worktree, "off" to explicitly disable.',
|
|
2525
|
+
},
|
|
2329
2526
|
),
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
// ---- Custom rendering: Claude Code style ----
|
|
2334
|
-
|
|
2335
|
-
renderCall(args, theme) {
|
|
2336
|
-
const displayName = args.subagent_type
|
|
2337
|
-
? getDisplayName(args.subagent_type)
|
|
2338
|
-
: "Agent";
|
|
2339
|
-
const desc = sanitizeDisplayText(args.description ?? "");
|
|
2340
|
-
const text = [
|
|
2341
|
-
theme.fg("toolTitle", theme.bold(displayName)),
|
|
2342
|
-
desc ? theme.fg("muted", desc) : undefined,
|
|
2343
|
-
]
|
|
2344
|
-
.filter((part): part is string => part !== undefined)
|
|
2345
|
-
.join(theme.fg("dim", " · "));
|
|
2346
|
-
return new Text(text, 0, 0);
|
|
2347
|
-
},
|
|
2348
|
-
|
|
2349
|
-
renderResult(result, { expanded, isPartial }, theme, renderContext) {
|
|
2350
|
-
// Everything below draws child-derived text into the parent transcript
|
|
2351
|
-
// through pi-tui's ANSI-preserving renderer, so it is scrubbed on the way in.
|
|
2352
|
-
const text = safeTerminalText(
|
|
2353
|
-
result.content[0]?.type === "text" ? result.content[0].text : "",
|
|
2354
|
-
);
|
|
2355
|
-
const details = result.details as AgentDetails | undefined;
|
|
2356
|
-
// Pre-execution failures have no agent status. Preserve pi's error text
|
|
2357
|
-
// instead of rendering them as an invented subagent failure.
|
|
2358
|
-
if (renderContext?.isError || !details?.status) {
|
|
2359
|
-
return new Text(text, 0, 0);
|
|
2360
|
-
}
|
|
2527
|
+
),
|
|
2528
|
+
...scheduleParam,
|
|
2529
|
+
}),
|
|
2361
2530
|
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2531
|
+
// ---- Custom rendering: Claude Code style ----
|
|
2532
|
+
|
|
2533
|
+
renderCall(args, theme) {
|
|
2534
|
+
const displayName = args.subagent_type
|
|
2535
|
+
? getDisplayName(args.subagent_type)
|
|
2536
|
+
: "Agent";
|
|
2537
|
+
const desc = sanitizeDisplayText(args.description ?? "");
|
|
2538
|
+
const text = [
|
|
2539
|
+
theme.fg("toolTitle", theme.bold(displayName)),
|
|
2540
|
+
desc ? theme.fg("muted", desc) : undefined,
|
|
2541
|
+
]
|
|
2542
|
+
.filter((part): part is string => part !== undefined)
|
|
2543
|
+
.join(theme.fg("dim", " · "));
|
|
2544
|
+
return new Text(text, 0, 0);
|
|
2545
|
+
},
|
|
2375
2546
|
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2547
|
+
renderResult(result, { expanded, isPartial }, theme, renderContext) {
|
|
2548
|
+
// Everything below draws child-derived text into the parent transcript
|
|
2549
|
+
// through pi-tui's ANSI-preserving renderer, so it is scrubbed on the way in.
|
|
2550
|
+
const text = safeTerminalText(
|
|
2551
|
+
result.content[0]?.type === "text" ? result.content[0].text : "",
|
|
2552
|
+
);
|
|
2553
|
+
const details = result.details as AgentDetails | undefined;
|
|
2554
|
+
// Pre-execution failures have no agent status. Preserve pi's error text
|
|
2555
|
+
// instead of rendering them as an invented subagent failure.
|
|
2556
|
+
if (renderContext?.isError || !details?.status) {
|
|
2557
|
+
return new Text(text, 0, 0);
|
|
2558
|
+
}
|
|
2387
2559
|
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
details.activity ?? "Thinking...",
|
|
2395
|
-
theme,
|
|
2396
|
-
);
|
|
2560
|
+
const stats = (d: AgentDetails) => {
|
|
2561
|
+
const parts: string[] = [];
|
|
2562
|
+
if (d.modelName) parts.push(d.modelName);
|
|
2563
|
+
if (d.tags) parts.push(...d.tags);
|
|
2564
|
+
if (d.turnCount != null && d.turnCount > 0) {
|
|
2565
|
+
parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
2397
2566
|
}
|
|
2567
|
+
if (d.toolUses > 0) parts.push(`tools ${d.toolUses}`);
|
|
2568
|
+
if (d.tokens) parts.push(d.tokens);
|
|
2569
|
+
return parts
|
|
2570
|
+
.map((p) => fgPreservingNestedStyles(theme, "dim", p))
|
|
2571
|
+
.join(theme.fg("dim", " · "));
|
|
2572
|
+
};
|
|
2398
2573
|
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2574
|
+
if (details.status === "queued") {
|
|
2575
|
+
const id = details.agentId ? ` · ID ${details.agentId}` : "";
|
|
2576
|
+
return new Text(
|
|
2577
|
+
theme.fg(
|
|
2578
|
+
"dim",
|
|
2579
|
+
`${getAgentStatusMark("queued")} queued · background${id}`,
|
|
2580
|
+
),
|
|
2581
|
+
0,
|
|
2582
|
+
0,
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2410
2585
|
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
`${getAgentStatusMark(details.status)} ${statusText}`,
|
|
2422
|
-
);
|
|
2423
|
-
if (s) line += theme.fg("dim", " · ") + s;
|
|
2424
|
-
line += theme.fg("dim", " · ") + theme.fg("dim", duration);
|
|
2425
|
-
|
|
2426
|
-
if (expanded) {
|
|
2427
|
-
if (text) {
|
|
2428
|
-
const lines = text.split("\n").slice(0, 50);
|
|
2429
|
-
for (const l of lines) {
|
|
2430
|
-
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
2431
|
-
}
|
|
2432
|
-
if (text.split("\n").length > 50) {
|
|
2433
|
-
line +=
|
|
2434
|
-
"\n" +
|
|
2435
|
-
theme.fg(
|
|
2436
|
-
"muted",
|
|
2437
|
-
" ... (use get_subagent_result with verbose for full output)",
|
|
2438
|
-
);
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
} else {
|
|
2442
|
-
const doneText = isSteered
|
|
2443
|
-
? "Wrapped up at the turn limit"
|
|
2444
|
-
: "Done";
|
|
2445
|
-
line += "\n" + theme.fg("dim", ` ${doneText}`);
|
|
2446
|
-
}
|
|
2447
|
-
return new Text(line, 0, 0);
|
|
2448
|
-
}
|
|
2586
|
+
if (isPartial || details.status === "running") {
|
|
2587
|
+
const frame = SPINNER[details.spinnerFrame ?? 0] ?? SPINNER[0]!;
|
|
2588
|
+
const s = stats(details);
|
|
2589
|
+
return renderRunningAgentStatus(
|
|
2590
|
+
frame,
|
|
2591
|
+
s,
|
|
2592
|
+
details.activity ?? "Thinking...",
|
|
2593
|
+
theme,
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2449
2596
|
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2597
|
+
if (details.status === "background") {
|
|
2598
|
+
const id = details.agentId ? ` · ID ${details.agentId}` : "";
|
|
2599
|
+
return new Text(
|
|
2600
|
+
theme.fg(
|
|
2453
2601
|
"dim",
|
|
2454
|
-
`${getAgentStatusMark("
|
|
2455
|
-
)
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
// Keep unknown/future statuses from falling through to the turn-limit
|
|
2462
|
-
// renderer, which is only valid for explicit error/aborted outcomes.
|
|
2463
|
-
if (details.status !== "error" && details.status !== "aborted") {
|
|
2464
|
-
return new Text(text, 0, 0);
|
|
2465
|
-
}
|
|
2602
|
+
`${getAgentStatusMark("running")} running · background${id}`,
|
|
2603
|
+
),
|
|
2604
|
+
0,
|
|
2605
|
+
0,
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2466
2608
|
|
|
2609
|
+
if (details.status === "completed" || details.status === "steered") {
|
|
2610
|
+
const duration = formatMs(details.durationMs);
|
|
2611
|
+
const isSteered = details.status === "steered";
|
|
2612
|
+
const statusText = isSteered ? "wrapped up · turn limit" : "completed";
|
|
2613
|
+
const statusColor = isSteered ? "warning" : "dim";
|
|
2467
2614
|
const s = stats(details);
|
|
2468
|
-
const isError = details.status === "error";
|
|
2469
2615
|
let line = theme.fg(
|
|
2470
|
-
|
|
2471
|
-
`${getAgentStatusMark(details.status)} ${
|
|
2616
|
+
statusColor,
|
|
2617
|
+
`${getAgentStatusMark(details.status)} ${statusText}`,
|
|
2472
2618
|
);
|
|
2473
2619
|
if (s) line += theme.fg("dim", " · ") + s;
|
|
2620
|
+
line += theme.fg("dim", " · ") + theme.fg("dim", duration);
|
|
2474
2621
|
|
|
2475
|
-
if (
|
|
2476
|
-
|
|
2477
|
-
"\n"
|
|
2478
|
-
|
|
2479
|
-
"
|
|
2480
|
-
|
|
2481
|
-
)
|
|
2622
|
+
if (expanded) {
|
|
2623
|
+
if (text) {
|
|
2624
|
+
const lines = text.split("\n").slice(0, 50);
|
|
2625
|
+
for (const l of lines) {
|
|
2626
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
2627
|
+
}
|
|
2628
|
+
if (text.split("\n").length > 50) {
|
|
2629
|
+
line +=
|
|
2630
|
+
"\n" +
|
|
2631
|
+
theme.fg(
|
|
2632
|
+
"muted",
|
|
2633
|
+
" ... (use get_subagent_result with verbose for full output)",
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2482
2637
|
} else {
|
|
2483
|
-
|
|
2638
|
+
const doneText = isSteered ? "Wrapped up at the turn limit" : "Done";
|
|
2639
|
+
line += "\n" + theme.fg("dim", ` ${doneText}`);
|
|
2484
2640
|
}
|
|
2641
|
+
return new Text(line, 0, 0);
|
|
2642
|
+
}
|
|
2485
2643
|
|
|
2644
|
+
if (details.status === "stopped") {
|
|
2645
|
+
const s = stats(details);
|
|
2646
|
+
let line = theme.fg("dim", `${getAgentStatusMark("stopped")} stopped`);
|
|
2647
|
+
if (s) line += theme.fg("dim", " · ") + s;
|
|
2648
|
+
line += "\n" + theme.fg("dim", " Stopped before completion");
|
|
2486
2649
|
return new Text(line, 0, 0);
|
|
2487
|
-
}
|
|
2650
|
+
}
|
|
2488
2651
|
|
|
2489
|
-
//
|
|
2652
|
+
// Keep unknown/future statuses from falling through to the turn-limit
|
|
2653
|
+
// renderer, which is only valid for explicit error/aborted outcomes.
|
|
2654
|
+
if (details.status !== "error" && details.status !== "aborted") {
|
|
2655
|
+
return new Text(text, 0, 0);
|
|
2656
|
+
}
|
|
2490
2657
|
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2658
|
+
const s = stats(details);
|
|
2659
|
+
const isError = details.status === "error";
|
|
2660
|
+
let line = theme.fg(
|
|
2661
|
+
isError ? "error" : "warning",
|
|
2662
|
+
`${getAgentStatusMark(details.status)} ${isError ? "failed" : "aborted"}`,
|
|
2663
|
+
);
|
|
2664
|
+
if (s) line += theme.fg("dim", " · ") + s;
|
|
2494
2665
|
|
|
2495
|
-
|
|
2496
|
-
|
|
2666
|
+
if (isError) {
|
|
2667
|
+
line +=
|
|
2668
|
+
"\n" +
|
|
2669
|
+
theme.fg(
|
|
2670
|
+
"error",
|
|
2671
|
+
` Error: ${sanitizeDisplayText(details.error ?? "unknown")}`,
|
|
2672
|
+
);
|
|
2673
|
+
} else {
|
|
2674
|
+
line += "\n" + theme.fg("warning", " Aborted at the turn limit");
|
|
2675
|
+
}
|
|
2497
2676
|
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
// case-ambiguous types are refused here, BEFORE anything spawns, so a
|
|
2501
|
-
// background or scheduled call can't start running the wrong agent while
|
|
2502
|
-
// the caller is still unaware. `fallbackSubagent` decides whether an
|
|
2503
|
-
// unresolvable type falls back or fails closed.
|
|
2504
|
-
const dispatch = resolveSpawnType(rawType);
|
|
2505
|
-
// `resume` replays a stored session and ignores `subagent_type` entirely,
|
|
2506
|
-
// but the parameter is required by the schema — so gating it here would
|
|
2507
|
-
// make a live agent unresumable the moment its type is deleted, disabled,
|
|
2508
|
-
// or gains a case-clashing sibling. Only a real spawn is gated.
|
|
2509
|
-
if (!dispatch.ok && !params.resume) return textResult(dispatch.message);
|
|
2510
|
-
const subagentType = dispatch.ok ? dispatch.type : rawType;
|
|
2511
|
-
// What the caller actually asked for, named once: `fellBackFrom` is "" for
|
|
2512
|
-
// a blank request, so reading it inline invites the `??`-vs-`||` slip that
|
|
2513
|
-
// once persisted an empty type into a scheduled job.
|
|
2514
|
-
const requestedType =
|
|
2515
|
-
(dispatch.ok && dispatch.fellBackFrom) || subagentType;
|
|
2516
|
-
// Computed at resolution rather than after the run, so the background and
|
|
2517
|
-
// schedule branches carry it too — previously it existed only on the
|
|
2518
|
-
// foreground path. Resume deliberately doesn't: it replays the stored
|
|
2519
|
-
// session and ignores `subagent_type` entirely, so a note about type
|
|
2520
|
-
// substitution would be describing something that didn't happen.
|
|
2521
|
-
const fallbackNote =
|
|
2522
|
-
dispatch.ok && dispatch.fellBackFrom !== undefined
|
|
2523
|
-
? `Note: Unknown agent type "${dispatch.fellBackFrom}" — using ${resolveType(subagentType) ? subagentType : "the fallback agent config"}.\n\n`
|
|
2524
|
-
: "";
|
|
2525
|
-
|
|
2526
|
-
const displayName = getDisplayName(subagentType);
|
|
2527
|
-
|
|
2528
|
-
// Get agent config (if any)
|
|
2529
|
-
const customConfig = getAgentConfig(subagentType);
|
|
2530
|
-
|
|
2531
|
-
const resolvedConfig = resolveAgentInvocationConfig(customConfig, {
|
|
2532
|
-
...params,
|
|
2533
|
-
agentTiers: getAgentTiersSettings(),
|
|
2534
|
-
});
|
|
2677
|
+
return new Text(line, 0, 0);
|
|
2678
|
+
},
|
|
2535
2679
|
|
|
2536
|
-
|
|
2537
|
-
// fields unset here so runAgent is the only resolver and ordinary Agent
|
|
2538
|
-
// calls cannot accidentally bypass the profile with a parent/default pin.
|
|
2539
|
-
let model = resolvedConfig.agentTierSelected
|
|
2540
|
-
? undefined
|
|
2541
|
-
: resolveConfiguredDefaultModel(ctx.modelRegistry) ?? ctx.model;
|
|
2542
|
-
if (!resolvedConfig.agentTierSelected && resolvedConfig.modelInput) {
|
|
2543
|
-
const resolved = resolveModel(
|
|
2544
|
-
resolvedConfig.modelInput,
|
|
2545
|
-
ctx.modelRegistry,
|
|
2546
|
-
);
|
|
2547
|
-
if (typeof resolved === "string") {
|
|
2548
|
-
if (resolvedConfig.modelFromParams) return textResult(resolved);
|
|
2549
|
-
// config-specified: silent fallback to the default model, then parent
|
|
2550
|
-
} else {
|
|
2551
|
-
model = resolved;
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2680
|
+
// ---- Execute ----
|
|
2554
2681
|
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2682
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
2683
|
+
// Ensure we have UI context for the FleetView list
|
|
2684
|
+
seatFleet(ctx);
|
|
2685
|
+
|
|
2686
|
+
// Reload custom agents so new project/global .md files are picked up without restart
|
|
2687
|
+
reloadCustomAgents();
|
|
2688
|
+
|
|
2689
|
+
const rawType = params.subagent_type as SubagentType;
|
|
2690
|
+
// Single decision point for dispatch (#183): unknown, disabled and
|
|
2691
|
+
// case-ambiguous types are refused here, BEFORE anything spawns, so a
|
|
2692
|
+
// background or scheduled call can't start running the wrong agent while
|
|
2693
|
+
// the caller is still unaware. `fallbackSubagent` decides whether an
|
|
2694
|
+
// unresolvable type falls back or fails closed.
|
|
2695
|
+
const dispatch = resolveSpawnType(rawType);
|
|
2696
|
+
// `resume` replays a stored session and ignores `subagent_type` entirely,
|
|
2697
|
+
// but the parameter is required by the schema — so gating it here would
|
|
2698
|
+
// make a live agent unresumable the moment its type is deleted, disabled,
|
|
2699
|
+
// or gains a case-clashing sibling. Only a real spawn is gated.
|
|
2700
|
+
if (!dispatch.ok && !params.resume) return textResult(dispatch.message);
|
|
2701
|
+
const subagentType = dispatch.ok ? dispatch.type : rawType;
|
|
2702
|
+
// What the caller actually asked for, named once: `fellBackFrom` is "" for
|
|
2703
|
+
// a blank request, so reading it inline invites the `??`-vs-`||` slip that
|
|
2704
|
+
// once persisted an empty type into a scheduled job.
|
|
2705
|
+
const requestedType =
|
|
2706
|
+
(dispatch.ok && dispatch.fellBackFrom) || subagentType;
|
|
2707
|
+
// Computed at resolution rather than after the run, so the background and
|
|
2708
|
+
// schedule branches carry it too — previously it existed only on the
|
|
2709
|
+
// foreground path. Resume deliberately doesn't: it replays the stored
|
|
2710
|
+
// session and ignores `subagent_type` entirely, so a note about type
|
|
2711
|
+
// substitution would be describing something that didn't happen.
|
|
2712
|
+
const fallbackNote =
|
|
2713
|
+
dispatch.ok && dispatch.fellBackFrom !== undefined
|
|
2714
|
+
? `Note: Unknown agent type "${dispatch.fellBackFrom}" — using ${resolveType(subagentType) ? subagentType : "the fallback agent config"}.\n\n`
|
|
2715
|
+
: "";
|
|
2716
|
+
|
|
2717
|
+
const displayName = getDisplayName(subagentType);
|
|
2718
|
+
|
|
2719
|
+
// Get agent config (if any)
|
|
2720
|
+
const customConfig = getAgentConfig(subagentType);
|
|
2721
|
+
|
|
2722
|
+
const resolvedConfig = resolveAgentInvocationConfig(customConfig, {
|
|
2723
|
+
...params,
|
|
2724
|
+
agentTiers: getAgentTiersSettings(),
|
|
2725
|
+
});
|
|
2726
|
+
if (!params.resume && !params.schedule && resolvedConfig.inheritContext) {
|
|
2727
|
+
return textResult(INHERIT_CONTEXT_UNAVAILABLE);
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
// A selected Agent tier owns final model/thinking resolution. Keep both
|
|
2731
|
+
// fields unset here so runAgent is the only resolver and ordinary Agent
|
|
2732
|
+
// calls cannot accidentally bypass the profile with a parent/default pin.
|
|
2733
|
+
let model = resolvedConfig.agentTierSelected
|
|
2734
|
+
? undefined
|
|
2735
|
+
: (resolveConfiguredDefaultModel(ctx.modelRegistry) ?? ctx.model);
|
|
2736
|
+
if (!resolvedConfig.agentTierSelected && resolvedConfig.modelInput) {
|
|
2737
|
+
const resolved = resolveModel(
|
|
2738
|
+
resolvedConfig.modelInput,
|
|
2739
|
+
ctx.modelRegistry,
|
|
2740
|
+
);
|
|
2741
|
+
if (typeof resolved === "string") {
|
|
2742
|
+
if (resolvedConfig.modelFromParams) return textResult(resolved);
|
|
2743
|
+
// config-specified: silent fallback to the default model, then parent
|
|
2744
|
+
} else {
|
|
2745
|
+
model = resolved;
|
|
2570
2746
|
}
|
|
2747
|
+
}
|
|
2571
2748
|
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
const
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
rec.outputFile = createOutputFilePath(
|
|
2590
|
-
ctx.cwd,
|
|
2591
|
-
agentId,
|
|
2592
|
-
ctx.sessionManager.getSessionId(),
|
|
2593
|
-
);
|
|
2594
|
-
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd);
|
|
2595
|
-
};
|
|
2749
|
+
// Tiered model scope is checked after the single final resolution in
|
|
2750
|
+
// runAgent. Only the legacy no-tier path is checked here.
|
|
2751
|
+
if (!resolvedConfig.agentTierSelected) {
|
|
2752
|
+
const scopeVerdict = checkModelScope({
|
|
2753
|
+
model,
|
|
2754
|
+
cwd: activeTrust.cwd,
|
|
2755
|
+
authority: activeTrust,
|
|
2756
|
+
modelRegistry: ctx.modelRegistry,
|
|
2757
|
+
callerSupplied: resolvedConfig.modelFromParams,
|
|
2758
|
+
agentLabel: customConfig?.displayName ?? subagentType,
|
|
2759
|
+
modelInput: resolvedConfig.modelInput,
|
|
2760
|
+
});
|
|
2761
|
+
if (scopeVerdict.kind === "error")
|
|
2762
|
+
return textResult(scopeVerdict.message);
|
|
2763
|
+
if (scopeVerdict.kind === "warn")
|
|
2764
|
+
ctx.ui.notify(scopeVerdict.message, "warning");
|
|
2765
|
+
}
|
|
2596
2766
|
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2767
|
+
const thinking = resolvedConfig.thinking;
|
|
2768
|
+
const inheritContext = resolvedConfig.inheritContext;
|
|
2769
|
+
const runInBackground = resolvedConfig.runInBackground;
|
|
2770
|
+
const isolated = resolvedConfig.isolated;
|
|
2771
|
+
const isolation = resolvedConfig.isolation;
|
|
2772
|
+
// Whether this spawn writes its .output transcript. Per-agent
|
|
2773
|
+
// frontmatter (`output_transcript`) wins; otherwise the project/global
|
|
2774
|
+
// default applies. `attachTranscript` below is the SOLE gate — every
|
|
2775
|
+
// downstream consumer keys off record.outputFile being set, so no spawn
|
|
2776
|
+
// path can re-enable the transcript by accident.
|
|
2777
|
+
const outputTranscript =
|
|
2778
|
+
customConfig?.outputTranscript ?? getOutputTranscriptDefault();
|
|
2779
|
+
const attachTranscript = (
|
|
2780
|
+
rec: AgentRecord | undefined,
|
|
2781
|
+
agentId: string,
|
|
2782
|
+
): void => {
|
|
2783
|
+
if (!rec || !outputTranscript) return;
|
|
2784
|
+
rec.outputFile = createOutputFilePath(
|
|
2785
|
+
ctx.cwd,
|
|
2786
|
+
agentId,
|
|
2787
|
+
ctx.sessionManager.getSessionId(),
|
|
2605
2788
|
);
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
...((resolvedConfig.requestedAgentTier ?? customConfig?.agentTier) === undefined
|
|
2609
|
-
? {}
|
|
2610
|
-
: { agentTier: resolvedConfig.requestedAgentTier ?? customConfig?.agentTier }),
|
|
2611
|
-
thinking,
|
|
2612
|
-
// Explicit value only — the default fallback would just add noise.
|
|
2613
|
-
// Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
|
|
2614
|
-
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
2615
|
-
isolated,
|
|
2616
|
-
inheritContext,
|
|
2617
|
-
runInBackground,
|
|
2618
|
-
isolation,
|
|
2619
|
-
};
|
|
2620
|
-
// Tool-result render shows the mode label too; viewer's header already does.
|
|
2621
|
-
const modeLabel = getPromptModeLabel(subagentType);
|
|
2622
|
-
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
2623
|
-
const agentTags = modeLabel
|
|
2624
|
-
? [modeLabel, ...invocationTags]
|
|
2625
|
-
: invocationTags;
|
|
2626
|
-
const detailBase = {
|
|
2627
|
-
displayName,
|
|
2628
|
-
description: params.description,
|
|
2629
|
-
subagentType,
|
|
2630
|
-
modelName,
|
|
2631
|
-
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
2632
|
-
};
|
|
2789
|
+
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd);
|
|
2790
|
+
};
|
|
2633
2791
|
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
// Store the resolved policy input (agent config first, tool params second)
|
|
2677
|
-
// so scheduled fires use the same model fallback as an immediate spawn.
|
|
2678
|
-
model: resolvedConfig.agentTierSelected ? undefined : resolvedConfig.modelInput,
|
|
2679
|
-
thinking: resolvedConfig.agentTierSelected ? undefined : thinking,
|
|
2680
|
-
max_turns: effectiveMaxTurns,
|
|
2681
|
-
isolated: isolated,
|
|
2682
|
-
isolation: isolation,
|
|
2683
|
-
});
|
|
2684
|
-
const next = scheduler.getNextRun(job.id);
|
|
2685
|
-
return textResult(
|
|
2686
|
-
`${fallbackNote}Scheduled "${job.name}" (id: ${job.id}, type: ${job.scheduleType}). ` +
|
|
2687
|
-
`Next run: ${next ?? "(unknown)"}. ` +
|
|
2688
|
-
`Manage via /agents, Scheduled jobs.`,
|
|
2689
|
-
);
|
|
2690
|
-
} catch (err) {
|
|
2691
|
-
return textResult(err instanceof Error ? err.message : String(err));
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2792
|
+
// Untiered spawns only. A tier resolves its model inside runAgent, and
|
|
2793
|
+
// its resolution callback supplies the label from there — computing one
|
|
2794
|
+
// here would name a model this path never resolved.
|
|
2795
|
+
const parentModelId = ctx.model?.id;
|
|
2796
|
+
const modelName =
|
|
2797
|
+
model && model.id !== parentModelId
|
|
2798
|
+
? shortModelLabel(model)
|
|
2799
|
+
: undefined;
|
|
2800
|
+
const effectiveMaxTurns = normalizeMaxTurns(
|
|
2801
|
+
resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
|
|
2802
|
+
);
|
|
2803
|
+
const agentInvocation: AgentInvocation = {
|
|
2804
|
+
modelName,
|
|
2805
|
+
...((resolvedConfig.requestedAgentTier ?? customConfig?.agentTier) ===
|
|
2806
|
+
undefined
|
|
2807
|
+
? {}
|
|
2808
|
+
: {
|
|
2809
|
+
agentTier:
|
|
2810
|
+
resolvedConfig.requestedAgentTier ?? customConfig?.agentTier,
|
|
2811
|
+
}),
|
|
2812
|
+
thinking,
|
|
2813
|
+
// Explicit value only — the default fallback would just add noise.
|
|
2814
|
+
// Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
|
|
2815
|
+
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
2816
|
+
isolated,
|
|
2817
|
+
inheritContext,
|
|
2818
|
+
runInBackground,
|
|
2819
|
+
isolation,
|
|
2820
|
+
};
|
|
2821
|
+
// Tool-result render shows the mode label too; viewer's header already does.
|
|
2822
|
+
const modeLabel = getPromptModeLabel(subagentType);
|
|
2823
|
+
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
2824
|
+
const agentTags = modeLabel
|
|
2825
|
+
? [modeLabel, ...invocationTags]
|
|
2826
|
+
: invocationTags;
|
|
2827
|
+
const detailBase = {
|
|
2828
|
+
displayName,
|
|
2829
|
+
description: params.description,
|
|
2830
|
+
subagentType,
|
|
2831
|
+
modelName,
|
|
2832
|
+
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
2833
|
+
};
|
|
2694
2834
|
|
|
2695
|
-
|
|
2835
|
+
// ---- Schedule: register a job, don't spawn now ----
|
|
2836
|
+
if (params.schedule) {
|
|
2837
|
+
if (!isSchedulingEnabled()) {
|
|
2838
|
+
return textResult(
|
|
2839
|
+
"Scheduling is disabled in this project. Enable via /agents, Settings, Scheduling.",
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2696
2842
|
if (params.resume) {
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2843
|
+
return textResult(
|
|
2844
|
+
"Cannot combine `schedule` with `resume` — schedules create fresh agents.",
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2847
|
+
if (inheritContext) {
|
|
2848
|
+
return textResult(
|
|
2849
|
+
"Cannot combine `schedule` with `inherit_context` — there is no parent conversation at fire time.",
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
if (params.run_in_background === false) {
|
|
2853
|
+
return textResult(
|
|
2854
|
+
"Cannot combine `schedule` with `run_in_background: false` — scheduled jobs always run in background.",
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
if (!scheduler.isActive()) {
|
|
2858
|
+
return textResult(
|
|
2859
|
+
"Scheduler is not active in this session yet. Try again after the session has fully started.",
|
|
2860
|
+
);
|
|
2861
|
+
}
|
|
2862
|
+
try {
|
|
2863
|
+
const job = scheduler.addJob({
|
|
2864
|
+
name: params.description as string,
|
|
2865
|
+
description: params.description as string,
|
|
2866
|
+
schedule: params.schedule as string,
|
|
2867
|
+
// The caller's own name, not the substitute — the scheduler re-resolves
|
|
2868
|
+
// at fire time, and the original is what a user edits.
|
|
2869
|
+
subagent_type: requestedType,
|
|
2870
|
+
prompt: params.prompt as string,
|
|
2871
|
+
// Only an explicitly requested tier is frozen into the job. An
|
|
2872
|
+
// agent's frontmatter tier is deliberately not copied: it is read
|
|
2873
|
+
// again at fire time, so editing the agent file still takes
|
|
2874
|
+
// effect, and it keeps its "frontmatter" source rather than being
|
|
2875
|
+
// replayed as a caller choice.
|
|
2876
|
+
tier: resolvedConfig.requestedAgentTier,
|
|
2877
|
+
// Store the resolved policy input (agent config first, tool params second)
|
|
2878
|
+
// so scheduled fires use the same model fallback as an immediate spawn.
|
|
2879
|
+
model: resolvedConfig.agentTierSelected
|
|
2880
|
+
? undefined
|
|
2881
|
+
: resolvedConfig.modelInput,
|
|
2882
|
+
thinking: resolvedConfig.agentTierSelected ? undefined : thinking,
|
|
2883
|
+
max_turns: effectiveMaxTurns,
|
|
2884
|
+
isolated: isolated,
|
|
2885
|
+
isolation: isolation,
|
|
2886
|
+
});
|
|
2887
|
+
const next = scheduler.getNextRun(job.id);
|
|
2888
|
+
return textResult(
|
|
2889
|
+
`${fallbackNote}Scheduled "${job.name}" (id: ${job.id}, type: ${job.scheduleType}). ` +
|
|
2890
|
+
`Next run: ${next ?? "(unknown)"}. ` +
|
|
2891
|
+
`Manage via /agents, Scheduled jobs.`,
|
|
2892
|
+
);
|
|
2893
|
+
} catch (err) {
|
|
2894
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2711
2897
|
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
);
|
|
2755
|
-
};
|
|
2756
|
-
const record = await manager.resume(
|
|
2898
|
+
// Resume existing agent
|
|
2899
|
+
if (params.resume) {
|
|
2900
|
+
const existing = manager.getRecordMutable(params.resume);
|
|
2901
|
+
if (!existing || existing.parentAgentId) {
|
|
2902
|
+
return textResult(
|
|
2903
|
+
`Agent not found: "${params.resume}". It may have been cleaned up.`,
|
|
2904
|
+
);
|
|
2905
|
+
}
|
|
2906
|
+
if (!existing.session) {
|
|
2907
|
+
return textResult(
|
|
2908
|
+
`Agent "${params.resume}" has no active session to resume.`,
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2911
|
+
if (signal?.aborted) {
|
|
2912
|
+
return textResult("Resume aborted.");
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// Assigned unconditionally, before either resume path. The completion
|
|
2916
|
+
// notification carries this as `<tool-use-id>` (see
|
|
2917
|
+
// formatTaskNotification), and `manager.resume` clears
|
|
2918
|
+
// `resultConsumed`, so a resumed run does notify. Keeping the id the
|
|
2919
|
+
// original spawn wrote would point that notification at a tool call
|
|
2920
|
+
// answered runs ago; a resume with no tool call of its own must clear
|
|
2921
|
+
// it rather than inherit one.
|
|
2922
|
+
existing.toolCallId = toolCallId;
|
|
2923
|
+
|
|
2924
|
+
// run_in_background on resume: settle asynchronously and notify on
|
|
2925
|
+
// completion like a background spawn, returning immediately. Previously
|
|
2926
|
+
// the flag was accepted then silently dropped — a resumed agent always
|
|
2927
|
+
// blocked the caller until it finished.
|
|
2928
|
+
if (runInBackground) {
|
|
2929
|
+
const { state: bgState, callbacks: bgCallbacks } =
|
|
2930
|
+
createActivityTracker(effectiveMaxTurns);
|
|
2931
|
+
// resumeAgent has no onSessionCreated — the session predates this run
|
|
2932
|
+
// — so seed the activity tracker directly.
|
|
2933
|
+
bgState.session = existing.session;
|
|
2934
|
+
// Reuse the agent's transcript rather than starting a fresh one: the
|
|
2935
|
+
// path is deterministic per agent+session, and writing an initial
|
|
2936
|
+
// entry would truncate the previous run's turns (B1#2).
|
|
2937
|
+
if (outputTranscript) {
|
|
2938
|
+
existing.outputFile = createOutputFilePath(
|
|
2939
|
+
ctx.cwd,
|
|
2757
2940
|
params.resume,
|
|
2758
|
-
|
|
2759
|
-
undefined,
|
|
2760
|
-
{
|
|
2761
|
-
isBackground: true,
|
|
2762
|
-
onToolActivity: bgCallbacks.onToolActivity,
|
|
2763
|
-
onAssistantUsage: bgCallbacks.onAssistantUsage,
|
|
2764
|
-
onStarted: attachTranscript,
|
|
2765
|
-
},
|
|
2766
|
-
);
|
|
2767
|
-
if (!record) {
|
|
2768
|
-
return textResult(
|
|
2769
|
-
`Cannot resume agent "${params.resume}" in background — it is already running. ` +
|
|
2770
|
-
"Wait for it to settle, or steer it with steer_subagent.",
|
|
2771
|
-
);
|
|
2772
|
-
}
|
|
2773
|
-
agentActivity.set(params.resume, bgState);
|
|
2774
|
-
void bgCallbacks;
|
|
2775
|
-
return textResult(
|
|
2776
|
-
record.status === "queued"
|
|
2777
|
-
? `Agent "${params.resume}" resumed in background (queued at the concurrency limit).`
|
|
2778
|
-
: `Agent "${params.resume}" resumed in background.`,
|
|
2779
|
-
buildDetails(detailBase, record),
|
|
2941
|
+
ctx.sessionManager.getSessionId(),
|
|
2780
2942
|
);
|
|
2943
|
+
ensureOutputFile(existing.outputFile);
|
|
2781
2944
|
}
|
|
2782
|
-
|
|
2945
|
+
// Anchor streaming past the turns already on disk, captured BEFORE the
|
|
2946
|
+
// run starts. The resumed prompt lands as an ordinary user message at
|
|
2947
|
+
// this index, so it is written exactly once.
|
|
2948
|
+
const transcriptAnchor = existing.session.messages.length ?? 0;
|
|
2949
|
+
const attachTranscript = (): void => {
|
|
2950
|
+
if (!existing.outputFile || existing.outputCleanup) return;
|
|
2951
|
+
existing.outputCleanup = streamToOutputFile(
|
|
2952
|
+
existing.session!,
|
|
2953
|
+
existing.outputFile,
|
|
2954
|
+
params.resume!,
|
|
2955
|
+
ctx.cwd,
|
|
2956
|
+
transcriptAnchor,
|
|
2957
|
+
);
|
|
2958
|
+
};
|
|
2783
2959
|
const record = await manager.resume(
|
|
2784
2960
|
params.resume,
|
|
2785
2961
|
params.prompt,
|
|
2786
|
-
|
|
2962
|
+
undefined,
|
|
2963
|
+
{
|
|
2964
|
+
isBackground: true,
|
|
2965
|
+
onToolActivity: bgCallbacks.onToolActivity,
|
|
2966
|
+
onAssistantUsage: bgCallbacks.onAssistantUsage,
|
|
2967
|
+
onStarted: attachTranscript,
|
|
2968
|
+
},
|
|
2787
2969
|
);
|
|
2788
2970
|
if (!record) {
|
|
2789
|
-
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
2790
|
-
}
|
|
2791
|
-
// A failed resume surfaces the error, plus any partial output THIS
|
|
2792
|
-
// resume produced (never the previous turn's answer, #144).
|
|
2793
|
-
if (record.status === "error") {
|
|
2794
2971
|
return textResult(
|
|
2795
|
-
`
|
|
2796
|
-
|
|
2972
|
+
`Cannot resume agent "${params.resume}" in background — it is already running. ` +
|
|
2973
|
+
"Wait for it to settle, or steer it with steer_subagent.",
|
|
2797
2974
|
);
|
|
2798
2975
|
}
|
|
2976
|
+
agentActivity.set(params.resume, bgState);
|
|
2977
|
+
void bgCallbacks;
|
|
2799
2978
|
return textResult(
|
|
2800
|
-
record.
|
|
2979
|
+
record.status === "queued"
|
|
2980
|
+
? `Agent "${params.resume}" resumed in background (queued at the concurrency limit).`
|
|
2981
|
+
: `Agent "${params.resume}" resumed in background.`,
|
|
2801
2982
|
buildDetails(detailBase, record),
|
|
2802
2983
|
);
|
|
2803
2984
|
}
|
|
2804
2985
|
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
};
|
|
2827
|
-
|
|
2828
|
-
// A startup throw means the agent never started. Let pi mark the tool
|
|
2829
|
-
// call as failed instead of returning a successful-looking text result.
|
|
2830
|
-
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
2831
|
-
description: params.description,
|
|
2832
|
-
model: resolvedConfig.agentTierSelected ? undefined : model,
|
|
2833
|
-
maxTurns: effectiveMaxTurns,
|
|
2834
|
-
isolated,
|
|
2835
|
-
inheritContext,
|
|
2836
|
-
thinkingLevel: resolvedConfig.agentTierSelected ? undefined : thinking,
|
|
2837
|
-
agentTier: resolvedConfig.requestedAgentTier,
|
|
2838
|
-
isBackground: true,
|
|
2839
|
-
isolation,
|
|
2840
|
-
invocation: agentInvocation,
|
|
2841
|
-
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
2842
|
-
...bgCallbacks,
|
|
2843
|
-
});
|
|
2844
|
-
|
|
2845
|
-
// Set output file + join mode synchronously after spawn, before the
|
|
2846
|
-
// event loop yields — onSessionCreated is async so this is safe.
|
|
2847
|
-
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
2848
|
-
const record = manager.getRecordMutable(id);
|
|
2849
|
-
if (record && joinMode) {
|
|
2850
|
-
record.joinMode = joinMode;
|
|
2851
|
-
record.toolCallId = toolCallId;
|
|
2852
|
-
attachTranscript(record, id);
|
|
2853
|
-
}
|
|
2986
|
+
const record = await manager.resume(
|
|
2987
|
+
params.resume,
|
|
2988
|
+
params.prompt,
|
|
2989
|
+
signal,
|
|
2990
|
+
);
|
|
2991
|
+
if (!record) {
|
|
2992
|
+
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
2993
|
+
}
|
|
2994
|
+
// A failed resume surfaces the error, plus any partial output THIS
|
|
2995
|
+
// resume produced (never the previous turn's answer, #144).
|
|
2996
|
+
if (record.status === "error") {
|
|
2997
|
+
return textResult(
|
|
2998
|
+
`Agent failed: ${record.error}${partialOutputSuffix(record)}`,
|
|
2999
|
+
buildDetails(detailBase, record),
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
return textResult(
|
|
3003
|
+
record.result?.trim() || "No output.",
|
|
3004
|
+
buildDetails(detailBase, record),
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
2854
3007
|
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
3008
|
+
// Background execution
|
|
3009
|
+
if (runInBackground) {
|
|
3010
|
+
const { state: bgState, callbacks: bgCallbacks } =
|
|
3011
|
+
createActivityTracker(effectiveMaxTurns);
|
|
3012
|
+
|
|
3013
|
+
// Wrap onSessionCreated to wire output file streaming.
|
|
3014
|
+
// The callback lazily reads record.outputFile (set right after spawn)
|
|
3015
|
+
// rather than closing over a value that doesn't exist yet.
|
|
3016
|
+
let id: string;
|
|
3017
|
+
const origBgOnSession = bgCallbacks.onSessionCreated;
|
|
3018
|
+
bgCallbacks.onSessionCreated = (session: any) => {
|
|
3019
|
+
origBgOnSession(session);
|
|
3020
|
+
const rec = manager.getRecordMutable(id);
|
|
3021
|
+
if (rec?.outputFile) {
|
|
3022
|
+
rec.outputCleanup = streamToOutputFile(
|
|
3023
|
+
session,
|
|
3024
|
+
rec.outputFile,
|
|
3025
|
+
id,
|
|
3026
|
+
ctx.cwd,
|
|
3027
|
+
);
|
|
2864
3028
|
}
|
|
3029
|
+
};
|
|
2865
3030
|
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
3031
|
+
// A startup throw means the agent never started. Let pi mark the tool
|
|
3032
|
+
// call as failed instead of returning a successful-looking text result.
|
|
3033
|
+
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
3034
|
+
description: params.description,
|
|
3035
|
+
model: resolvedConfig.agentTierSelected ? undefined : model,
|
|
3036
|
+
maxTurns: effectiveMaxTurns,
|
|
3037
|
+
isolated,
|
|
3038
|
+
inheritContext,
|
|
3039
|
+
thinkingLevel: resolvedConfig.agentTierSelected
|
|
3040
|
+
? undefined
|
|
3041
|
+
: thinking,
|
|
3042
|
+
agentTier: resolvedConfig.requestedAgentTier,
|
|
3043
|
+
isBackground: true,
|
|
3044
|
+
isolation,
|
|
3045
|
+
invocation: agentInvocation,
|
|
3046
|
+
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
3047
|
+
...bgCallbacks,
|
|
3048
|
+
});
|
|
3049
|
+
// The clone needs a positive spawn witness before any UI, transcript,
|
|
3050
|
+
// or event observer can throw. A startup failure inside manager.spawn
|
|
3051
|
+
// removes its record and does not reach this acknowledgement.
|
|
3052
|
+
const record = manager.getRecordMutable(id);
|
|
3053
|
+
if (record) {
|
|
3054
|
+
(
|
|
3055
|
+
params as typeof params & {
|
|
3056
|
+
[MENTION_SPAWNED]?: (id: string) => void;
|
|
3057
|
+
}
|
|
3058
|
+
)[MENTION_SPAWNED]?.(id);
|
|
3059
|
+
}
|
|
2869
3060
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
}
|
|
3061
|
+
// Set output file + join mode synchronously after spawn, before the
|
|
3062
|
+
// event loop yields — onSessionCreated is async so this is safe.
|
|
3063
|
+
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
3064
|
+
if (record && joinMode) {
|
|
3065
|
+
record.joinMode = joinMode;
|
|
3066
|
+
record.toolCallId = toolCallId;
|
|
3067
|
+
attachTranscript(record, id);
|
|
3068
|
+
}
|
|
2879
3069
|
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
(isQueued
|
|
2890
|
-
? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n`
|
|
2891
|
-
: "") +
|
|
2892
|
-
`\nYou will be notified when this agent completes.\n` +
|
|
2893
|
-
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
2894
|
-
`Do not duplicate this agent's work.`,
|
|
2895
|
-
{
|
|
2896
|
-
...detailBase,
|
|
2897
|
-
toolUses: 0,
|
|
2898
|
-
tokens: "",
|
|
2899
|
-
durationMs: 0,
|
|
2900
|
-
status: isQueued ? "queued" : "background",
|
|
2901
|
-
agentId: id,
|
|
2902
|
-
},
|
|
2903
|
-
);
|
|
3070
|
+
if (joinMode == null || joinMode === "async") {
|
|
3071
|
+
// Foreground/no join mode or explicit async — not part of any batch
|
|
3072
|
+
} else {
|
|
3073
|
+
// smart or group — add to current batch
|
|
3074
|
+
currentBatchAgents.push({ id, joinMode });
|
|
3075
|
+
// Debounce: reset timer on each new agent so parallel tool calls
|
|
3076
|
+
// dispatched across multiple event loop ticks are captured together
|
|
3077
|
+
if (batchFinalizeTimer) clearTimeout(batchFinalizeTimer);
|
|
3078
|
+
batchFinalizeTimer = setTimeout(finalizeBatch, 100);
|
|
2904
3079
|
}
|
|
2905
3080
|
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
let fgId: string | undefined;
|
|
3081
|
+
agentActivity.set(id, bgState);
|
|
3082
|
+
fleet.ensureTimer();
|
|
3083
|
+
fleet.update();
|
|
2910
3084
|
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
durationMs: Date.now() - startedAt,
|
|
2919
|
-
status: "running",
|
|
2920
|
-
activity: describeActivity(
|
|
2921
|
-
fgState.activeTools,
|
|
2922
|
-
fgState.responseText,
|
|
2923
|
-
),
|
|
2924
|
-
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
2925
|
-
};
|
|
2926
|
-
onUpdate?.({
|
|
2927
|
-
content: [
|
|
2928
|
-
{ type: "text", text: `${fgState.toolUses} tool uses...` },
|
|
2929
|
-
],
|
|
2930
|
-
details: details as any,
|
|
3085
|
+
// Emit created event unless a branch replacement detached the record.
|
|
3086
|
+
if (!record?.detached) {
|
|
3087
|
+
pi.events.emit("subagents:created", {
|
|
3088
|
+
id,
|
|
3089
|
+
type: subagentType,
|
|
3090
|
+
description: params.description,
|
|
3091
|
+
isBackground: true,
|
|
2931
3092
|
});
|
|
2932
|
-
}
|
|
3093
|
+
}
|
|
2933
3094
|
|
|
2934
|
-
const
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
session,
|
|
2958
|
-
rec.outputFile,
|
|
2959
|
-
fgId,
|
|
2960
|
-
ctx.cwd,
|
|
2961
|
-
);
|
|
2962
|
-
}
|
|
2963
|
-
}
|
|
2964
|
-
};
|
|
3095
|
+
const isQueued = record?.status === "queued";
|
|
3096
|
+
return textResult(
|
|
3097
|
+
`${fallbackNote}Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
3098
|
+
`Agent ID: ${id}\n` +
|
|
3099
|
+
`Type: ${displayName}\n` +
|
|
3100
|
+
`Description: ${params.description}\n` +
|
|
3101
|
+
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
3102
|
+
(isQueued
|
|
3103
|
+
? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n`
|
|
3104
|
+
: "") +
|
|
3105
|
+
`\nYou will be notified when this agent completes.\n` +
|
|
3106
|
+
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
3107
|
+
`Do not duplicate this agent's work.`,
|
|
3108
|
+
{
|
|
3109
|
+
...detailBase,
|
|
3110
|
+
toolUses: 0,
|
|
3111
|
+
tokens: "",
|
|
3112
|
+
durationMs: 0,
|
|
3113
|
+
status: isQueued ? "queued" : "background",
|
|
3114
|
+
agentId: id,
|
|
3115
|
+
},
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
2965
3118
|
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
3119
|
+
// Foreground (synchronous) execution — stream progress via onUpdate
|
|
3120
|
+
let spinnerFrame = 0;
|
|
3121
|
+
const startedAt = Date.now();
|
|
3122
|
+
let fgId: string | undefined;
|
|
3123
|
+
|
|
3124
|
+
const streamUpdate = () => {
|
|
3125
|
+
const details: AgentDetails = {
|
|
3126
|
+
...detailBase,
|
|
3127
|
+
toolUses: fgState.toolUses,
|
|
3128
|
+
tokens: formatLifetimeTokens(fgState),
|
|
3129
|
+
turnCount: fgState.turnCount,
|
|
3130
|
+
maxTurns: fgState.maxTurns,
|
|
3131
|
+
durationMs: Date.now() - startedAt,
|
|
3132
|
+
status: "running",
|
|
3133
|
+
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
3134
|
+
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
3135
|
+
};
|
|
3136
|
+
onUpdate?.({
|
|
3137
|
+
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
|
|
3138
|
+
details: details as any,
|
|
3139
|
+
});
|
|
3140
|
+
};
|
|
2970
3141
|
|
|
2971
|
-
|
|
3142
|
+
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(
|
|
3143
|
+
effectiveMaxTurns,
|
|
3144
|
+
streamUpdate,
|
|
3145
|
+
);
|
|
2972
3146
|
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
thinkingLevel: resolvedConfig.agentTierSelected ? undefined : thinking,
|
|
2987
|
-
agentTier: resolvedConfig.requestedAgentTier,
|
|
2988
|
-
isolation,
|
|
2989
|
-
invocation: agentInvocation,
|
|
2990
|
-
signal,
|
|
2991
|
-
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
2992
|
-
...fgCallbacks,
|
|
2993
|
-
},
|
|
2994
|
-
(fgAgentId) => {
|
|
2995
|
-
// onSpawned: called synchronously after spawn, before onSessionCreated fires.
|
|
2996
|
-
// Set up the output file so streamToOutputFile can pick it up.
|
|
2997
|
-
const fgRec = manager.getRecordMutable(fgAgentId);
|
|
2998
|
-
attachTranscript(fgRec, fgAgentId);
|
|
2999
|
-
},
|
|
3000
|
-
);
|
|
3001
|
-
record = fgResult.record;
|
|
3002
|
-
} finally {
|
|
3003
|
-
// Always stop the spinner and drop the row, including startup errors
|
|
3004
|
-
// that now propagate to pi as failed tool calls.
|
|
3005
|
-
clearInterval(spinnerInterval);
|
|
3006
|
-
if (fgId) {
|
|
3007
|
-
agentActivity.delete(fgId);
|
|
3008
|
-
fleet.onAgentFinished(fgId);
|
|
3147
|
+
// Wire session creation: register in the FleetView list + stream to the output file.
|
|
3148
|
+
// The output file path is set synchronously after spawn (below),
|
|
3149
|
+
// before onSessionCreated fires — same pattern as background agents.
|
|
3150
|
+
const origOnSession = fgCallbacks.onSessionCreated;
|
|
3151
|
+
fgCallbacks.onSessionCreated = (session: any) => {
|
|
3152
|
+
origOnSession(session);
|
|
3153
|
+
for (const a of manager.listAgentsMutable()) {
|
|
3154
|
+
if (a.session === session) {
|
|
3155
|
+
fgId = a.id;
|
|
3156
|
+
agentActivity.set(a.id, fgState);
|
|
3157
|
+
fleet.ensureTimer();
|
|
3158
|
+
fleet.update();
|
|
3159
|
+
break;
|
|
3009
3160
|
}
|
|
3010
3161
|
}
|
|
3162
|
+
// Stream conversation to output file (foreground agent logging)
|
|
3163
|
+
if (fgId) {
|
|
3164
|
+
const rec = manager.getRecordMutable(fgId);
|
|
3165
|
+
if (rec?.outputFile) {
|
|
3166
|
+
rec.outputCleanup = streamToOutputFile(
|
|
3167
|
+
session,
|
|
3168
|
+
rec.outputFile,
|
|
3169
|
+
fgId,
|
|
3170
|
+
ctx.cwd,
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
};
|
|
3011
3175
|
|
|
3012
|
-
|
|
3013
|
-
|
|
3176
|
+
const spinnerInterval = setInterval(() => {
|
|
3177
|
+
spinnerFrame++;
|
|
3178
|
+
streamUpdate();
|
|
3179
|
+
}, SPINNER_INTERVAL_MS);
|
|
3014
3180
|
|
|
3015
|
-
|
|
3016
|
-
tokens: tokenText,
|
|
3017
|
-
});
|
|
3181
|
+
streamUpdate();
|
|
3018
3182
|
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3183
|
+
let record: AgentRecord;
|
|
3184
|
+
try {
|
|
3185
|
+
const fgResult = await manager.spawnAndWait(
|
|
3186
|
+
pi,
|
|
3187
|
+
ctx,
|
|
3188
|
+
subagentType,
|
|
3189
|
+
params.prompt,
|
|
3190
|
+
{
|
|
3191
|
+
description: params.description,
|
|
3192
|
+
model: resolvedConfig.agentTierSelected ? undefined : model,
|
|
3193
|
+
maxTurns: effectiveMaxTurns,
|
|
3194
|
+
isolated,
|
|
3195
|
+
inheritContext,
|
|
3196
|
+
thinkingLevel: resolvedConfig.agentTierSelected
|
|
3197
|
+
? undefined
|
|
3198
|
+
: thinking,
|
|
3199
|
+
agentTier: resolvedConfig.requestedAgentTier,
|
|
3200
|
+
isolation,
|
|
3201
|
+
invocation: agentInvocation,
|
|
3202
|
+
signal,
|
|
3203
|
+
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
3204
|
+
...fgCallbacks,
|
|
3205
|
+
},
|
|
3206
|
+
(fgAgentId) => {
|
|
3207
|
+
// onSpawned: called synchronously after spawn, before onSessionCreated fires.
|
|
3208
|
+
// Set up the output file so streamToOutputFile can pick it up.
|
|
3209
|
+
const fgRec = manager.getRecordMutable(fgAgentId);
|
|
3210
|
+
attachTranscript(fgRec, fgAgentId);
|
|
3211
|
+
},
|
|
3212
|
+
);
|
|
3213
|
+
record = fgResult.record;
|
|
3214
|
+
} finally {
|
|
3215
|
+
// Always stop the spinner and drop the row, including startup errors
|
|
3216
|
+
// that now propagate to pi as failed tool calls.
|
|
3217
|
+
clearInterval(spinnerInterval);
|
|
3218
|
+
if (fgId) {
|
|
3219
|
+
agentActivity.delete(fgId);
|
|
3220
|
+
fleet.onAgentFinished(fgId);
|
|
3025
3221
|
}
|
|
3222
|
+
}
|
|
3026
3223
|
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3224
|
+
// Get final token count
|
|
3225
|
+
const tokenText = formatLifetimeTokens(fgState);
|
|
3226
|
+
|
|
3227
|
+
const details = buildDetails(detailBase, record, fgState, {
|
|
3228
|
+
tokens: tokenText,
|
|
3229
|
+
});
|
|
3230
|
+
|
|
3231
|
+
if (record.status === "error") {
|
|
3232
|
+
// Error headline + any partial output the run produced before failing.
|
|
3031
3233
|
return textResult(
|
|
3032
|
-
`${fallbackNote}Agent
|
|
3033
|
-
(record.result?.trim() || "No output."),
|
|
3234
|
+
`${fallbackNote}Agent failed: ${record.error}${partialOutputSuffix(record)}`,
|
|
3034
3235
|
details,
|
|
3035
3236
|
);
|
|
3036
|
-
}
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
3240
|
+
const statsParts = [`${record.toolUses} tool uses`];
|
|
3241
|
+
if (tokenText) statsParts.push(tokenText);
|
|
3242
|
+
return textResult(
|
|
3243
|
+
`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getForegroundOutcomeNote(record.status)}.\n\n` +
|
|
3244
|
+
(record.result?.trim() || "No output."),
|
|
3245
|
+
details,
|
|
3246
|
+
);
|
|
3247
|
+
},
|
|
3037
3248
|
});
|
|
3038
3249
|
pi.registerTool(agentTool);
|
|
3039
3250
|
agentToolRef = agentTool;
|
|
3040
3251
|
|
|
3252
|
+
// Pi snapshots description and parameters when wrapping a registered tool.
|
|
3253
|
+
// Trust changes alone emit no host event. Re-register before each new prompt,
|
|
3254
|
+
// ahead of Pi's effective loadout snapshot, using its public post-bind refresh.
|
|
3255
|
+
// Turn end also covers provider continuations after tools revoke authority.
|
|
3256
|
+
// Keep the same exposure/execution policy and retain the active set.
|
|
3257
|
+
function freshAgentToolDefinition() {
|
|
3258
|
+
reloadCustomAgents();
|
|
3259
|
+
return {
|
|
3260
|
+
...agentTool,
|
|
3261
|
+
description: agentToolDescription(),
|
|
3262
|
+
parameters: {
|
|
3263
|
+
...agentTool.parameters,
|
|
3264
|
+
properties: {
|
|
3265
|
+
...agentTool.parameters.properties,
|
|
3266
|
+
subagent_type: { ...agentTool.parameters.properties.subagent_type, description: agentTypeParameterDescription() },
|
|
3267
|
+
tier: { ...agentTool.parameters.properties.tier, description: buildAgentTierParameterDescription() },
|
|
3268
|
+
},
|
|
3269
|
+
},
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
const refreshAgentDeclaration = () => {
|
|
3273
|
+
const refreshed = freshAgentToolDefinition();
|
|
3274
|
+
const activeTools = pi.getActiveTools();
|
|
3275
|
+
pi.registerTool(refreshed);
|
|
3276
|
+
// Pi's explicit --tools/SDK tools selector can re-activate every matching
|
|
3277
|
+
// name during registry refresh. A metadata refresh must not undo a later
|
|
3278
|
+
// activation/exposure decision by the host or another extension.
|
|
3279
|
+
const refreshedActive = pi.getActiveTools();
|
|
3280
|
+
if (activeTools.length !== refreshedActive.length || activeTools.some((name, i) => name !== refreshedActive[i])) {
|
|
3281
|
+
pi.setActiveTools(activeTools);
|
|
3282
|
+
}
|
|
3283
|
+
agentToolRef = refreshed;
|
|
3284
|
+
};
|
|
3285
|
+
pi.on("before_agent_start", refreshAgentDeclaration);
|
|
3286
|
+
pi.on("turn_end", refreshAgentDeclaration);
|
|
3287
|
+
|
|
3041
3288
|
// ---- get_subagent_result tool ----
|
|
3042
3289
|
|
|
3043
3290
|
pi.registerTool(
|
|
@@ -3154,7 +3401,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3154
3401
|
"Chat with or redirect a running or queued background agent",
|
|
3155
3402
|
parameters: Type.Object({
|
|
3156
3403
|
agent_id: Type.String({
|
|
3157
|
-
description:
|
|
3404
|
+
description:
|
|
3405
|
+
"The agent ID to message (must be currently running or queued).",
|
|
3158
3406
|
}),
|
|
3159
3407
|
message: Type.String({
|
|
3160
3408
|
description:
|
|
@@ -3185,7 +3433,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3185
3433
|
id: record.id,
|
|
3186
3434
|
message: params.message,
|
|
3187
3435
|
});
|
|
3188
|
-
const delivery =
|
|
3436
|
+
const delivery =
|
|
3437
|
+
record.status === "queued"
|
|
3438
|
+
? "when the agent starts"
|
|
3439
|
+
: "once the session initializes";
|
|
3189
3440
|
return textResult(
|
|
3190
3441
|
`Chat message queued for agent ${record.id}. It will be delivered ${delivery}.`,
|
|
3191
3442
|
);
|
|
@@ -3493,7 +3744,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3493
3744
|
`"${sanitizeDisplayText(record.description)}" has not started yet.${handleHint}\n\nStop this queued run?`,
|
|
3494
3745
|
);
|
|
3495
3746
|
if (stop && manager.abort(record.id)) {
|
|
3496
|
-
ctx.ui.notify(
|
|
3747
|
+
ctx.ui.notify(
|
|
3748
|
+
`Stopped queued agent "${sanitizeDisplayText(record.description)}".`,
|
|
3749
|
+
"info",
|
|
3750
|
+
);
|
|
3497
3751
|
}
|
|
3498
3752
|
return;
|
|
3499
3753
|
}
|
|
@@ -4161,7 +4415,7 @@ prompt_mode: <"replace" (body IS the full system prompt) or "append" (body is ap
|
|
|
4161
4415
|
extensions: <true (inherit all MCP/extension tools), false (none), or comma-separated names. Default: true>
|
|
4162
4416
|
skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
|
|
4163
4417
|
disallowed_tools: <comma-separated tool names to block, even if otherwise available. Omit for none>
|
|
4164
|
-
inherit_context: <
|
|
4418
|
+
inherit_context: <omit or false; true is rejected because Pi cannot safely transfer the post-hook parent context>
|
|
4165
4419
|
run_in_background: <true to run in background by default. Default: false>
|
|
4166
4420
|
output_transcript: <false to write no transcript file or path for this agent. Independent of persist_session. Default: true>
|
|
4167
4421
|
isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
|
|
@@ -4177,7 +4431,7 @@ Guidelines for choosing settings:
|
|
|
4177
4431
|
- For code modification tasks: include edit, write
|
|
4178
4432
|
- Use prompt_mode: append if the agent should keep the default system prompt and add specialization on top
|
|
4179
4433
|
- Use prompt_mode: replace for fully custom agents with their own personality/instructions
|
|
4180
|
-
-
|
|
4434
|
+
- Never set inherit_context: true; put an explicitly sanitized summary in the Agent task instead
|
|
4181
4435
|
- Set isolated: true if the agent should NOT have access to MCP servers or other extensions
|
|
4182
4436
|
- Set output_transcript: false to skip writing the agent's transcript; this alone doesn't keep the run off disk (persist_session, isolation: worktree commits, and memory still write) — set those too if that's the goal
|
|
4183
4437
|
- Only include frontmatter fields that differ from defaults — omit fields where the default is fine
|
|
@@ -4648,7 +4902,7 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4648
4902
|
// scoped set would fail the scope check at spawn time.
|
|
4649
4903
|
if (isScopeModelsEnabled()) {
|
|
4650
4904
|
const enabled = resolveEnabledModels(
|
|
4651
|
-
readEnabledModels(sessionCwd),
|
|
4905
|
+
readEnabledModels(sessionCwd, captureProjectTrust(configurationOwner)),
|
|
4652
4906
|
ctx.modelRegistry,
|
|
4653
4907
|
sessionCwd,
|
|
4654
4908
|
);
|
|
@@ -4781,7 +5035,8 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4781
5035
|
id: "defaultTier",
|
|
4782
5036
|
label: "Default tier",
|
|
4783
5037
|
description: (() => {
|
|
4784
|
-
if (tierKeys.length === 0)
|
|
5038
|
+
if (tierKeys.length === 0)
|
|
5039
|
+
return "No tiers defined yet — create one in /agents → Model tiers.";
|
|
4785
5040
|
const fallback = shippedFallbackAgentTier();
|
|
4786
5041
|
// Named from the resolver, not from a constant: on a catalogue that
|
|
4787
5042
|
// removed the shipped profile, "unset" reaches nothing and behaves
|
|
@@ -4796,7 +5051,9 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4796
5051
|
currentValue: (() => {
|
|
4797
5052
|
const selection = getDefaultAgentTierSelection();
|
|
4798
5053
|
if (selection.kind === "tier") return selection.tier;
|
|
4799
|
-
return selection.kind === "none"
|
|
5054
|
+
return selection.kind === "none"
|
|
5055
|
+
? NO_DEFAULT_TIER
|
|
5056
|
+
: UNSET_DEFAULT_TIER;
|
|
4800
5057
|
})(),
|
|
4801
5058
|
values: [UNSET_DEFAULT_TIER, NO_DEFAULT_TIER, ...tierKeys],
|
|
4802
5059
|
},
|
|
@@ -4874,7 +5131,7 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4874
5131
|
id: "agentMentions",
|
|
4875
5132
|
label: "Agent mentions",
|
|
4876
5133
|
description:
|
|
4877
|
-
"How `@handle message` is dispatched: model (an
|
|
5134
|
+
"How `@handle message` is dispatched: model (an isolated mention-only turn rewrites the typed task without copying parent history), direct (start it from the typed text), or off (send the text to the main model verbatim). Messaging and resuming an existing agent are always direct.",
|
|
4878
5135
|
currentValue: getAgentMentionMode(),
|
|
4879
5136
|
values: [...AGENT_MENTION_MODES],
|
|
4880
5137
|
},
|
|
@@ -5055,13 +5312,13 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
5055
5312
|
const enabled = value === "on";
|
|
5056
5313
|
supervisorQuestionsEnabled = enabled;
|
|
5057
5314
|
manager.setSupervisorQuestions(enabled);
|
|
5058
|
-
notifyApplied(ctx, `Supervisor questions ${enabled ? "enabled" : "disabled"}`);
|
|
5059
|
-
} else if (id === "agentMentions") {
|
|
5060
|
-
agentMentionMode = value as AgentMentionMode;
|
|
5061
5315
|
notifyApplied(
|
|
5062
5316
|
ctx,
|
|
5063
|
-
`
|
|
5317
|
+
`Supervisor questions ${enabled ? "enabled" : "disabled"}`,
|
|
5064
5318
|
);
|
|
5319
|
+
} else if (id === "agentMentions") {
|
|
5320
|
+
agentMentionMode = value as AgentMentionMode;
|
|
5321
|
+
notifyApplied(ctx, `Agent mentions set to ${agentMentionMode}`);
|
|
5065
5322
|
}
|
|
5066
5323
|
}
|
|
5067
5324
|
|