@herbertgao/pi-subagents 0.16.0 → 0.17.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 +18 -0
- package/README.md +36 -17
- package/package.json +3 -3
- package/src/agent-file-toggle.ts +11 -8
- package/src/agent-manager.ts +184 -106
- package/src/cross-extension-rpc.ts +46 -15
- package/src/custom-agents.ts +9 -1
- package/src/index.ts +172 -31
- package/src/invocation-config.ts +17 -0
- package/src/model-resolver.ts +14 -0
- package/src/schedule.ts +8 -0
- package/src/settings.ts +32 -1
- package/src/types.ts +17 -2
- package/src/ui/agent-widget.ts +23 -2
- package/src/ui/conversation-viewer.ts +264 -25
package/src/index.ts
CHANGED
|
@@ -77,7 +77,11 @@ import {
|
|
|
77
77
|
resolveAgentInvocationConfig,
|
|
78
78
|
resolveJoinMode,
|
|
79
79
|
} from "./invocation-config.js"
|
|
80
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
describeModel,
|
|
82
|
+
type ModelRegistry,
|
|
83
|
+
resolveModel,
|
|
84
|
+
} from "./model-resolver.js"
|
|
81
85
|
import {
|
|
82
86
|
checkModelScope,
|
|
83
87
|
isScopeModelsEnabled,
|
|
@@ -113,6 +117,7 @@ import {
|
|
|
113
117
|
type JoinMode,
|
|
114
118
|
type NotificationDetails,
|
|
115
119
|
type SubagentType,
|
|
120
|
+
type ViewerMarkdownMode,
|
|
116
121
|
type WidgetMode,
|
|
117
122
|
} from "./types.js"
|
|
118
123
|
import {
|
|
@@ -541,6 +546,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
541
546
|
widget.update()
|
|
542
547
|
fleet.update()
|
|
543
548
|
}
|
|
549
|
+
let showModel = false
|
|
550
|
+
function isShowModelEnabled(): boolean {
|
|
551
|
+
return showModel
|
|
552
|
+
}
|
|
553
|
+
function setShowModel(enabled: boolean): void {
|
|
554
|
+
showModel = enabled
|
|
555
|
+
widget.update()
|
|
556
|
+
}
|
|
557
|
+
let viewerMarkdown: ViewerMarkdownMode = "assistant"
|
|
558
|
+
function getViewerMarkdown(): ViewerMarkdownMode {
|
|
559
|
+
return viewerMarkdown
|
|
560
|
+
}
|
|
561
|
+
function setViewerMarkdown(mode: ViewerMarkdownMode): void {
|
|
562
|
+
viewerMarkdown = mode
|
|
563
|
+
}
|
|
544
564
|
|
|
545
565
|
// ---- Cancellable pending notifications ----
|
|
546
566
|
// Holds notifications briefly so get_subagent_result can cancel them
|
|
@@ -800,6 +820,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
800
820
|
// Also internal: it names a transcript directory, so a forged value would
|
|
801
821
|
// be a path-traversal primitive.
|
|
802
822
|
delete safeOptions.rootSessionId
|
|
823
|
+
// Every call through this registry is detached, never a blocking tool call.
|
|
824
|
+
delete safeOptions.blocking
|
|
803
825
|
// Cross-extension callers get the same dispatch contract as the LLM (#183).
|
|
804
826
|
// The RPC layer already throws for an unresolvable model rather than falling
|
|
805
827
|
// back silently; a bad agent type should not be quieter. Throws become error
|
|
@@ -890,6 +912,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
890
912
|
const record = manager.getRecord(id)
|
|
891
913
|
return !record?.parentAgentId && manager.abort(id)
|
|
892
914
|
},
|
|
915
|
+
consumeResult: (id) => {
|
|
916
|
+
const record = manager.getRecord(id)
|
|
917
|
+
if (
|
|
918
|
+
!record ||
|
|
919
|
+
record.parentAgentId ||
|
|
920
|
+
record.status === "running" ||
|
|
921
|
+
record.status === "queued"
|
|
922
|
+
) {
|
|
923
|
+
return false
|
|
924
|
+
}
|
|
925
|
+
record.resultConsumed = true
|
|
926
|
+
cancelNudge(record.id)
|
|
927
|
+
return true
|
|
928
|
+
},
|
|
893
929
|
},
|
|
894
930
|
})
|
|
895
931
|
// Broadcast readiness so extensions loaded alongside us can discover us.
|
|
@@ -911,6 +947,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
911
947
|
rpcHandle?.unsubSpawn()
|
|
912
948
|
rpcHandle?.unsubStop()
|
|
913
949
|
rpcHandle?.unsubPing()
|
|
950
|
+
rpcHandle?.unsubConsume()
|
|
914
951
|
rpcHandle = undefined
|
|
915
952
|
currentCtx = undefined
|
|
916
953
|
// Only release the global slot if this activation claimed it — a child
|
|
@@ -943,6 +980,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
943
980
|
agentActivity,
|
|
944
981
|
getWidgetMode,
|
|
945
982
|
isShowCostEnabled,
|
|
983
|
+
isShowModelEnabled,
|
|
946
984
|
)
|
|
947
985
|
function setWidgetMode(m: WidgetMode): void {
|
|
948
986
|
widgetMode = m
|
|
@@ -1068,6 +1106,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1068
1106
|
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
1069
1107
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
1070
1108
|
widget.setUICtx(ctx.ui as UICtx)
|
|
1109
|
+
// SAFETY: both UI adapters receive the same Pi ExtensionContext UI surface.
|
|
1071
1110
|
fleet.setUICtx(ctx.ui as unknown as FleetUICtx)
|
|
1072
1111
|
widget.onTurnStart()
|
|
1073
1112
|
})
|
|
@@ -1117,6 +1156,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1117
1156
|
applyAndEmitLoaded(
|
|
1118
1157
|
{
|
|
1119
1158
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
1159
|
+
setMaxConcurrentForeground: (n) => manager.setMaxConcurrentForeground(n),
|
|
1120
1160
|
setDefaultMaxTurns,
|
|
1121
1161
|
setGraceTurns,
|
|
1122
1162
|
setDefaultJoinMode,
|
|
@@ -1136,6 +1176,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1136
1176
|
setFallbackSubagent: setFallbackSubagent,
|
|
1137
1177
|
setReportUsage,
|
|
1138
1178
|
setShowCost,
|
|
1179
|
+
setShowModel,
|
|
1180
|
+
setViewerMarkdown,
|
|
1139
1181
|
},
|
|
1140
1182
|
(event, payload) => pi.events.emit(event, payload),
|
|
1141
1183
|
)
|
|
@@ -1621,20 +1663,27 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1621
1663
|
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd)
|
|
1622
1664
|
}
|
|
1623
1665
|
|
|
1624
|
-
const
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1666
|
+
const { modelName, modelId } = model
|
|
1667
|
+
? describeModel(model)
|
|
1668
|
+
: { modelName: undefined, modelId: undefined }
|
|
1669
|
+
const askedModel = ((asked: string | undefined) => {
|
|
1670
|
+
if (!asked) return undefined
|
|
1671
|
+
const resolvedAsked = resolveModel(asked, ctx.modelRegistry)
|
|
1672
|
+
if (typeof resolvedAsked === "string") return asked
|
|
1673
|
+
return resolvedAsked.provider === model?.provider &&
|
|
1674
|
+
resolvedAsked.id === model?.id
|
|
1675
|
+
? undefined
|
|
1676
|
+
: asked
|
|
1677
|
+
})(resolvedConfig.overridden?.model)
|
|
1632
1678
|
const effectiveMaxTurns = normalizeMaxTurns(
|
|
1633
1679
|
resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
|
|
1634
1680
|
)
|
|
1635
1681
|
const agentInvocation: AgentInvocation = {
|
|
1636
1682
|
modelName,
|
|
1683
|
+
modelId,
|
|
1637
1684
|
thinking,
|
|
1685
|
+
requestedThinking: resolvedConfig.overridden?.thinking,
|
|
1686
|
+
requestedModel: askedModel,
|
|
1638
1687
|
// Explicit value only — the default fallback would just add noise.
|
|
1639
1688
|
// Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
|
|
1640
1689
|
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
@@ -1656,6 +1705,23 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1656
1705
|
modelName,
|
|
1657
1706
|
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
1658
1707
|
}
|
|
1708
|
+
const detailBaseFor = (
|
|
1709
|
+
rec: AgentRecord | undefined,
|
|
1710
|
+
): typeof detailBase => {
|
|
1711
|
+
if (!rec?.invocation) return detailBase
|
|
1712
|
+
const { modelName: recModelName, tags } = buildInvocationTags(
|
|
1713
|
+
rec.invocation,
|
|
1714
|
+
)
|
|
1715
|
+
const recModeLabel = getPromptModeLabel(rec.type)
|
|
1716
|
+
const recTags = recModeLabel ? [recModeLabel, ...tags] : tags
|
|
1717
|
+
return {
|
|
1718
|
+
displayName: getDisplayName(rec.type),
|
|
1719
|
+
description: rec.description,
|
|
1720
|
+
subagentType: rec.type,
|
|
1721
|
+
modelName: recModelName,
|
|
1722
|
+
tags: recTags.length > 0 ? recTags : undefined,
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1659
1725
|
|
|
1660
1726
|
// ---- Schedule: register a job, don't spawn now ----
|
|
1661
1727
|
if (params.schedule) {
|
|
@@ -1849,9 +1915,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1849
1915
|
`\nYou will be notified when this agent completes.\n` +
|
|
1850
1916
|
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.`,
|
|
1851
1917
|
{
|
|
1852
|
-
...
|
|
1853
|
-
subagentType: existing.type,
|
|
1854
|
-
displayName: existing.type,
|
|
1918
|
+
...detailBaseFor(record),
|
|
1855
1919
|
toolUses: record.toolUses,
|
|
1856
1920
|
tokens: "",
|
|
1857
1921
|
durationMs: 0,
|
|
@@ -1874,12 +1938,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1874
1938
|
if (record.status === "error") {
|
|
1875
1939
|
return textResult(
|
|
1876
1940
|
`Agent failed: ${record.error}${partialOutputSuffix(record)}`,
|
|
1877
|
-
buildDetails(
|
|
1941
|
+
buildDetails(detailBaseFor(record), record),
|
|
1878
1942
|
)
|
|
1879
1943
|
}
|
|
1880
1944
|
return textResult(
|
|
1881
1945
|
record.result?.trim() || "No output.",
|
|
1882
|
-
buildDetails(
|
|
1946
|
+
buildDetails(detailBaseFor(record), record),
|
|
1883
1947
|
)
|
|
1884
1948
|
}
|
|
1885
1949
|
|
|
@@ -1974,7 +2038,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1974
2038
|
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
1975
2039
|
`Do not duplicate this agent's work.`,
|
|
1976
2040
|
{
|
|
1977
|
-
...
|
|
2041
|
+
...detailBaseFor(record),
|
|
1978
2042
|
toolUses: 0,
|
|
1979
2043
|
tokens: "",
|
|
1980
2044
|
durationMs: 0,
|
|
@@ -1988,10 +2052,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1988
2052
|
let spinnerFrame = 0
|
|
1989
2053
|
const startedAt = Date.now()
|
|
1990
2054
|
let fgId: string | undefined
|
|
2055
|
+
let queuedAhead: number | undefined
|
|
1991
2056
|
|
|
1992
2057
|
const streamUpdate = () => {
|
|
1993
2058
|
const details: AgentDetails = {
|
|
1994
|
-
...
|
|
2059
|
+
...detailBaseFor(fgId ? manager.getRecord(fgId) : undefined),
|
|
1995
2060
|
toolUses: fgState.toolUses,
|
|
1996
2061
|
tokens: fgId ? formatLifetimeTokens(manager.getRecord(fgId)!) : "",
|
|
1997
2062
|
cost: fgId
|
|
@@ -2001,10 +2066,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2001
2066
|
maxTurns: fgState.maxTurns,
|
|
2002
2067
|
durationMs: Date.now() - startedAt,
|
|
2003
2068
|
status: "running",
|
|
2004
|
-
activity:
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2069
|
+
activity:
|
|
2070
|
+
queuedAhead === undefined
|
|
2071
|
+
? describeActivity(fgState.activeTools, fgState.responseText)
|
|
2072
|
+
: `queued — waiting for a foreground slot${queuedAhead > 0 ? ` (${queuedAhead} ahead)` : ""}`,
|
|
2008
2073
|
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
2009
2074
|
}
|
|
2010
2075
|
onUpdate?.({
|
|
@@ -2024,6 +2089,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2024
2089
|
const origOnSession = fgCallbacks.onSessionCreated
|
|
2025
2090
|
fgCallbacks.onSessionCreated = (session: any) => {
|
|
2026
2091
|
origOnSession(session)
|
|
2092
|
+
if (queuedAhead !== undefined) {
|
|
2093
|
+
queuedAhead = undefined
|
|
2094
|
+
streamUpdate()
|
|
2095
|
+
}
|
|
2027
2096
|
for (const a of manager.listAgents()) {
|
|
2028
2097
|
if (a.session === session) {
|
|
2029
2098
|
fgId = a.id
|
|
@@ -2074,6 +2143,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2074
2143
|
invocation: agentInvocation,
|
|
2075
2144
|
signal,
|
|
2076
2145
|
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
2146
|
+
onQueued: (_id, ahead) => {
|
|
2147
|
+
queuedAhead = ahead
|
|
2148
|
+
streamUpdate()
|
|
2149
|
+
},
|
|
2077
2150
|
...fgCallbacks,
|
|
2078
2151
|
},
|
|
2079
2152
|
(fgAgentId) => {
|
|
@@ -2098,7 +2171,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2098
2171
|
// Get final token count
|
|
2099
2172
|
const tokenText = formatLifetimeTokens(record)
|
|
2100
2173
|
|
|
2101
|
-
const details = buildDetails(
|
|
2174
|
+
const details = buildDetails(detailBaseFor(record), record, fgState, {
|
|
2102
2175
|
tokens: tokenText,
|
|
2103
2176
|
})
|
|
2104
2177
|
|
|
@@ -2537,6 +2610,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2537
2610
|
keybindings,
|
|
2538
2611
|
(message: string) => manager.steer(record.id, message),
|
|
2539
2612
|
showCost,
|
|
2613
|
+
getViewerMarkdown,
|
|
2614
|
+
(mode) => {
|
|
2615
|
+
setViewerMarkdown(mode)
|
|
2616
|
+
persistSettings(ctx, `Viewer markdown set to ${mode}`)
|
|
2617
|
+
},
|
|
2540
2618
|
)
|
|
2541
2619
|
},
|
|
2542
2620
|
{
|
|
@@ -2828,6 +2906,7 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
2828
2906
|
{
|
|
2829
2907
|
description: `Generate ${name} agent`,
|
|
2830
2908
|
maxTurns: 5,
|
|
2909
|
+
bypassQueue: true,
|
|
2831
2910
|
},
|
|
2832
2911
|
)
|
|
2833
2912
|
|
|
@@ -2952,6 +3031,7 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
2952
3031
|
function snapshotSettings() {
|
|
2953
3032
|
return {
|
|
2954
3033
|
maxConcurrent: manager.getMaxConcurrent(),
|
|
3034
|
+
maxConcurrentForeground: manager.getMaxConcurrentForeground(),
|
|
2955
3035
|
// 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
|
|
2956
3036
|
// normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
|
|
2957
3037
|
defaultMaxTurns: getDefaultMaxTurns() ?? 0,
|
|
@@ -2975,6 +3055,8 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
2975
3055
|
fallbackSubagent: getFallbackSubagent(),
|
|
2976
3056
|
reportUsage: isReportUsageEnabled(),
|
|
2977
3057
|
showCost: isShowCostEnabled(),
|
|
3058
|
+
showModel: isShowModelEnabled(),
|
|
3059
|
+
viewerMarkdown: getViewerMarkdown(),
|
|
2978
3060
|
} satisfies SubagentsSettings
|
|
2979
3061
|
}
|
|
2980
3062
|
|
|
@@ -2994,6 +3076,7 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
2994
3076
|
|
|
2995
3077
|
const NUMERIC_IDS = new Set([
|
|
2996
3078
|
"maxConcurrent",
|
|
3079
|
+
"maxConcurrentForeground",
|
|
2997
3080
|
"defaultMaxTurns",
|
|
2998
3081
|
"graceTurns",
|
|
2999
3082
|
"maxSubagentDepth",
|
|
@@ -3002,6 +3085,7 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3002
3085
|
async function showSettings(ctx: ExtensionCommandContext) {
|
|
3003
3086
|
function buildItems(): SettingItem[] {
|
|
3004
3087
|
const mc = manager.getMaxConcurrent()
|
|
3088
|
+
const mcf = manager.getMaxConcurrentForeground()
|
|
3005
3089
|
const dmt = getDefaultMaxTurns() ?? 0
|
|
3006
3090
|
const gt = getGraceTurns()
|
|
3007
3091
|
const msd = getMaxSubagentDepth()
|
|
@@ -3021,6 +3105,14 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3021
3105
|
currentValue: String(mc),
|
|
3022
3106
|
values: [String(mc)],
|
|
3023
3107
|
},
|
|
3108
|
+
{
|
|
3109
|
+
id: "maxConcurrentForeground",
|
|
3110
|
+
label: "Max foreground concurrency",
|
|
3111
|
+
description:
|
|
3112
|
+
"Max concurrent blocking agents (0 = unlimited, Enter to type)",
|
|
3113
|
+
currentValue: String(mcf),
|
|
3114
|
+
values: [String(mcf)],
|
|
3115
|
+
},
|
|
3024
3116
|
{
|
|
3025
3117
|
id: "defaultMaxTurns",
|
|
3026
3118
|
label: "Default max turns",
|
|
@@ -3128,6 +3220,21 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3128
3220
|
currentValue: isShowCostEnabled() ? "on" : "off",
|
|
3129
3221
|
values: ["on", "off"],
|
|
3130
3222
|
},
|
|
3223
|
+
{
|
|
3224
|
+
id: "showModel",
|
|
3225
|
+
label: "Show model",
|
|
3226
|
+
description: "Show model and thinking level in running widget rows",
|
|
3227
|
+
currentValue: isShowModelEnabled() ? "on" : "off",
|
|
3228
|
+
values: ["on", "off"],
|
|
3229
|
+
},
|
|
3230
|
+
{
|
|
3231
|
+
id: "viewerMarkdown",
|
|
3232
|
+
label: "Viewer markdown",
|
|
3233
|
+
description:
|
|
3234
|
+
"Conversation Markdown: assistant (default), all tool results, or off; press m in the viewer to cycle",
|
|
3235
|
+
currentValue: getViewerMarkdown(),
|
|
3236
|
+
values: ["off", "assistant", "all"],
|
|
3237
|
+
},
|
|
3131
3238
|
{
|
|
3132
3239
|
id: "fleetView",
|
|
3133
3240
|
label: "Fleet view",
|
|
@@ -3162,6 +3269,17 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3162
3269
|
manager.setMaxConcurrent(n)
|
|
3163
3270
|
notifyApplied(ctx, `Max concurrency set to ${n}`)
|
|
3164
3271
|
}
|
|
3272
|
+
} else if (id === "maxConcurrentForeground") {
|
|
3273
|
+
const n = parseInt(value, 10)
|
|
3274
|
+
if (n >= 0) {
|
|
3275
|
+
manager.setMaxConcurrentForeground(n)
|
|
3276
|
+
notifyApplied(
|
|
3277
|
+
ctx,
|
|
3278
|
+
n === 0
|
|
3279
|
+
? "Max foreground concurrency set to unlimited"
|
|
3280
|
+
: `Max foreground concurrency set to ${n}`,
|
|
3281
|
+
)
|
|
3282
|
+
}
|
|
3165
3283
|
} else if (id === "defaultMaxTurns") {
|
|
3166
3284
|
const n = parseInt(value, 10)
|
|
3167
3285
|
if (n === 0) {
|
|
@@ -3266,6 +3384,13 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3266
3384
|
const enabled = value === "on"
|
|
3267
3385
|
setShowCost(enabled)
|
|
3268
3386
|
notifyApplied(ctx, `Cost display ${enabled ? "enabled" : "disabled"}`)
|
|
3387
|
+
} else if (id === "showModel") {
|
|
3388
|
+
const enabled = value === "on"
|
|
3389
|
+
setShowModel(enabled)
|
|
3390
|
+
notifyApplied(ctx, `Model display ${enabled ? "enabled" : "disabled"}`)
|
|
3391
|
+
} else if (id === "viewerMarkdown") {
|
|
3392
|
+
setViewerMarkdown(value as ViewerMarkdownMode)
|
|
3393
|
+
notifyApplied(ctx, `Viewer markdown set to ${value}`)
|
|
3269
3394
|
} else if (id === "toolDescriptionMode") {
|
|
3270
3395
|
setToolDescriptionMode(value as ToolDescriptionMode)
|
|
3271
3396
|
notifyApplied(
|
|
@@ -3336,20 +3461,24 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3336
3461
|
const current =
|
|
3337
3462
|
result === "maxConcurrent"
|
|
3338
3463
|
? String(manager.getMaxConcurrent())
|
|
3339
|
-
: result === "
|
|
3340
|
-
? String(
|
|
3341
|
-
: result === "
|
|
3342
|
-
? String(
|
|
3343
|
-
:
|
|
3464
|
+
: result === "maxConcurrentForeground"
|
|
3465
|
+
? String(manager.getMaxConcurrentForeground())
|
|
3466
|
+
: result === "defaultMaxTurns"
|
|
3467
|
+
? String(getDefaultMaxTurns() ?? 0)
|
|
3468
|
+
: result === "maxSubagentDepth"
|
|
3469
|
+
? String(getMaxSubagentDepth())
|
|
3470
|
+
: String(getGraceTurns())
|
|
3344
3471
|
|
|
3345
3472
|
const label =
|
|
3346
3473
|
result === "maxConcurrent"
|
|
3347
3474
|
? "Max concurrency (1+)"
|
|
3348
|
-
: result === "
|
|
3349
|
-
? "
|
|
3350
|
-
: result === "
|
|
3351
|
-
? "
|
|
3352
|
-
:
|
|
3475
|
+
: result === "maxConcurrentForeground"
|
|
3476
|
+
? "Max foreground concurrency (0 = unlimited)"
|
|
3477
|
+
: result === "defaultMaxTurns"
|
|
3478
|
+
? "Default max turns (0 = unlimited)"
|
|
3479
|
+
: result === "maxSubagentDepth"
|
|
3480
|
+
? "Nested depth (0/1 = nesting off)"
|
|
3481
|
+
: "Grace turns (1+)"
|
|
3353
3482
|
|
|
3354
3483
|
// Loop until user enters a valid integer or cancels (Esc / null).
|
|
3355
3484
|
// Silently trims whitespace; rejects non-numeric input by re-prompting.
|
|
@@ -3372,6 +3501,18 @@ Write the file using the write tool. Only write the file, nothing else.`
|
|
|
3372
3501
|
// the right toast. Successful saves show info; persistence failures downgrade
|
|
3373
3502
|
// to warning so users aren't silently reverted on restart. Event fires regardless
|
|
3374
3503
|
// of outcome so listeners see the in-memory change.
|
|
3504
|
+
function persistSettings(
|
|
3505
|
+
ctx: ExtensionCommandContext,
|
|
3506
|
+
successMsg: string,
|
|
3507
|
+
): void {
|
|
3508
|
+
const { message, level } = saveAndEmitChanged(
|
|
3509
|
+
snapshotSettings(),
|
|
3510
|
+
successMsg,
|
|
3511
|
+
(event, payload) => pi.events.emit(event, payload),
|
|
3512
|
+
)
|
|
3513
|
+
if (level === "warning") ctx.ui.notify(message, level)
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3375
3516
|
function notifyApplied(ctx: ExtensionCommandContext, successMsg: string) {
|
|
3376
3517
|
const { message, level } = saveAndEmitChanged(
|
|
3377
3518
|
snapshotSettings(),
|
package/src/invocation-config.ts
CHANGED
|
@@ -113,6 +113,7 @@ export function resolveAgentInvocationConfig(
|
|
|
113
113
|
runInBackground: boolean
|
|
114
114
|
isolated: boolean
|
|
115
115
|
isolation?: IsolationMode
|
|
116
|
+
overridden?: { thinking?: ThinkingLevel; model?: string }
|
|
116
117
|
} {
|
|
117
118
|
// Precedence first, collapse second — reversing these loses the veto, since
|
|
118
119
|
// an agent file's "off" only outranks a caller's "worktree" while it is still
|
|
@@ -122,6 +123,18 @@ export function resolveAgentInvocationConfig(
|
|
|
122
123
|
requested === "worktree" && opts?.worktreeAllowed !== false
|
|
123
124
|
? "worktree"
|
|
124
125
|
: undefined
|
|
126
|
+
const overriddenThinking =
|
|
127
|
+
agentConfig?.thinking != null &&
|
|
128
|
+
params.thinking != null &&
|
|
129
|
+
agentConfig.thinking !== params.thinking
|
|
130
|
+
? (params.thinking as ThinkingLevel)
|
|
131
|
+
: undefined
|
|
132
|
+
const overriddenModel =
|
|
133
|
+
agentConfig?.model != null &&
|
|
134
|
+
params.model != null &&
|
|
135
|
+
agentConfig.model !== params.model
|
|
136
|
+
? params.model
|
|
137
|
+
: undefined
|
|
125
138
|
|
|
126
139
|
return {
|
|
127
140
|
modelInput: agentConfig?.model ?? params.model,
|
|
@@ -139,6 +152,10 @@ export function resolveAgentInvocationConfig(
|
|
|
139
152
|
false,
|
|
140
153
|
isolated: agentConfig?.isolated ?? params.isolated ?? false,
|
|
141
154
|
isolation,
|
|
155
|
+
overridden:
|
|
156
|
+
overriddenThinking || overriddenModel
|
|
157
|
+
? { thinking: overriddenThinking, model: overriddenModel }
|
|
158
|
+
: undefined,
|
|
142
159
|
}
|
|
143
160
|
}
|
|
144
161
|
|
package/src/model-resolver.ts
CHANGED
|
@@ -14,6 +14,20 @@ export interface ModelRegistry {
|
|
|
14
14
|
getAvailable?(): any[]
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Both display forms of a resolved model. */
|
|
18
|
+
export function describeModel(model: {
|
|
19
|
+
provider: string
|
|
20
|
+
id: string
|
|
21
|
+
name?: string
|
|
22
|
+
}): { modelName: string; modelId: string } {
|
|
23
|
+
return {
|
|
24
|
+
modelName: (model.name ?? model.id)
|
|
25
|
+
.replace(/^Claude\s+/i, "")
|
|
26
|
+
.toLowerCase(),
|
|
27
|
+
modelId: `${model.provider}/${model.id}`,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
/**
|
|
18
32
|
* Resolve a model string to a Model instance.
|
|
19
33
|
* Tries exact match first ("provider/modelId"), then fuzzy match against all available models.
|
package/src/schedule.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
import { Cron } from "croner"
|
|
23
23
|
import { nanoid } from "nanoid"
|
|
24
24
|
import type { AgentManager } from "./agent-manager.js"
|
|
25
|
+
import { normalizeMaxTurns } from "./agent-runner.js"
|
|
25
26
|
import { resolveSpawnType } from "./agent-types.js"
|
|
26
27
|
import { resolveModel } from "./model-resolver.js"
|
|
27
28
|
import type { ScheduleStore } from "./schedule-store.js"
|
|
@@ -280,6 +281,13 @@ export class SubagentScheduler {
|
|
|
280
281
|
isolated: job.isolated,
|
|
281
282
|
thinkingLevel: job.thinking,
|
|
282
283
|
isolation: job.isolation,
|
|
284
|
+
invocation: {
|
|
285
|
+
thinking: job.thinking,
|
|
286
|
+
maxTurns: normalizeMaxTurns(job.max_turns),
|
|
287
|
+
isolated: job.isolated,
|
|
288
|
+
runInBackground: true,
|
|
289
|
+
isolation: job.isolation,
|
|
290
|
+
},
|
|
283
291
|
})
|
|
284
292
|
} catch (err) {
|
|
285
293
|
const error = err instanceof Error ? err.message : String(err)
|
package/src/settings.ts
CHANGED
|
@@ -6,10 +6,12 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
|
6
6
|
import { dirname, join } from "node:path"
|
|
7
7
|
import { getAgentDir } from "@earendil-works/pi-coding-agent"
|
|
8
8
|
import { NO_FALLBACK } from "./agent-types.js"
|
|
9
|
-
import type { JoinMode, WidgetMode } from "./types.js"
|
|
9
|
+
import type { JoinMode, ViewerMarkdownMode, WidgetMode } from "./types.js"
|
|
10
10
|
|
|
11
11
|
export interface SubagentsSettings {
|
|
12
12
|
maxConcurrent?: number
|
|
13
|
+
/** Max concurrent blocking agents. 0 = unlimited (default). */
|
|
14
|
+
maxConcurrentForeground?: number
|
|
13
15
|
/**
|
|
14
16
|
* 0 = unlimited — the extension's single source of truth for that convention:
|
|
15
17
|
* `normalizeMaxTurns()` in agent-runner.ts treats 0 → `undefined`, and the
|
|
@@ -200,6 +202,10 @@ export interface SubagentsSettings {
|
|
|
200
202
|
* what the parent session counts.
|
|
201
203
|
*/
|
|
202
204
|
showCost?: boolean
|
|
205
|
+
/** Show effective model and thinking level in running widget rows. */
|
|
206
|
+
showModel?: boolean
|
|
207
|
+
/** Markdown scope in the conversation viewer. Defaults to assistant. */
|
|
208
|
+
viewerMarkdown?: ViewerMarkdownMode
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
export type ToolDescriptionMode = "full" | "compact" | "custom"
|
|
@@ -207,6 +213,7 @@ export type ToolDescriptionMode = "full" | "compact" | "custom"
|
|
|
207
213
|
/** Setter hooks used by applySettings to wire persisted values into in-memory state. */
|
|
208
214
|
export interface SettingsAppliers {
|
|
209
215
|
setMaxConcurrent: (n: number) => void
|
|
216
|
+
setMaxConcurrentForeground: (n: number) => void
|
|
210
217
|
setDefaultMaxTurns: (n: number) => void
|
|
211
218
|
setGraceTurns: (n: number) => void
|
|
212
219
|
setDefaultJoinMode: (mode: JoinMode) => void
|
|
@@ -224,6 +231,8 @@ export interface SettingsAppliers {
|
|
|
224
231
|
setFallbackSubagent: (v: string | undefined) => void
|
|
225
232
|
setReportUsage: (b: boolean) => void
|
|
226
233
|
setShowCost: (b: boolean) => void
|
|
234
|
+
setShowModel: (b: boolean) => void
|
|
235
|
+
setViewerMarkdown: (mode: ViewerMarkdownMode) => void
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
@@ -241,6 +250,8 @@ const VALID_WIDGET_MODES: ReadonlySet<string> = new Set<WidgetMode>([
|
|
|
241
250
|
"background",
|
|
242
251
|
"off",
|
|
243
252
|
])
|
|
253
|
+
const VALID_VIEWER_MARKDOWN_MODES: ReadonlySet<string> =
|
|
254
|
+
new Set<ViewerMarkdownMode>(["off", "assistant", "all"])
|
|
244
255
|
|
|
245
256
|
// Sanity ceilings — prevent hand-edited configs from asking for values that
|
|
246
257
|
// make no operational sense (e.g. 1e6 concurrent subagents). Permissive enough
|
|
@@ -262,6 +273,13 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
262
273
|
) {
|
|
263
274
|
out.maxConcurrent = r.maxConcurrent as number
|
|
264
275
|
}
|
|
276
|
+
if (
|
|
277
|
+
Number.isInteger(r.maxConcurrentForeground) &&
|
|
278
|
+
(r.maxConcurrentForeground as number) >= 0 &&
|
|
279
|
+
(r.maxConcurrentForeground as number) <= MAX_CONCURRENT_CEILING
|
|
280
|
+
) {
|
|
281
|
+
out.maxConcurrentForeground = r.maxConcurrentForeground as number
|
|
282
|
+
}
|
|
265
283
|
if (
|
|
266
284
|
Number.isInteger(r.defaultMaxTurns) &&
|
|
267
285
|
(r.defaultMaxTurns as number) >= 0 &&
|
|
@@ -331,6 +349,15 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
331
349
|
if (typeof r.showCost === "boolean") {
|
|
332
350
|
out.showCost = r.showCost
|
|
333
351
|
}
|
|
352
|
+
if (typeof r.showModel === "boolean") {
|
|
353
|
+
out.showModel = r.showModel
|
|
354
|
+
}
|
|
355
|
+
if (
|
|
356
|
+
typeof r.viewerMarkdown === "string" &&
|
|
357
|
+
VALID_VIEWER_MARKDOWN_MODES.has(r.viewerMarkdown)
|
|
358
|
+
) {
|
|
359
|
+
out.viewerMarkdown = r.viewerMarkdown as ViewerMarkdownMode
|
|
360
|
+
}
|
|
334
361
|
if (r.fallbackSubagent === false) {
|
|
335
362
|
// The only non-string spelling worth accepting: a boolean would otherwise be
|
|
336
363
|
// dropped, silently leaving the PERMISSIVE default in place. Every string is
|
|
@@ -407,6 +434,8 @@ export function applySettings(
|
|
|
407
434
|
): void {
|
|
408
435
|
if (typeof s.maxConcurrent === "number")
|
|
409
436
|
appliers.setMaxConcurrent(s.maxConcurrent)
|
|
437
|
+
if (typeof s.maxConcurrentForeground === "number")
|
|
438
|
+
appliers.setMaxConcurrentForeground(s.maxConcurrentForeground)
|
|
410
439
|
if (typeof s.defaultMaxTurns === "number")
|
|
411
440
|
appliers.setDefaultMaxTurns(s.defaultMaxTurns)
|
|
412
441
|
if (typeof s.graceTurns === "number") appliers.setGraceTurns(s.graceTurns)
|
|
@@ -434,6 +463,8 @@ export function applySettings(
|
|
|
434
463
|
appliers.setWorktreeIsolation(s.worktreeIsolation)
|
|
435
464
|
if (typeof s.reportUsage === "boolean") appliers.setReportUsage(s.reportUsage)
|
|
436
465
|
if (typeof s.showCost === "boolean") appliers.setShowCost(s.showCost)
|
|
466
|
+
if (typeof s.showModel === "boolean") appliers.setShowModel(s.showModel)
|
|
467
|
+
if (s.viewerMarkdown) appliers.setViewerMarkdown(s.viewerMarkdown)
|
|
437
468
|
}
|
|
438
469
|
|
|
439
470
|
/**
|
package/src/types.ts
CHANGED
|
@@ -105,6 +105,9 @@ export type JoinMode = "async" | "group" | "smart"
|
|
|
105
105
|
*/
|
|
106
106
|
export type WidgetMode = "all" | "background" | "off"
|
|
107
107
|
|
|
108
|
+
/** How much of the conversation viewer renders as Markdown. */
|
|
109
|
+
export type ViewerMarkdownMode = "off" | "assistant" | "all"
|
|
110
|
+
|
|
108
111
|
export interface AgentRecord {
|
|
109
112
|
id: string
|
|
110
113
|
type: SubagentType
|
|
@@ -125,6 +128,10 @@ export interface AgentRecord {
|
|
|
125
128
|
session?: AgentSession
|
|
126
129
|
abortController?: AbortController
|
|
127
130
|
promise?: Promise<string>
|
|
131
|
+
/** Whether a caller is awaiting this agent inline. */
|
|
132
|
+
blocking?: boolean
|
|
133
|
+
/** Resolves when a queued blocking agent starts or is removed from the queue. */
|
|
134
|
+
startGate?: Promise<void>
|
|
128
135
|
groupId?: string
|
|
129
136
|
joinMode?: JoinMode
|
|
130
137
|
/** Set when result was already consumed via get_subagent_result — suppresses completion notification. */
|
|
@@ -176,10 +183,18 @@ export interface AgentRecord {
|
|
|
176
183
|
rootSessionId?: string
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
/** Effective session level, including pi's display-only `off` value. */
|
|
187
|
+
export type EffectiveThinkingLevel = ThinkingLevel | "off"
|
|
188
|
+
|
|
179
189
|
export interface AgentInvocation {
|
|
180
|
-
/** Short
|
|
190
|
+
/** Short model label for tight UI rows. */
|
|
181
191
|
modelName?: string
|
|
182
|
-
|
|
192
|
+
/** Canonical provider/model id. */
|
|
193
|
+
modelId?: string
|
|
194
|
+
thinking?: EffectiveThinkingLevel
|
|
195
|
+
/** Requested values retained only when the run used something else. */
|
|
196
|
+
requestedThinking?: EffectiveThinkingLevel
|
|
197
|
+
requestedModel?: string
|
|
183
198
|
maxTurns?: number
|
|
184
199
|
isolated?: boolean
|
|
185
200
|
inheritContext?: boolean
|