@signalridge/pi-subagents 1.10.2 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +21 -10
- package/examples/agent-tool-description.md +1 -1
- package/package.json +7 -6
- package/src/agent-manager.ts +1405 -486
- package/src/agent-runner.ts +486 -254
- 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 +7 -0
- package/src/enabled-models.ts +72 -78
- package/src/index.ts +988 -799
- package/src/mention-clone.ts +129 -83
- package/src/model-runtime-bridge.ts +68 -0
- package/src/model-scope.ts +4 -2
- package/src/nested-tools.ts +3 -1
- 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,
|
|
@@ -227,7 +233,10 @@ import {
|
|
|
227
233
|
getSessionContextPercent,
|
|
228
234
|
type LifetimeUsage,
|
|
229
235
|
} from "./usage.js";
|
|
230
|
-
import {
|
|
236
|
+
import {
|
|
237
|
+
isWorktreeIsolationEnabled,
|
|
238
|
+
setWorktreeIsolationEnabled,
|
|
239
|
+
} from "./worktree.js";
|
|
231
240
|
|
|
232
241
|
// ---- Shared helpers ----
|
|
233
242
|
|
|
@@ -804,8 +813,12 @@ function activateRootRuntime(
|
|
|
804
813
|
durationMs,
|
|
805
814
|
tokens,
|
|
806
815
|
...(record.outputFile ? { outputFile: record.outputFile } : {}),
|
|
807
|
-
...(record.invocation?.agentTier === undefined
|
|
808
|
-
|
|
816
|
+
...(record.invocation?.agentTier === undefined
|
|
817
|
+
? {}
|
|
818
|
+
: { tier: record.invocation.agentTier }),
|
|
819
|
+
...(record.invocation?.agentTierSnapshot
|
|
820
|
+
? { tierSnapshot: record.invocation.agentTierSnapshot }
|
|
821
|
+
: {}),
|
|
809
822
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
810
823
|
};
|
|
811
824
|
}
|
|
@@ -843,7 +856,9 @@ function activateRootRuntime(
|
|
|
843
856
|
? { tokens: { total: terminal.tokenCount } }
|
|
844
857
|
: {}),
|
|
845
858
|
...(tombstone.tier === undefined ? {} : { tier: tombstone.tier }),
|
|
846
|
-
...(tombstone.tierSnapshot
|
|
859
|
+
...(tombstone.tierSnapshot
|
|
860
|
+
? { tierSnapshot: tombstone.tierSnapshot }
|
|
861
|
+
: {}),
|
|
847
862
|
owner: snapshotOwner(tombstone.owner),
|
|
848
863
|
};
|
|
849
864
|
}
|
|
@@ -868,7 +883,9 @@ function activateRootRuntime(
|
|
|
868
883
|
...(terminal.outputFile ? { outputFile: terminal.outputFile } : {}),
|
|
869
884
|
...(terminal.tokenCount ? { tokens: terminal.tokenCount } : {}),
|
|
870
885
|
...(tombstone.tier === undefined ? {} : { tier: tombstone.tier }),
|
|
871
|
-
...(tombstone.tierSnapshot
|
|
886
|
+
...(tombstone.tierSnapshot
|
|
887
|
+
? { tierSnapshot: tombstone.tierSnapshot }
|
|
888
|
+
: {}),
|
|
872
889
|
owner: snapshotOwner(tombstone.owner),
|
|
873
890
|
});
|
|
874
891
|
}
|
|
@@ -934,8 +951,12 @@ function activateRootRuntime(
|
|
|
934
951
|
...(record.lifetimeUsage.input + record.lifetimeUsage.output > 0
|
|
935
952
|
? { tokens: record.lifetimeUsage.input + record.lifetimeUsage.output }
|
|
936
953
|
: {}),
|
|
937
|
-
...(record.invocation?.agentTier === undefined
|
|
938
|
-
|
|
954
|
+
...(record.invocation?.agentTier === undefined
|
|
955
|
+
? {}
|
|
956
|
+
: { tier: record.invocation.agentTier }),
|
|
957
|
+
...(record.invocation?.agentTierSnapshot
|
|
958
|
+
? { tierSnapshot: record.invocation.agentTierSnapshot }
|
|
959
|
+
: {}),
|
|
939
960
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
940
961
|
});
|
|
941
962
|
|
|
@@ -980,7 +1001,9 @@ function activateRootRuntime(
|
|
|
980
1001
|
id: record.id,
|
|
981
1002
|
type: record.type,
|
|
982
1003
|
description: record.description,
|
|
983
|
-
...(record.invocation?.agentTier === undefined
|
|
1004
|
+
...(record.invocation?.agentTier === undefined
|
|
1005
|
+
? {}
|
|
1006
|
+
: { tier: record.invocation.agentTier }),
|
|
984
1007
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
985
1008
|
});
|
|
986
1009
|
},
|
|
@@ -994,8 +1017,12 @@ function activateRootRuntime(
|
|
|
994
1017
|
reason: info.reason,
|
|
995
1018
|
tokensBefore: info.tokensBefore,
|
|
996
1019
|
compactionCount: record.compactionCount,
|
|
997
|
-
...(record.invocation?.agentTier === undefined
|
|
998
|
-
|
|
1020
|
+
...(record.invocation?.agentTier === undefined
|
|
1021
|
+
? {}
|
|
1022
|
+
: { tier: record.invocation.agentTier }),
|
|
1023
|
+
...(record.invocation?.agentTierSnapshot
|
|
1024
|
+
? { tierSnapshot: record.invocation.agentTierSnapshot }
|
|
1025
|
+
: {}),
|
|
999
1026
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
1000
1027
|
});
|
|
1001
1028
|
},
|
|
@@ -1009,7 +1036,9 @@ function activateRootRuntime(
|
|
|
1009
1036
|
type: record.type,
|
|
1010
1037
|
description: record.description,
|
|
1011
1038
|
isBackground: record.isBackground,
|
|
1012
|
-
...(record.invocation?.agentTier === undefined
|
|
1039
|
+
...(record.invocation?.agentTier === undefined
|
|
1040
|
+
? {}
|
|
1041
|
+
: { tier: record.invocation.agentTier }),
|
|
1013
1042
|
owner: snapshotOwner(record.owner),
|
|
1014
1043
|
});
|
|
1015
1044
|
},
|
|
@@ -1067,8 +1096,7 @@ function activateRootRuntime(
|
|
|
1067
1096
|
const globalRegistry = globalThis as any;
|
|
1068
1097
|
const predecessor =
|
|
1069
1098
|
(globalRegistry[MANAGER_TAKEOVER_LOCK_KEY] as
|
|
1070
|
-
|
|
1071
|
-
| undefined) ?? Promise.resolve();
|
|
1099
|
+
Promise<void> | undefined) ?? Promise.resolve();
|
|
1072
1100
|
let release!: () => void;
|
|
1073
1101
|
const gate = new Promise<void>((resolve) => {
|
|
1074
1102
|
release = resolve;
|
|
@@ -1152,6 +1180,33 @@ function activateRootRuntime(
|
|
|
1152
1180
|
|
|
1153
1181
|
// --- Cross-extension RPC via pi.events ---
|
|
1154
1182
|
let currentCtx: ExtensionContext | undefined;
|
|
1183
|
+
// A session ID alone cannot distinguish a branch change or a resume of the
|
|
1184
|
+
// same session. Invalidate pending off-screen mention turns only when the
|
|
1185
|
+
// replacement commits: before-switch/tree handlers can still cancel it.
|
|
1186
|
+
let sessionGeneration = 0;
|
|
1187
|
+
// Public synchronous bus preflight works in either extension load order. A
|
|
1188
|
+
// filtered-out activation has no bound context and must not veto navigation.
|
|
1189
|
+
pi.events.on("pi:navigation-preflight", (raw: unknown) => {
|
|
1190
|
+
if (!currentCtx || !manager.hasUnsettledWork()) return;
|
|
1191
|
+
const report = (raw as { report?: unknown } | null)?.report;
|
|
1192
|
+
if (typeof report === "function") report("subagents");
|
|
1193
|
+
});
|
|
1194
|
+
const navigationPreflight = (
|
|
1195
|
+
ctx: ExtensionContext,
|
|
1196
|
+
): { cancel: true } | undefined => {
|
|
1197
|
+
let busy = false;
|
|
1198
|
+
pi.events.emit("pi:navigation-preflight", {
|
|
1199
|
+
report: () => {
|
|
1200
|
+
busy = true;
|
|
1201
|
+
},
|
|
1202
|
+
});
|
|
1203
|
+
if (!busy) return undefined;
|
|
1204
|
+
const notice =
|
|
1205
|
+
"Navigation blocked while agents or workflows are active or cleaning up. Wait for them to finish, or stop them explicitly and retry.";
|
|
1206
|
+
if (ctx.hasUI) ctx.ui.notify(notice, "warning");
|
|
1207
|
+
else console.warn(`[pi-subagents] ${notice}`);
|
|
1208
|
+
return { cancel: true };
|
|
1209
|
+
};
|
|
1155
1210
|
// RPC handlers + the `subagents:ready` broadcast are wired on `session_start`
|
|
1156
1211
|
// (a bound lifecycle event), not at factory time. pi runs every extension
|
|
1157
1212
|
// factory before the `extensions:` filter and only fires lifecycle events for
|
|
@@ -1192,6 +1247,14 @@ function activateRootRuntime(
|
|
|
1192
1247
|
event: { reason?: string },
|
|
1193
1248
|
ctx: ExtensionContext,
|
|
1194
1249
|
): Promise<void> {
|
|
1250
|
+
// A successful session_start is the commit point for a reused runtime. A
|
|
1251
|
+
// cancelled session_before_switch leaves the old context and scheduler live.
|
|
1252
|
+
if (currentCtx) {
|
|
1253
|
+
scheduler.stop();
|
|
1254
|
+
manager.detachForBranchChange();
|
|
1255
|
+
manager.resetManagedSpawns();
|
|
1256
|
+
}
|
|
1257
|
+
sessionGeneration++;
|
|
1195
1258
|
sessionCwd = ctx.cwd;
|
|
1196
1259
|
boundSessionId = ctx.sessionManager?.getSessionId?.();
|
|
1197
1260
|
currentCtx = ctx;
|
|
@@ -1233,7 +1296,8 @@ function activateRootRuntime(
|
|
|
1233
1296
|
abortOwned: (id, owner) => manager.abortOwned(id, owner),
|
|
1234
1297
|
quiesceOwned: (runId, agentIds, timeoutMs, owners) =>
|
|
1235
1298
|
manager.quiesceOwned(runId, agentIds, timeoutMs, owners),
|
|
1236
|
-
reconcileManaged: (spawnKey, owner) =>
|
|
1299
|
+
reconcileManaged: (spawnKey, owner) =>
|
|
1300
|
+
manager.reconcileManaged(spawnKey, owner),
|
|
1237
1301
|
// The live pool size, not a snapshot: `/subagents` can change it
|
|
1238
1302
|
// mid-session, and a peer that sizes its fan-out from this must see
|
|
1239
1303
|
// the value that is actually throttling it now.
|
|
@@ -1297,38 +1361,32 @@ function activateRootRuntime(
|
|
|
1297
1361
|
startScheduler(ctx);
|
|
1298
1362
|
}
|
|
1299
1363
|
|
|
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
|
-
}
|
|
1364
|
+
// Both before-events are cancellable. A later handler may veto, and tree
|
|
1365
|
+
// summarization can still abort after every handler has returned. Never stop
|
|
1366
|
+
// live work on an attempted navigation.
|
|
1367
|
+
pi.on("session_before_switch", (_event, ctx) => navigationPreflight(ctx));
|
|
1368
|
+
pi.on("session_before_tree", (_event, ctx) => navigationPreflight(ctx));
|
|
1369
|
+
let detachedTreeEvent: unknown;
|
|
1370
|
+
const detachCommittedTree = (event: unknown): void => {
|
|
1371
|
+
if (!currentCtx || detachedTreeEvent === event) return;
|
|
1372
|
+
detachedTreeEvent = event;
|
|
1373
|
+
manager.detachForBranchChange();
|
|
1374
|
+
};
|
|
1375
|
+
// If workflows' committed-tree handler runs first, it must detach our old
|
|
1376
|
+
// records before aborting its controller: abort listeners can synchronously
|
|
1377
|
+
// issue stop-owned RPC and otherwise append a managed tombstone on the new leaf.
|
|
1378
|
+
pi.events.on("pi-workflows:session_tree_committed", (raw: unknown) => {
|
|
1379
|
+
const event = (raw as { event?: unknown } | null)?.event;
|
|
1380
|
+
if (event) detachCommittedTree(event);
|
|
1327
1381
|
});
|
|
1328
|
-
pi.on("session_tree", (
|
|
1382
|
+
pi.on("session_tree", (event, ctx) => {
|
|
1383
|
+
sessionGeneration++;
|
|
1384
|
+
detachCommittedTree(event);
|
|
1385
|
+
// Pi has already changed the leaf. Fence workflow listeners synchronously
|
|
1386
|
+
// even when subagents' session_tree handler runs first.
|
|
1387
|
+
pi.events.emit("subagents:session_tree_committed", { event });
|
|
1329
1388
|
currentCtx = ctx;
|
|
1330
|
-
|
|
1331
|
-
manager.detachForBranchChange();
|
|
1389
|
+
scheduler.stop();
|
|
1332
1390
|
manager.clearCompleted(true);
|
|
1333
1391
|
manager.resetManagedSpawns();
|
|
1334
1392
|
const entries =
|
|
@@ -1350,6 +1408,52 @@ function activateRootRuntime(
|
|
|
1350
1408
|
let runtimeShutdown: Promise<void> | undefined;
|
|
1351
1409
|
const shutdownRuntime = (): Promise<void> => {
|
|
1352
1410
|
runtimeShutdown ??= (async () => {
|
|
1411
|
+
// session_shutdown is confirmed, unlike either before-event. Start the
|
|
1412
|
+
// workflow's owned-agent cleanup while its RPC responder and the old leaf
|
|
1413
|
+
// are still available, regardless of which extension loads first.
|
|
1414
|
+
const pending: Array<
|
|
1415
|
+
Promise<{ settled: boolean; pending: string[]; diagnostic?: string }>
|
|
1416
|
+
> = [];
|
|
1417
|
+
pi.events.emit("pi-workflows:shutdown-quiesce", {
|
|
1418
|
+
respond: (
|
|
1419
|
+
operation: Promise<{
|
|
1420
|
+
settled: boolean;
|
|
1421
|
+
pending: string[];
|
|
1422
|
+
diagnostic?: string;
|
|
1423
|
+
}>,
|
|
1424
|
+
) => {
|
|
1425
|
+
pending.push(operation);
|
|
1426
|
+
},
|
|
1427
|
+
});
|
|
1428
|
+
scheduler.stop();
|
|
1429
|
+
currentCtx = undefined; // reject new spawns; owned cleanup RPC remains bound
|
|
1430
|
+
if (pending.length > 0) {
|
|
1431
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1432
|
+
const results = await Promise.race([
|
|
1433
|
+
Promise.allSettled(pending),
|
|
1434
|
+
new Promise<undefined>((resolve) => {
|
|
1435
|
+
timer = setTimeout(() => resolve(undefined), 6_000);
|
|
1436
|
+
}),
|
|
1437
|
+
]);
|
|
1438
|
+
if (timer) clearTimeout(timer);
|
|
1439
|
+
if (
|
|
1440
|
+
!results ||
|
|
1441
|
+
results.some(
|
|
1442
|
+
(result) => result.status === "rejected" || !result.value.settled,
|
|
1443
|
+
)
|
|
1444
|
+
) {
|
|
1445
|
+
console.warn(
|
|
1446
|
+
"[pi-subagents] workflow shutdown quiescence timed out or failed; pending work will be quarantined",
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
const quiesced = await manager.quiesceAll(5_000);
|
|
1451
|
+
if (!quiesced.settled) {
|
|
1452
|
+
console.warn(
|
|
1453
|
+
`[pi-subagents] shutdown quiescence timed out; quarantined: ${quiesced.pending.join(", ")}`,
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
sessionGeneration++;
|
|
1353
1457
|
unsubscribeRootContext();
|
|
1354
1458
|
rpcHandle?.unsubSpawn();
|
|
1355
1459
|
rpcHandle?.unsubSpawnManaged();
|
|
@@ -1474,7 +1578,11 @@ function activateRootRuntime(
|
|
|
1474
1578
|
const effectiveTier = (() => {
|
|
1475
1579
|
try {
|
|
1476
1580
|
return selectAgentTier(
|
|
1477
|
-
{
|
|
1581
|
+
{
|
|
1582
|
+
requestedTier: request.tier,
|
|
1583
|
+
requireTier: true,
|
|
1584
|
+
agentConfig: customConfig,
|
|
1585
|
+
},
|
|
1478
1586
|
agentTiers,
|
|
1479
1587
|
)?.tier;
|
|
1480
1588
|
} catch {
|
|
@@ -1496,7 +1604,12 @@ function activateRootRuntime(
|
|
|
1496
1604
|
resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
|
|
1497
1605
|
);
|
|
1498
1606
|
const effectiveIsolation = request.isolation ?? resolvedConfig.isolation;
|
|
1499
|
-
const effectiveExcludeTools = [
|
|
1607
|
+
const effectiveExcludeTools = [
|
|
1608
|
+
...new Set([
|
|
1609
|
+
...(customConfig?.disallowedTools ?? []),
|
|
1610
|
+
...(request.excludeTools ?? []),
|
|
1611
|
+
]),
|
|
1612
|
+
];
|
|
1500
1613
|
const managedPolicy: ManagedSpawnPolicy = {
|
|
1501
1614
|
maxTurns: effectiveMaxTurns,
|
|
1502
1615
|
isolated: resolvedConfig.isolated,
|
|
@@ -1963,18 +2076,25 @@ function activateRootRuntime(
|
|
|
1963
2076
|
}
|
|
1964
2077
|
|
|
1965
2078
|
if (record.session) {
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2079
|
+
try {
|
|
2080
|
+
const resumed = await manager.resume(
|
|
2081
|
+
record.id,
|
|
2082
|
+
mention.message,
|
|
2083
|
+
undefined,
|
|
2084
|
+
{ isBackground: true },
|
|
2085
|
+
);
|
|
2086
|
+
ctx.ui.notify(
|
|
2087
|
+
resumed
|
|
2088
|
+
? `Resuming ${target}`
|
|
2089
|
+
: `Could not resume ${target} — it is still running.`,
|
|
2090
|
+
resumed ? "info" : "warning",
|
|
2091
|
+
);
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
ctx.ui.notify(
|
|
2094
|
+
`Could not resume ${target}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2095
|
+
"warning",
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
1978
2098
|
return { action: "handled" };
|
|
1979
2099
|
}
|
|
1980
2100
|
// A live record with no session never got far enough to continue, so it
|
|
@@ -2053,37 +2173,82 @@ function activateRootRuntime(
|
|
|
2053
2173
|
if (!type) return { action: "continue" };
|
|
2054
2174
|
|
|
2055
2175
|
const label = `@${handleBase(type)}`;
|
|
2176
|
+
const originGeneration = sessionGeneration;
|
|
2177
|
+
const originSessionId = ctx.sessionManager.getSessionId();
|
|
2178
|
+
const isOriginCurrent = (): boolean =>
|
|
2179
|
+
currentCtx !== undefined &&
|
|
2180
|
+
sessionGeneration === originGeneration &&
|
|
2181
|
+
boundSessionId === originSessionId &&
|
|
2182
|
+
ctx.sessionManager.getSessionId() === originSessionId;
|
|
2056
2183
|
const startDirectly = (): void => {
|
|
2057
|
-
|
|
2184
|
+
// The hidden model turn may outlive a change to agent files or settings.
|
|
2185
|
+
// Revalidate the handle rather than bypassing Agent's dispatch policy
|
|
2186
|
+
// with a type that has since been disabled or removed.
|
|
2187
|
+
reloadCustomAgents();
|
|
2188
|
+
const current = resolveSpawnType(type);
|
|
2189
|
+
if (
|
|
2190
|
+
!current.ok ||
|
|
2191
|
+
current.fellBackFrom !== undefined ||
|
|
2192
|
+
current.type !== type
|
|
2193
|
+
) {
|
|
2194
|
+
throw new Error(`Agent type "${type}" is no longer available`);
|
|
2195
|
+
}
|
|
2196
|
+
spawnMention(ctx, current.type, mention.message, {
|
|
2058
2197
|
description: describeMention(mention.message),
|
|
2059
2198
|
});
|
|
2060
2199
|
};
|
|
2061
2200
|
|
|
2062
|
-
// In `model` mode
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2065
|
-
// chat, and what it starts is an ordinary top-level agent.
|
|
2201
|
+
// In `model` mode an off-screen mention-only turn rewrites the typed task
|
|
2202
|
+
// without copying parent history or its request-local context hooks.
|
|
2203
|
+
// Nothing reaches the chat, and it starts an ordinary top-level agent.
|
|
2066
2204
|
const registeredAgentTool = agentToolRef;
|
|
2067
2205
|
if (getAgentMentionMode() === "model" && registeredAgentTool) {
|
|
2206
|
+
// The hidden turn is itself a provider call. Refuse before saying
|
|
2207
|
+
// "Starting" or spending that call when its inherited parent model is
|
|
2208
|
+
// unavailable. A shape-only check would let the hidden clone refuse, then
|
|
2209
|
+
// let its direct fallback allocate an ID and announce an agent that fails.
|
|
2210
|
+
try {
|
|
2211
|
+
parentModelSessionOptions(ctx, ctx.model);
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
ctx.ui.notify(
|
|
2214
|
+
`Could not start ${label}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2215
|
+
"error",
|
|
2216
|
+
);
|
|
2217
|
+
return { action: "handled" };
|
|
2218
|
+
}
|
|
2068
2219
|
ctx.ui.notify(`Starting ${label}…`, "info");
|
|
2069
2220
|
// Not awaited: the clone runs a full model turn and `prompt()` is blocked
|
|
2070
2221
|
// 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
|
-
|
|
2222
|
+
void runMentionClone({
|
|
2223
|
+
ctx,
|
|
2224
|
+
type,
|
|
2225
|
+
message: mention.message,
|
|
2226
|
+
agentTool: registeredAgentTool,
|
|
2227
|
+
isOriginCurrent,
|
|
2228
|
+
}).then((result) => {
|
|
2229
|
+
if (result.spawned || !isOriginCurrent()) return;
|
|
2230
|
+
if (result.refused) {
|
|
2231
|
+
ctx.ui.notify(
|
|
2232
|
+
`Could not start ${label} — ${result.error}`,
|
|
2233
|
+
"warning",
|
|
2234
|
+
);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
// A clone that could not run must not swallow the mention: start the
|
|
2238
|
+
// agent the direct way rather than leaving a toast and nothing running.
|
|
2239
|
+
try {
|
|
2240
|
+
startDirectly();
|
|
2241
|
+
ctx.ui.notify(
|
|
2242
|
+
`Started ${label} directly — ${result.error}`,
|
|
2243
|
+
"warning",
|
|
2244
|
+
);
|
|
2245
|
+
} catch (err) {
|
|
2246
|
+
ctx.ui.notify(
|
|
2247
|
+
`Could not start ${label}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2248
|
+
"error",
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2087
2252
|
return { action: "handled" };
|
|
2088
2253
|
}
|
|
2089
2254
|
|
|
@@ -2146,7 +2311,8 @@ Notes:
|
|
|
2146
2311
|
- 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
2312
|
- The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
|
|
2148
2313
|
- 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
|
|
2314
|
+
- isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.
|
|
2315
|
+
- Raw inherit_context is unavailable; put only an explicitly sanitized summary in the task prompt.`;
|
|
2150
2316
|
|
|
2151
2317
|
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
2318
|
|
|
@@ -2174,7 +2340,7 @@ If the target is already known, use a direct tool — \`read\` for a known path,
|
|
|
2174
2340
|
- 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
2341
|
- 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
2342
|
- 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
|
-
-
|
|
2343
|
+
- 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
2344
|
- 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
2345
|
|
|
2180
2346
|
## Writing the prompt
|
|
@@ -2253,787 +2419,800 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2253
2419
|
// its handler closes over this activation, which is what makes a clone-driven
|
|
2254
2420
|
// spawn an ordinary top-level agent rather than something the fork owns.
|
|
2255
2421
|
const agentTool = defineTool({
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2422
|
+
name: SUBAGENT_TOOL_NAMES.AGENT,
|
|
2423
|
+
label: "Agent",
|
|
2424
|
+
description: agentToolDescription,
|
|
2425
|
+
promptSnippet: "Launch autonomous sub-agents for complex multi-step tasks",
|
|
2426
|
+
promptGuidelines: [
|
|
2427
|
+
"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.",
|
|
2428
|
+
"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.",
|
|
2429
|
+
"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.",
|
|
2430
|
+
"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.",
|
|
2431
|
+
],
|
|
2432
|
+
parameters: Type.Object({
|
|
2433
|
+
prompt: Type.String({
|
|
2434
|
+
description: "The task for the agent to perform.",
|
|
2435
|
+
}),
|
|
2436
|
+
description: Type.String({
|
|
2437
|
+
description:
|
|
2438
|
+
"A short (3-5 word) description of the task (shown in UI).",
|
|
2439
|
+
}),
|
|
2440
|
+
subagent_type: Type.String({
|
|
2441
|
+
description: `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.`,
|
|
2442
|
+
}),
|
|
2443
|
+
tier: Type.Optional(
|
|
2444
|
+
Type.String({
|
|
2445
|
+
description: buildAgentTierParameterDescription(),
|
|
2270
2446
|
}),
|
|
2271
|
-
|
|
2447
|
+
),
|
|
2448
|
+
max_turns: Type.Optional(
|
|
2449
|
+
Type.Number({
|
|
2272
2450
|
description:
|
|
2273
|
-
"
|
|
2451
|
+
"Maximum number of agentic turns before stopping. Omit for unlimited (default).",
|
|
2452
|
+
minimum: 1,
|
|
2274
2453
|
}),
|
|
2275
|
-
|
|
2276
|
-
|
|
2454
|
+
),
|
|
2455
|
+
run_in_background: Type.Optional(
|
|
2456
|
+
Type.Boolean({
|
|
2457
|
+
description:
|
|
2458
|
+
"Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
|
|
2277
2459
|
}),
|
|
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
|
-
|
|
2460
|
+
),
|
|
2461
|
+
resume: Type.Optional(
|
|
2462
|
+
Type.String({
|
|
2463
|
+
description:
|
|
2464
|
+
"Optional agent ID to resume from. Continues from previous context.",
|
|
2465
|
+
}),
|
|
2466
|
+
),
|
|
2467
|
+
isolated: Type.Optional(
|
|
2468
|
+
Type.Boolean({
|
|
2469
|
+
description:
|
|
2470
|
+
"If true, agent gets no extension/MCP tools — only built-in tools.",
|
|
2471
|
+
}),
|
|
2472
|
+
),
|
|
2473
|
+
inherit_context: Type.Optional(
|
|
2474
|
+
Type.Boolean({
|
|
2475
|
+
description:
|
|
2476
|
+
"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.",
|
|
2477
|
+
}),
|
|
2478
|
+
),
|
|
2479
|
+
isolation: Type.Optional(
|
|
2480
|
+
Type.Union(
|
|
2481
|
+
[
|
|
2482
|
+
Type.Literal("worktree", {
|
|
2483
|
+
description:
|
|
2484
|
+
"Run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.",
|
|
2485
|
+
}),
|
|
2486
|
+
Type.Literal("off", {
|
|
2487
|
+
description:
|
|
2488
|
+
"Explicitly disable worktree isolation for this agent.",
|
|
2489
|
+
}),
|
|
2490
|
+
],
|
|
2491
|
+
{
|
|
2310
2492
|
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
|
-
),
|
|
2493
|
+
'Isolation mode: "worktree" for isolated git worktree, "off" to explicitly disable.',
|
|
2494
|
+
},
|
|
2329
2495
|
),
|
|
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
|
-
}
|
|
2496
|
+
),
|
|
2497
|
+
...scheduleParam,
|
|
2498
|
+
}),
|
|
2361
2499
|
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2500
|
+
// ---- Custom rendering: Claude Code style ----
|
|
2501
|
+
|
|
2502
|
+
renderCall(args, theme) {
|
|
2503
|
+
const displayName = args.subagent_type
|
|
2504
|
+
? getDisplayName(args.subagent_type)
|
|
2505
|
+
: "Agent";
|
|
2506
|
+
const desc = sanitizeDisplayText(args.description ?? "");
|
|
2507
|
+
const text = [
|
|
2508
|
+
theme.fg("toolTitle", theme.bold(displayName)),
|
|
2509
|
+
desc ? theme.fg("muted", desc) : undefined,
|
|
2510
|
+
]
|
|
2511
|
+
.filter((part): part is string => part !== undefined)
|
|
2512
|
+
.join(theme.fg("dim", " · "));
|
|
2513
|
+
return new Text(text, 0, 0);
|
|
2514
|
+
},
|
|
2375
2515
|
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2516
|
+
renderResult(result, { expanded, isPartial }, theme, renderContext) {
|
|
2517
|
+
// Everything below draws child-derived text into the parent transcript
|
|
2518
|
+
// through pi-tui's ANSI-preserving renderer, so it is scrubbed on the way in.
|
|
2519
|
+
const text = safeTerminalText(
|
|
2520
|
+
result.content[0]?.type === "text" ? result.content[0].text : "",
|
|
2521
|
+
);
|
|
2522
|
+
const details = result.details as AgentDetails | undefined;
|
|
2523
|
+
// Pre-execution failures have no agent status. Preserve pi's error text
|
|
2524
|
+
// instead of rendering them as an invented subagent failure.
|
|
2525
|
+
if (renderContext?.isError || !details?.status) {
|
|
2526
|
+
return new Text(text, 0, 0);
|
|
2527
|
+
}
|
|
2387
2528
|
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
details.activity ?? "Thinking...",
|
|
2395
|
-
theme,
|
|
2396
|
-
);
|
|
2529
|
+
const stats = (d: AgentDetails) => {
|
|
2530
|
+
const parts: string[] = [];
|
|
2531
|
+
if (d.modelName) parts.push(d.modelName);
|
|
2532
|
+
if (d.tags) parts.push(...d.tags);
|
|
2533
|
+
if (d.turnCount != null && d.turnCount > 0) {
|
|
2534
|
+
parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
2397
2535
|
}
|
|
2536
|
+
if (d.toolUses > 0) parts.push(`tools ${d.toolUses}`);
|
|
2537
|
+
if (d.tokens) parts.push(d.tokens);
|
|
2538
|
+
return parts
|
|
2539
|
+
.map((p) => fgPreservingNestedStyles(theme, "dim", p))
|
|
2540
|
+
.join(theme.fg("dim", " · "));
|
|
2541
|
+
};
|
|
2398
2542
|
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2543
|
+
if (details.status === "queued") {
|
|
2544
|
+
const id = details.agentId ? ` · ID ${details.agentId}` : "";
|
|
2545
|
+
return new Text(
|
|
2546
|
+
theme.fg(
|
|
2547
|
+
"dim",
|
|
2548
|
+
`${getAgentStatusMark("queued")} queued · background${id}`,
|
|
2549
|
+
),
|
|
2550
|
+
0,
|
|
2551
|
+
0,
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2410
2554
|
|
|
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
|
-
}
|
|
2555
|
+
if (isPartial || details.status === "running") {
|
|
2556
|
+
const frame = SPINNER[details.spinnerFrame ?? 0] ?? SPINNER[0]!;
|
|
2557
|
+
const s = stats(details);
|
|
2558
|
+
return renderRunningAgentStatus(
|
|
2559
|
+
frame,
|
|
2560
|
+
s,
|
|
2561
|
+
details.activity ?? "Thinking...",
|
|
2562
|
+
theme,
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2449
2565
|
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2566
|
+
if (details.status === "background") {
|
|
2567
|
+
const id = details.agentId ? ` · ID ${details.agentId}` : "";
|
|
2568
|
+
return new Text(
|
|
2569
|
+
theme.fg(
|
|
2453
2570
|
"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
|
-
}
|
|
2571
|
+
`${getAgentStatusMark("running")} running · background${id}`,
|
|
2572
|
+
),
|
|
2573
|
+
0,
|
|
2574
|
+
0,
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2466
2577
|
|
|
2578
|
+
if (details.status === "completed" || details.status === "steered") {
|
|
2579
|
+
const duration = formatMs(details.durationMs);
|
|
2580
|
+
const isSteered = details.status === "steered";
|
|
2581
|
+
const statusText = isSteered ? "wrapped up · turn limit" : "completed";
|
|
2582
|
+
const statusColor = isSteered ? "warning" : "dim";
|
|
2467
2583
|
const s = stats(details);
|
|
2468
|
-
const isError = details.status === "error";
|
|
2469
2584
|
let line = theme.fg(
|
|
2470
|
-
|
|
2471
|
-
`${getAgentStatusMark(details.status)} ${
|
|
2585
|
+
statusColor,
|
|
2586
|
+
`${getAgentStatusMark(details.status)} ${statusText}`,
|
|
2472
2587
|
);
|
|
2473
2588
|
if (s) line += theme.fg("dim", " · ") + s;
|
|
2589
|
+
line += theme.fg("dim", " · ") + theme.fg("dim", duration);
|
|
2474
2590
|
|
|
2475
|
-
if (
|
|
2476
|
-
|
|
2477
|
-
"\n"
|
|
2478
|
-
|
|
2479
|
-
"
|
|
2480
|
-
|
|
2481
|
-
)
|
|
2591
|
+
if (expanded) {
|
|
2592
|
+
if (text) {
|
|
2593
|
+
const lines = text.split("\n").slice(0, 50);
|
|
2594
|
+
for (const l of lines) {
|
|
2595
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
2596
|
+
}
|
|
2597
|
+
if (text.split("\n").length > 50) {
|
|
2598
|
+
line +=
|
|
2599
|
+
"\n" +
|
|
2600
|
+
theme.fg(
|
|
2601
|
+
"muted",
|
|
2602
|
+
" ... (use get_subagent_result with verbose for full output)",
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2482
2606
|
} else {
|
|
2483
|
-
|
|
2607
|
+
const doneText = isSteered ? "Wrapped up at the turn limit" : "Done";
|
|
2608
|
+
line += "\n" + theme.fg("dim", ` ${doneText}`);
|
|
2484
2609
|
}
|
|
2610
|
+
return new Text(line, 0, 0);
|
|
2611
|
+
}
|
|
2485
2612
|
|
|
2613
|
+
if (details.status === "stopped") {
|
|
2614
|
+
const s = stats(details);
|
|
2615
|
+
let line = theme.fg("dim", `${getAgentStatusMark("stopped")} stopped`);
|
|
2616
|
+
if (s) line += theme.fg("dim", " · ") + s;
|
|
2617
|
+
line += "\n" + theme.fg("dim", " Stopped before completion");
|
|
2486
2618
|
return new Text(line, 0, 0);
|
|
2487
|
-
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// Keep unknown/future statuses from falling through to the turn-limit
|
|
2622
|
+
// renderer, which is only valid for explicit error/aborted outcomes.
|
|
2623
|
+
if (details.status !== "error" && details.status !== "aborted") {
|
|
2624
|
+
return new Text(text, 0, 0);
|
|
2625
|
+
}
|
|
2488
2626
|
|
|
2489
|
-
|
|
2627
|
+
const s = stats(details);
|
|
2628
|
+
const isError = details.status === "error";
|
|
2629
|
+
let line = theme.fg(
|
|
2630
|
+
isError ? "error" : "warning",
|
|
2631
|
+
`${getAgentStatusMark(details.status)} ${isError ? "failed" : "aborted"}`,
|
|
2632
|
+
);
|
|
2633
|
+
if (s) line += theme.fg("dim", " · ") + s;
|
|
2490
2634
|
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2635
|
+
if (isError) {
|
|
2636
|
+
line +=
|
|
2637
|
+
"\n" +
|
|
2638
|
+
theme.fg(
|
|
2639
|
+
"error",
|
|
2640
|
+
` Error: ${sanitizeDisplayText(details.error ?? "unknown")}`,
|
|
2641
|
+
);
|
|
2642
|
+
} else {
|
|
2643
|
+
line += "\n" + theme.fg("warning", " Aborted at the turn limit");
|
|
2644
|
+
}
|
|
2494
2645
|
|
|
2495
|
-
|
|
2496
|
-
|
|
2646
|
+
return new Text(line, 0, 0);
|
|
2647
|
+
},
|
|
2497
2648
|
|
|
2498
|
-
|
|
2499
|
-
// Single decision point for dispatch (#183): unknown, disabled and
|
|
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
|
-
});
|
|
2649
|
+
// ---- Execute ----
|
|
2535
2650
|
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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
|
-
}
|
|
2651
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
2652
|
+
// Ensure we have UI context for the FleetView list
|
|
2653
|
+
seatFleet(ctx);
|
|
2554
2654
|
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2655
|
+
// Reload custom agents so new project/global .md files are picked up without restart
|
|
2656
|
+
reloadCustomAgents();
|
|
2657
|
+
|
|
2658
|
+
const rawType = params.subagent_type as SubagentType;
|
|
2659
|
+
// Single decision point for dispatch (#183): unknown, disabled and
|
|
2660
|
+
// case-ambiguous types are refused here, BEFORE anything spawns, so a
|
|
2661
|
+
// background or scheduled call can't start running the wrong agent while
|
|
2662
|
+
// the caller is still unaware. `fallbackSubagent` decides whether an
|
|
2663
|
+
// unresolvable type falls back or fails closed.
|
|
2664
|
+
const dispatch = resolveSpawnType(rawType);
|
|
2665
|
+
// `resume` replays a stored session and ignores `subagent_type` entirely,
|
|
2666
|
+
// but the parameter is required by the schema — so gating it here would
|
|
2667
|
+
// make a live agent unresumable the moment its type is deleted, disabled,
|
|
2668
|
+
// or gains a case-clashing sibling. Only a real spawn is gated.
|
|
2669
|
+
if (!dispatch.ok && !params.resume) return textResult(dispatch.message);
|
|
2670
|
+
const subagentType = dispatch.ok ? dispatch.type : rawType;
|
|
2671
|
+
// What the caller actually asked for, named once: `fellBackFrom` is "" for
|
|
2672
|
+
// a blank request, so reading it inline invites the `??`-vs-`||` slip that
|
|
2673
|
+
// once persisted an empty type into a scheduled job.
|
|
2674
|
+
const requestedType =
|
|
2675
|
+
(dispatch.ok && dispatch.fellBackFrom) || subagentType;
|
|
2676
|
+
// Computed at resolution rather than after the run, so the background and
|
|
2677
|
+
// schedule branches carry it too — previously it existed only on the
|
|
2678
|
+
// foreground path. Resume deliberately doesn't: it replays the stored
|
|
2679
|
+
// session and ignores `subagent_type` entirely, so a note about type
|
|
2680
|
+
// substitution would be describing something that didn't happen.
|
|
2681
|
+
const fallbackNote =
|
|
2682
|
+
dispatch.ok && dispatch.fellBackFrom !== undefined
|
|
2683
|
+
? `Note: Unknown agent type "${dispatch.fellBackFrom}" — using ${resolveType(subagentType) ? subagentType : "the fallback agent config"}.\n\n`
|
|
2684
|
+
: "";
|
|
2685
|
+
|
|
2686
|
+
const displayName = getDisplayName(subagentType);
|
|
2687
|
+
|
|
2688
|
+
// Get agent config (if any)
|
|
2689
|
+
const customConfig = getAgentConfig(subagentType);
|
|
2690
|
+
|
|
2691
|
+
const resolvedConfig = resolveAgentInvocationConfig(customConfig, {
|
|
2692
|
+
...params,
|
|
2693
|
+
agentTiers: getAgentTiersSettings(),
|
|
2694
|
+
});
|
|
2695
|
+
if (!params.resume && !params.schedule && resolvedConfig.inheritContext) {
|
|
2696
|
+
return textResult(INHERIT_CONTEXT_UNAVAILABLE);
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
// A selected Agent tier owns final model/thinking resolution. Keep both
|
|
2700
|
+
// fields unset here so runAgent is the only resolver and ordinary Agent
|
|
2701
|
+
// calls cannot accidentally bypass the profile with a parent/default pin.
|
|
2702
|
+
let model = resolvedConfig.agentTierSelected
|
|
2703
|
+
? undefined
|
|
2704
|
+
: (resolveConfiguredDefaultModel(ctx.modelRegistry) ?? ctx.model);
|
|
2705
|
+
if (!resolvedConfig.agentTierSelected && resolvedConfig.modelInput) {
|
|
2706
|
+
const resolved = resolveModel(
|
|
2707
|
+
resolvedConfig.modelInput,
|
|
2708
|
+
ctx.modelRegistry,
|
|
2709
|
+
);
|
|
2710
|
+
if (typeof resolved === "string") {
|
|
2711
|
+
if (resolvedConfig.modelFromParams) return textResult(resolved);
|
|
2712
|
+
// config-specified: silent fallback to the default model, then parent
|
|
2713
|
+
} else {
|
|
2714
|
+
model = resolved;
|
|
2570
2715
|
}
|
|
2716
|
+
}
|
|
2571
2717
|
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
const
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
if (!rec || !outputTranscript) return;
|
|
2589
|
-
rec.outputFile = createOutputFilePath(
|
|
2590
|
-
ctx.cwd,
|
|
2591
|
-
agentId,
|
|
2592
|
-
ctx.sessionManager.getSessionId(),
|
|
2593
|
-
);
|
|
2594
|
-
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd);
|
|
2595
|
-
};
|
|
2718
|
+
// Tiered model scope is checked after the single final resolution in
|
|
2719
|
+
// runAgent. Only the legacy no-tier path is checked here.
|
|
2720
|
+
if (!resolvedConfig.agentTierSelected) {
|
|
2721
|
+
const scopeVerdict = checkModelScope({
|
|
2722
|
+
model,
|
|
2723
|
+
cwd: ctx.cwd,
|
|
2724
|
+
modelRegistry: ctx.modelRegistry,
|
|
2725
|
+
callerSupplied: resolvedConfig.modelFromParams,
|
|
2726
|
+
agentLabel: customConfig?.displayName ?? subagentType,
|
|
2727
|
+
modelInput: resolvedConfig.modelInput,
|
|
2728
|
+
});
|
|
2729
|
+
if (scopeVerdict.kind === "error")
|
|
2730
|
+
return textResult(scopeVerdict.message);
|
|
2731
|
+
if (scopeVerdict.kind === "warn")
|
|
2732
|
+
ctx.ui.notify(scopeVerdict.message, "warning");
|
|
2733
|
+
}
|
|
2596
2734
|
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2735
|
+
const thinking = resolvedConfig.thinking;
|
|
2736
|
+
const inheritContext = resolvedConfig.inheritContext;
|
|
2737
|
+
const runInBackground = resolvedConfig.runInBackground;
|
|
2738
|
+
const isolated = resolvedConfig.isolated;
|
|
2739
|
+
const isolation = resolvedConfig.isolation;
|
|
2740
|
+
// Whether this spawn writes its .output transcript. Per-agent
|
|
2741
|
+
// frontmatter (`output_transcript`) wins; otherwise the project/global
|
|
2742
|
+
// default applies. `attachTranscript` below is the SOLE gate — every
|
|
2743
|
+
// downstream consumer keys off record.outputFile being set, so no spawn
|
|
2744
|
+
// path can re-enable the transcript by accident.
|
|
2745
|
+
const outputTranscript =
|
|
2746
|
+
customConfig?.outputTranscript ?? getOutputTranscriptDefault();
|
|
2747
|
+
const attachTranscript = (
|
|
2748
|
+
rec: AgentRecord | undefined,
|
|
2749
|
+
agentId: string,
|
|
2750
|
+
): void => {
|
|
2751
|
+
if (!rec || !outputTranscript) return;
|
|
2752
|
+
rec.outputFile = createOutputFilePath(
|
|
2753
|
+
ctx.cwd,
|
|
2754
|
+
agentId,
|
|
2755
|
+
ctx.sessionManager.getSessionId(),
|
|
2605
2756
|
);
|
|
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
|
-
};
|
|
2757
|
+
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd);
|
|
2758
|
+
};
|
|
2633
2759
|
|
|
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
|
-
}
|
|
2760
|
+
// Untiered spawns only. A tier resolves its model inside runAgent, and
|
|
2761
|
+
// its resolution callback supplies the label from there — computing one
|
|
2762
|
+
// here would name a model this path never resolved.
|
|
2763
|
+
const parentModelId = ctx.model?.id;
|
|
2764
|
+
const modelName =
|
|
2765
|
+
model && model.id !== parentModelId
|
|
2766
|
+
? shortModelLabel(model)
|
|
2767
|
+
: undefined;
|
|
2768
|
+
const effectiveMaxTurns = normalizeMaxTurns(
|
|
2769
|
+
resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
|
|
2770
|
+
);
|
|
2771
|
+
const agentInvocation: AgentInvocation = {
|
|
2772
|
+
modelName,
|
|
2773
|
+
...((resolvedConfig.requestedAgentTier ?? customConfig?.agentTier) ===
|
|
2774
|
+
undefined
|
|
2775
|
+
? {}
|
|
2776
|
+
: {
|
|
2777
|
+
agentTier:
|
|
2778
|
+
resolvedConfig.requestedAgentTier ?? customConfig?.agentTier,
|
|
2779
|
+
}),
|
|
2780
|
+
thinking,
|
|
2781
|
+
// Explicit value only — the default fallback would just add noise.
|
|
2782
|
+
// Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
|
|
2783
|
+
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
2784
|
+
isolated,
|
|
2785
|
+
inheritContext,
|
|
2786
|
+
runInBackground,
|
|
2787
|
+
isolation,
|
|
2788
|
+
};
|
|
2789
|
+
// Tool-result render shows the mode label too; viewer's header already does.
|
|
2790
|
+
const modeLabel = getPromptModeLabel(subagentType);
|
|
2791
|
+
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
2792
|
+
const agentTags = modeLabel
|
|
2793
|
+
? [modeLabel, ...invocationTags]
|
|
2794
|
+
: invocationTags;
|
|
2795
|
+
const detailBase = {
|
|
2796
|
+
displayName,
|
|
2797
|
+
description: params.description,
|
|
2798
|
+
subagentType,
|
|
2799
|
+
modelName,
|
|
2800
|
+
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
2801
|
+
};
|
|
2694
2802
|
|
|
2695
|
-
|
|
2803
|
+
// ---- Schedule: register a job, don't spawn now ----
|
|
2804
|
+
if (params.schedule) {
|
|
2805
|
+
if (!isSchedulingEnabled()) {
|
|
2806
|
+
return textResult(
|
|
2807
|
+
"Scheduling is disabled in this project. Enable via /agents, Settings, Scheduling.",
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2696
2810
|
if (params.resume) {
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2811
|
+
return textResult(
|
|
2812
|
+
"Cannot combine `schedule` with `resume` — schedules create fresh agents.",
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2815
|
+
if (inheritContext) {
|
|
2816
|
+
return textResult(
|
|
2817
|
+
"Cannot combine `schedule` with `inherit_context` — there is no parent conversation at fire time.",
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
if (params.run_in_background === false) {
|
|
2821
|
+
return textResult(
|
|
2822
|
+
"Cannot combine `schedule` with `run_in_background: false` — scheduled jobs always run in background.",
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
if (!scheduler.isActive()) {
|
|
2826
|
+
return textResult(
|
|
2827
|
+
"Scheduler is not active in this session yet. Try again after the session has fully started.",
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
try {
|
|
2831
|
+
const job = scheduler.addJob({
|
|
2832
|
+
name: params.description as string,
|
|
2833
|
+
description: params.description as string,
|
|
2834
|
+
schedule: params.schedule as string,
|
|
2835
|
+
// The caller's own name, not the substitute — the scheduler re-resolves
|
|
2836
|
+
// at fire time, and the original is what a user edits.
|
|
2837
|
+
subagent_type: requestedType,
|
|
2838
|
+
prompt: params.prompt as string,
|
|
2839
|
+
// Only an explicitly requested tier is frozen into the job. An
|
|
2840
|
+
// agent's frontmatter tier is deliberately not copied: it is read
|
|
2841
|
+
// again at fire time, so editing the agent file still takes
|
|
2842
|
+
// effect, and it keeps its "frontmatter" source rather than being
|
|
2843
|
+
// replayed as a caller choice.
|
|
2844
|
+
tier: resolvedConfig.requestedAgentTier,
|
|
2845
|
+
// Store the resolved policy input (agent config first, tool params second)
|
|
2846
|
+
// so scheduled fires use the same model fallback as an immediate spawn.
|
|
2847
|
+
model: resolvedConfig.agentTierSelected
|
|
2848
|
+
? undefined
|
|
2849
|
+
: resolvedConfig.modelInput,
|
|
2850
|
+
thinking: resolvedConfig.agentTierSelected ? undefined : thinking,
|
|
2851
|
+
max_turns: effectiveMaxTurns,
|
|
2852
|
+
isolated: isolated,
|
|
2853
|
+
isolation: isolation,
|
|
2854
|
+
});
|
|
2855
|
+
const next = scheduler.getNextRun(job.id);
|
|
2856
|
+
return textResult(
|
|
2857
|
+
`${fallbackNote}Scheduled "${job.name}" (id: ${job.id}, type: ${job.scheduleType}). ` +
|
|
2858
|
+
`Next run: ${next ?? "(unknown)"}. ` +
|
|
2859
|
+
`Manage via /agents, Scheduled jobs.`,
|
|
2860
|
+
);
|
|
2861
|
+
} catch (err) {
|
|
2862
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2711
2865
|
|
|
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(
|
|
2866
|
+
// Resume existing agent
|
|
2867
|
+
if (params.resume) {
|
|
2868
|
+
const existing = manager.getRecordMutable(params.resume);
|
|
2869
|
+
if (!existing || existing.parentAgentId) {
|
|
2870
|
+
return textResult(
|
|
2871
|
+
`Agent not found: "${params.resume}". It may have been cleaned up.`,
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
if (!existing.session) {
|
|
2875
|
+
return textResult(
|
|
2876
|
+
`Agent "${params.resume}" has no active session to resume.`,
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
if (signal?.aborted) {
|
|
2880
|
+
return textResult("Resume aborted.");
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// Assigned unconditionally, before either resume path. The completion
|
|
2884
|
+
// notification carries this as `<tool-use-id>` (see
|
|
2885
|
+
// formatTaskNotification), and `manager.resume` clears
|
|
2886
|
+
// `resultConsumed`, so a resumed run does notify. Keeping the id the
|
|
2887
|
+
// original spawn wrote would point that notification at a tool call
|
|
2888
|
+
// answered runs ago; a resume with no tool call of its own must clear
|
|
2889
|
+
// it rather than inherit one.
|
|
2890
|
+
existing.toolCallId = toolCallId;
|
|
2891
|
+
|
|
2892
|
+
// run_in_background on resume: settle asynchronously and notify on
|
|
2893
|
+
// completion like a background spawn, returning immediately. Previously
|
|
2894
|
+
// the flag was accepted then silently dropped — a resumed agent always
|
|
2895
|
+
// blocked the caller until it finished.
|
|
2896
|
+
if (runInBackground) {
|
|
2897
|
+
const { state: bgState, callbacks: bgCallbacks } =
|
|
2898
|
+
createActivityTracker(effectiveMaxTurns);
|
|
2899
|
+
// resumeAgent has no onSessionCreated — the session predates this run
|
|
2900
|
+
// — so seed the activity tracker directly.
|
|
2901
|
+
bgState.session = existing.session;
|
|
2902
|
+
// Reuse the agent's transcript rather than starting a fresh one: the
|
|
2903
|
+
// path is deterministic per agent+session, and writing an initial
|
|
2904
|
+
// entry would truncate the previous run's turns (B1#2).
|
|
2905
|
+
if (outputTranscript) {
|
|
2906
|
+
existing.outputFile = createOutputFilePath(
|
|
2907
|
+
ctx.cwd,
|
|
2757
2908
|
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),
|
|
2909
|
+
ctx.sessionManager.getSessionId(),
|
|
2780
2910
|
);
|
|
2911
|
+
ensureOutputFile(existing.outputFile);
|
|
2781
2912
|
}
|
|
2782
|
-
|
|
2913
|
+
// Anchor streaming past the turns already on disk, captured BEFORE the
|
|
2914
|
+
// run starts. The resumed prompt lands as an ordinary user message at
|
|
2915
|
+
// this index, so it is written exactly once.
|
|
2916
|
+
const transcriptAnchor = existing.session.messages.length ?? 0;
|
|
2917
|
+
const attachTranscript = (): void => {
|
|
2918
|
+
if (!existing.outputFile || existing.outputCleanup) return;
|
|
2919
|
+
existing.outputCleanup = streamToOutputFile(
|
|
2920
|
+
existing.session!,
|
|
2921
|
+
existing.outputFile,
|
|
2922
|
+
params.resume!,
|
|
2923
|
+
ctx.cwd,
|
|
2924
|
+
transcriptAnchor,
|
|
2925
|
+
);
|
|
2926
|
+
};
|
|
2783
2927
|
const record = await manager.resume(
|
|
2784
2928
|
params.resume,
|
|
2785
2929
|
params.prompt,
|
|
2786
|
-
|
|
2930
|
+
undefined,
|
|
2931
|
+
{
|
|
2932
|
+
isBackground: true,
|
|
2933
|
+
onToolActivity: bgCallbacks.onToolActivity,
|
|
2934
|
+
onAssistantUsage: bgCallbacks.onAssistantUsage,
|
|
2935
|
+
onStarted: attachTranscript,
|
|
2936
|
+
},
|
|
2787
2937
|
);
|
|
2788
2938
|
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
2939
|
return textResult(
|
|
2795
|
-
`
|
|
2796
|
-
|
|
2940
|
+
`Cannot resume agent "${params.resume}" in background — it is already running. ` +
|
|
2941
|
+
"Wait for it to settle, or steer it with steer_subagent.",
|
|
2797
2942
|
);
|
|
2798
2943
|
}
|
|
2944
|
+
agentActivity.set(params.resume, bgState);
|
|
2945
|
+
void bgCallbacks;
|
|
2799
2946
|
return textResult(
|
|
2800
|
-
record.
|
|
2947
|
+
record.status === "queued"
|
|
2948
|
+
? `Agent "${params.resume}" resumed in background (queued at the concurrency limit).`
|
|
2949
|
+
: `Agent "${params.resume}" resumed in background.`,
|
|
2801
2950
|
buildDetails(detailBase, record),
|
|
2802
2951
|
);
|
|
2803
2952
|
}
|
|
2804
2953
|
|
|
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
|
-
}
|
|
2954
|
+
const record = await manager.resume(
|
|
2955
|
+
params.resume,
|
|
2956
|
+
params.prompt,
|
|
2957
|
+
signal,
|
|
2958
|
+
);
|
|
2959
|
+
if (!record) {
|
|
2960
|
+
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
2961
|
+
}
|
|
2962
|
+
// A failed resume surfaces the error, plus any partial output THIS
|
|
2963
|
+
// resume produced (never the previous turn's answer, #144).
|
|
2964
|
+
if (record.status === "error") {
|
|
2965
|
+
return textResult(
|
|
2966
|
+
`Agent failed: ${record.error}${partialOutputSuffix(record)}`,
|
|
2967
|
+
buildDetails(detailBase, record),
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
return textResult(
|
|
2971
|
+
record.result?.trim() || "No output.",
|
|
2972
|
+
buildDetails(detailBase, record),
|
|
2973
|
+
);
|
|
2974
|
+
}
|
|
2854
2975
|
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2976
|
+
// Background execution
|
|
2977
|
+
if (runInBackground) {
|
|
2978
|
+
const { state: bgState, callbacks: bgCallbacks } =
|
|
2979
|
+
createActivityTracker(effectiveMaxTurns);
|
|
2980
|
+
|
|
2981
|
+
// Wrap onSessionCreated to wire output file streaming.
|
|
2982
|
+
// The callback lazily reads record.outputFile (set right after spawn)
|
|
2983
|
+
// rather than closing over a value that doesn't exist yet.
|
|
2984
|
+
let id: string;
|
|
2985
|
+
const origBgOnSession = bgCallbacks.onSessionCreated;
|
|
2986
|
+
bgCallbacks.onSessionCreated = (session: any) => {
|
|
2987
|
+
origBgOnSession(session);
|
|
2988
|
+
const rec = manager.getRecordMutable(id);
|
|
2989
|
+
if (rec?.outputFile) {
|
|
2990
|
+
rec.outputCleanup = streamToOutputFile(
|
|
2991
|
+
session,
|
|
2992
|
+
rec.outputFile,
|
|
2993
|
+
id,
|
|
2994
|
+
ctx.cwd,
|
|
2995
|
+
);
|
|
2864
2996
|
}
|
|
2997
|
+
};
|
|
2865
2998
|
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2999
|
+
// A startup throw means the agent never started. Let pi mark the tool
|
|
3000
|
+
// call as failed instead of returning a successful-looking text result.
|
|
3001
|
+
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
3002
|
+
description: params.description,
|
|
3003
|
+
model: resolvedConfig.agentTierSelected ? undefined : model,
|
|
3004
|
+
maxTurns: effectiveMaxTurns,
|
|
3005
|
+
isolated,
|
|
3006
|
+
inheritContext,
|
|
3007
|
+
thinkingLevel: resolvedConfig.agentTierSelected
|
|
3008
|
+
? undefined
|
|
3009
|
+
: thinking,
|
|
3010
|
+
agentTier: resolvedConfig.requestedAgentTier,
|
|
3011
|
+
isBackground: true,
|
|
3012
|
+
isolation,
|
|
3013
|
+
invocation: agentInvocation,
|
|
3014
|
+
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
3015
|
+
...bgCallbacks,
|
|
3016
|
+
});
|
|
3017
|
+
// The clone needs a positive spawn witness before any UI, transcript,
|
|
3018
|
+
// or event observer can throw. A startup failure inside manager.spawn
|
|
3019
|
+
// removes its record and does not reach this acknowledgement.
|
|
3020
|
+
const record = manager.getRecordMutable(id);
|
|
3021
|
+
if (record) {
|
|
3022
|
+
(
|
|
3023
|
+
params as typeof params & {
|
|
3024
|
+
[MENTION_SPAWNED]?: (id: string) => void;
|
|
3025
|
+
}
|
|
3026
|
+
)[MENTION_SPAWNED]?.(id);
|
|
3027
|
+
}
|
|
2869
3028
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
}
|
|
3029
|
+
// Set output file + join mode synchronously after spawn, before the
|
|
3030
|
+
// event loop yields — onSessionCreated is async so this is safe.
|
|
3031
|
+
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
3032
|
+
if (record && joinMode) {
|
|
3033
|
+
record.joinMode = joinMode;
|
|
3034
|
+
record.toolCallId = toolCallId;
|
|
3035
|
+
attachTranscript(record, id);
|
|
3036
|
+
}
|
|
2879
3037
|
|
|
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
|
-
);
|
|
3038
|
+
if (joinMode == null || joinMode === "async") {
|
|
3039
|
+
// Foreground/no join mode or explicit async — not part of any batch
|
|
3040
|
+
} else {
|
|
3041
|
+
// smart or group — add to current batch
|
|
3042
|
+
currentBatchAgents.push({ id, joinMode });
|
|
3043
|
+
// Debounce: reset timer on each new agent so parallel tool calls
|
|
3044
|
+
// dispatched across multiple event loop ticks are captured together
|
|
3045
|
+
if (batchFinalizeTimer) clearTimeout(batchFinalizeTimer);
|
|
3046
|
+
batchFinalizeTimer = setTimeout(finalizeBatch, 100);
|
|
2904
3047
|
}
|
|
2905
3048
|
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
let fgId: string | undefined;
|
|
3049
|
+
agentActivity.set(id, bgState);
|
|
3050
|
+
fleet.ensureTimer();
|
|
3051
|
+
fleet.update();
|
|
2910
3052
|
|
|
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,
|
|
3053
|
+
// Emit created event unless a branch replacement detached the record.
|
|
3054
|
+
if (!record?.detached) {
|
|
3055
|
+
pi.events.emit("subagents:created", {
|
|
3056
|
+
id,
|
|
3057
|
+
type: subagentType,
|
|
3058
|
+
description: params.description,
|
|
3059
|
+
isBackground: true,
|
|
2931
3060
|
});
|
|
2932
|
-
}
|
|
3061
|
+
}
|
|
2933
3062
|
|
|
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
|
-
};
|
|
3063
|
+
const isQueued = record?.status === "queued";
|
|
3064
|
+
return textResult(
|
|
3065
|
+
`${fallbackNote}Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
3066
|
+
`Agent ID: ${id}\n` +
|
|
3067
|
+
`Type: ${displayName}\n` +
|
|
3068
|
+
`Description: ${params.description}\n` +
|
|
3069
|
+
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
3070
|
+
(isQueued
|
|
3071
|
+
? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n`
|
|
3072
|
+
: "") +
|
|
3073
|
+
`\nYou will be notified when this agent completes.\n` +
|
|
3074
|
+
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
3075
|
+
`Do not duplicate this agent's work.`,
|
|
3076
|
+
{
|
|
3077
|
+
...detailBase,
|
|
3078
|
+
toolUses: 0,
|
|
3079
|
+
tokens: "",
|
|
3080
|
+
durationMs: 0,
|
|
3081
|
+
status: isQueued ? "queued" : "background",
|
|
3082
|
+
agentId: id,
|
|
3083
|
+
},
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
2965
3086
|
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
3087
|
+
// Foreground (synchronous) execution — stream progress via onUpdate
|
|
3088
|
+
let spinnerFrame = 0;
|
|
3089
|
+
const startedAt = Date.now();
|
|
3090
|
+
let fgId: string | undefined;
|
|
3091
|
+
|
|
3092
|
+
const streamUpdate = () => {
|
|
3093
|
+
const details: AgentDetails = {
|
|
3094
|
+
...detailBase,
|
|
3095
|
+
toolUses: fgState.toolUses,
|
|
3096
|
+
tokens: formatLifetimeTokens(fgState),
|
|
3097
|
+
turnCount: fgState.turnCount,
|
|
3098
|
+
maxTurns: fgState.maxTurns,
|
|
3099
|
+
durationMs: Date.now() - startedAt,
|
|
3100
|
+
status: "running",
|
|
3101
|
+
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
3102
|
+
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
3103
|
+
};
|
|
3104
|
+
onUpdate?.({
|
|
3105
|
+
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
|
|
3106
|
+
details: details as any,
|
|
3107
|
+
});
|
|
3108
|
+
};
|
|
2970
3109
|
|
|
2971
|
-
|
|
3110
|
+
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(
|
|
3111
|
+
effectiveMaxTurns,
|
|
3112
|
+
streamUpdate,
|
|
3113
|
+
);
|
|
2972
3114
|
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
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);
|
|
3115
|
+
// Wire session creation: register in the FleetView list + stream to the output file.
|
|
3116
|
+
// The output file path is set synchronously after spawn (below),
|
|
3117
|
+
// before onSessionCreated fires — same pattern as background agents.
|
|
3118
|
+
const origOnSession = fgCallbacks.onSessionCreated;
|
|
3119
|
+
fgCallbacks.onSessionCreated = (session: any) => {
|
|
3120
|
+
origOnSession(session);
|
|
3121
|
+
for (const a of manager.listAgentsMutable()) {
|
|
3122
|
+
if (a.session === session) {
|
|
3123
|
+
fgId = a.id;
|
|
3124
|
+
agentActivity.set(a.id, fgState);
|
|
3125
|
+
fleet.ensureTimer();
|
|
3126
|
+
fleet.update();
|
|
3127
|
+
break;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
// Stream conversation to output file (foreground agent logging)
|
|
3131
|
+
if (fgId) {
|
|
3132
|
+
const rec = manager.getRecordMutable(fgId);
|
|
3133
|
+
if (rec?.outputFile) {
|
|
3134
|
+
rec.outputCleanup = streamToOutputFile(
|
|
3135
|
+
session,
|
|
3136
|
+
rec.outputFile,
|
|
3137
|
+
fgId,
|
|
3138
|
+
ctx.cwd,
|
|
3139
|
+
);
|
|
3009
3140
|
}
|
|
3010
3141
|
}
|
|
3142
|
+
};
|
|
3011
3143
|
|
|
3012
|
-
|
|
3013
|
-
|
|
3144
|
+
const spinnerInterval = setInterval(() => {
|
|
3145
|
+
spinnerFrame++;
|
|
3146
|
+
streamUpdate();
|
|
3147
|
+
}, SPINNER_INTERVAL_MS);
|
|
3014
3148
|
|
|
3015
|
-
|
|
3016
|
-
tokens: tokenText,
|
|
3017
|
-
});
|
|
3149
|
+
streamUpdate();
|
|
3018
3150
|
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3151
|
+
let record: AgentRecord;
|
|
3152
|
+
try {
|
|
3153
|
+
const fgResult = await manager.spawnAndWait(
|
|
3154
|
+
pi,
|
|
3155
|
+
ctx,
|
|
3156
|
+
subagentType,
|
|
3157
|
+
params.prompt,
|
|
3158
|
+
{
|
|
3159
|
+
description: params.description,
|
|
3160
|
+
model: resolvedConfig.agentTierSelected ? undefined : model,
|
|
3161
|
+
maxTurns: effectiveMaxTurns,
|
|
3162
|
+
isolated,
|
|
3163
|
+
inheritContext,
|
|
3164
|
+
thinkingLevel: resolvedConfig.agentTierSelected
|
|
3165
|
+
? undefined
|
|
3166
|
+
: thinking,
|
|
3167
|
+
agentTier: resolvedConfig.requestedAgentTier,
|
|
3168
|
+
isolation,
|
|
3169
|
+
invocation: agentInvocation,
|
|
3170
|
+
signal,
|
|
3171
|
+
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
3172
|
+
...fgCallbacks,
|
|
3173
|
+
},
|
|
3174
|
+
(fgAgentId) => {
|
|
3175
|
+
// onSpawned: called synchronously after spawn, before onSessionCreated fires.
|
|
3176
|
+
// Set up the output file so streamToOutputFile can pick it up.
|
|
3177
|
+
const fgRec = manager.getRecordMutable(fgAgentId);
|
|
3178
|
+
attachTranscript(fgRec, fgAgentId);
|
|
3179
|
+
},
|
|
3180
|
+
);
|
|
3181
|
+
record = fgResult.record;
|
|
3182
|
+
} finally {
|
|
3183
|
+
// Always stop the spinner and drop the row, including startup errors
|
|
3184
|
+
// that now propagate to pi as failed tool calls.
|
|
3185
|
+
clearInterval(spinnerInterval);
|
|
3186
|
+
if (fgId) {
|
|
3187
|
+
agentActivity.delete(fgId);
|
|
3188
|
+
fleet.onAgentFinished(fgId);
|
|
3025
3189
|
}
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
// Get final token count
|
|
3193
|
+
const tokenText = formatLifetimeTokens(fgState);
|
|
3194
|
+
|
|
3195
|
+
const details = buildDetails(detailBase, record, fgState, {
|
|
3196
|
+
tokens: tokenText,
|
|
3197
|
+
});
|
|
3026
3198
|
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
const statsParts = [`${record.toolUses} tool uses`];
|
|
3030
|
-
if (tokenText) statsParts.push(tokenText);
|
|
3199
|
+
if (record.status === "error") {
|
|
3200
|
+
// Error headline + any partial output the run produced before failing.
|
|
3031
3201
|
return textResult(
|
|
3032
|
-
`${fallbackNote}Agent
|
|
3033
|
-
(record.result?.trim() || "No output."),
|
|
3202
|
+
`${fallbackNote}Agent failed: ${record.error}${partialOutputSuffix(record)}`,
|
|
3034
3203
|
details,
|
|
3035
3204
|
);
|
|
3036
|
-
}
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
3208
|
+
const statsParts = [`${record.toolUses} tool uses`];
|
|
3209
|
+
if (tokenText) statsParts.push(tokenText);
|
|
3210
|
+
return textResult(
|
|
3211
|
+
`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getForegroundOutcomeNote(record.status)}.\n\n` +
|
|
3212
|
+
(record.result?.trim() || "No output."),
|
|
3213
|
+
details,
|
|
3214
|
+
);
|
|
3215
|
+
},
|
|
3037
3216
|
});
|
|
3038
3217
|
pi.registerTool(agentTool);
|
|
3039
3218
|
agentToolRef = agentTool;
|
|
@@ -3154,7 +3333,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3154
3333
|
"Chat with or redirect a running or queued background agent",
|
|
3155
3334
|
parameters: Type.Object({
|
|
3156
3335
|
agent_id: Type.String({
|
|
3157
|
-
description:
|
|
3336
|
+
description:
|
|
3337
|
+
"The agent ID to message (must be currently running or queued).",
|
|
3158
3338
|
}),
|
|
3159
3339
|
message: Type.String({
|
|
3160
3340
|
description:
|
|
@@ -3185,7 +3365,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3185
3365
|
id: record.id,
|
|
3186
3366
|
message: params.message,
|
|
3187
3367
|
});
|
|
3188
|
-
const delivery =
|
|
3368
|
+
const delivery =
|
|
3369
|
+
record.status === "queued"
|
|
3370
|
+
? "when the agent starts"
|
|
3371
|
+
: "once the session initializes";
|
|
3189
3372
|
return textResult(
|
|
3190
3373
|
`Chat message queued for agent ${record.id}. It will be delivered ${delivery}.`,
|
|
3191
3374
|
);
|
|
@@ -3493,7 +3676,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3493
3676
|
`"${sanitizeDisplayText(record.description)}" has not started yet.${handleHint}\n\nStop this queued run?`,
|
|
3494
3677
|
);
|
|
3495
3678
|
if (stop && manager.abort(record.id)) {
|
|
3496
|
-
ctx.ui.notify(
|
|
3679
|
+
ctx.ui.notify(
|
|
3680
|
+
`Stopped queued agent "${sanitizeDisplayText(record.description)}".`,
|
|
3681
|
+
"info",
|
|
3682
|
+
);
|
|
3497
3683
|
}
|
|
3498
3684
|
return;
|
|
3499
3685
|
}
|
|
@@ -4161,7 +4347,7 @@ prompt_mode: <"replace" (body IS the full system prompt) or "append" (body is ap
|
|
|
4161
4347
|
extensions: <true (inherit all MCP/extension tools), false (none), or comma-separated names. Default: true>
|
|
4162
4348
|
skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
|
|
4163
4349
|
disallowed_tools: <comma-separated tool names to block, even if otherwise available. Omit for none>
|
|
4164
|
-
inherit_context: <
|
|
4350
|
+
inherit_context: <omit or false; true is rejected because Pi cannot safely transfer the post-hook parent context>
|
|
4165
4351
|
run_in_background: <true to run in background by default. Default: false>
|
|
4166
4352
|
output_transcript: <false to write no transcript file or path for this agent. Independent of persist_session. Default: true>
|
|
4167
4353
|
isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
|
|
@@ -4177,7 +4363,7 @@ Guidelines for choosing settings:
|
|
|
4177
4363
|
- For code modification tasks: include edit, write
|
|
4178
4364
|
- Use prompt_mode: append if the agent should keep the default system prompt and add specialization on top
|
|
4179
4365
|
- Use prompt_mode: replace for fully custom agents with their own personality/instructions
|
|
4180
|
-
-
|
|
4366
|
+
- Never set inherit_context: true; put an explicitly sanitized summary in the Agent task instead
|
|
4181
4367
|
- Set isolated: true if the agent should NOT have access to MCP servers or other extensions
|
|
4182
4368
|
- 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
4369
|
- Only include frontmatter fields that differ from defaults — omit fields where the default is fine
|
|
@@ -4781,7 +4967,8 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4781
4967
|
id: "defaultTier",
|
|
4782
4968
|
label: "Default tier",
|
|
4783
4969
|
description: (() => {
|
|
4784
|
-
if (tierKeys.length === 0)
|
|
4970
|
+
if (tierKeys.length === 0)
|
|
4971
|
+
return "No tiers defined yet — create one in /agents → Model tiers.";
|
|
4785
4972
|
const fallback = shippedFallbackAgentTier();
|
|
4786
4973
|
// Named from the resolver, not from a constant: on a catalogue that
|
|
4787
4974
|
// removed the shipped profile, "unset" reaches nothing and behaves
|
|
@@ -4796,7 +4983,9 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4796
4983
|
currentValue: (() => {
|
|
4797
4984
|
const selection = getDefaultAgentTierSelection();
|
|
4798
4985
|
if (selection.kind === "tier") return selection.tier;
|
|
4799
|
-
return selection.kind === "none"
|
|
4986
|
+
return selection.kind === "none"
|
|
4987
|
+
? NO_DEFAULT_TIER
|
|
4988
|
+
: UNSET_DEFAULT_TIER;
|
|
4800
4989
|
})(),
|
|
4801
4990
|
values: [UNSET_DEFAULT_TIER, NO_DEFAULT_TIER, ...tierKeys],
|
|
4802
4991
|
},
|
|
@@ -4874,7 +5063,7 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4874
5063
|
id: "agentMentions",
|
|
4875
5064
|
label: "Agent mentions",
|
|
4876
5065
|
description:
|
|
4877
|
-
"How `@handle message` is dispatched: model (an
|
|
5066
|
+
"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
5067
|
currentValue: getAgentMentionMode(),
|
|
4879
5068
|
values: [...AGENT_MENTION_MODES],
|
|
4880
5069
|
},
|
|
@@ -5055,13 +5244,13 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
5055
5244
|
const enabled = value === "on";
|
|
5056
5245
|
supervisorQuestionsEnabled = enabled;
|
|
5057
5246
|
manager.setSupervisorQuestions(enabled);
|
|
5058
|
-
notifyApplied(ctx, `Supervisor questions ${enabled ? "enabled" : "disabled"}`);
|
|
5059
|
-
} else if (id === "agentMentions") {
|
|
5060
|
-
agentMentionMode = value as AgentMentionMode;
|
|
5061
5247
|
notifyApplied(
|
|
5062
5248
|
ctx,
|
|
5063
|
-
`
|
|
5249
|
+
`Supervisor questions ${enabled ? "enabled" : "disabled"}`,
|
|
5064
5250
|
);
|
|
5251
|
+
} else if (id === "agentMentions") {
|
|
5252
|
+
agentMentionMode = value as AgentMentionMode;
|
|
5253
|
+
notifyApplied(ctx, `Agent mentions set to ${agentMentionMode}`);
|
|
5065
5254
|
}
|
|
5066
5255
|
}
|
|
5067
5256
|
|