@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/mention-clone.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* mention-clone.ts — start a mentioned agent through a
|
|
3
|
-
*
|
|
2
|
+
* mention-clone.ts — start a mentioned agent through a hidden, mention-only
|
|
3
|
+
* session without putting anything in the chat.
|
|
4
4
|
*
|
|
5
5
|
* Claude Code routes `@agent-<type>` through the main model: the mention
|
|
6
6
|
* becomes a `<system-reminder>` appended to the prompt and the model makes the
|
|
@@ -9,27 +9,13 @@
|
|
|
9
9
|
* reasoning and its tool block land in the transcript, for a decision the user
|
|
10
10
|
* already made when they typed the handle.
|
|
11
11
|
*
|
|
12
|
-
* So the turn happens somewhere else.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* on: `SessionManager._persist` withholds every write until the first assistant
|
|
20
|
-
* message lands, so a fork taken before then reads an empty file and throws.
|
|
21
|
-
* `buildSessionContext()` has no such timing, and is compaction-aware — it walks
|
|
22
|
-
* the leaf path and substitutes the summary for entries folded into it, so a
|
|
23
|
-
* long conversation clones as what the main model is actually working from. A
|
|
24
|
-
* conversation with nothing in it yet clones to nothing in it yet, which is the
|
|
25
|
-
* correct answer rather than a failure.
|
|
26
|
-
*
|
|
27
|
-
* It is also the oldest of the equivalent Pi APIs — `buildContextEntries` on
|
|
28
|
-
* ReadonlySessionManager and the `sessionEntryToContextMessages` export both
|
|
29
|
-
* arrived in 0.80.5 — where this one has been exported unchanged from before
|
|
30
|
-
* the declared peer floor, and is the same code path (`byId` is only an index
|
|
31
|
-
* cache, so passing it or not cannot change the result). Keeping the floor
|
|
32
|
-
* honest costs nothing here: see the `compat-floor-pi` job.
|
|
12
|
+
* So the turn happens somewhere else. A throwaway in-memory session takes it
|
|
13
|
+
* off-screen on the parent's model, but receives only this mention's text.
|
|
14
|
+
* Pi's public ExtensionContext exposes a pre-hook SessionManager projection,
|
|
15
|
+
* not the parent's request-local context hooks (including programmatic hooks).
|
|
16
|
+
* Copying even a compacted or edited branch could send private parent content
|
|
17
|
+
* to the clone's provider before its redaction runs. The clone must start empty:
|
|
18
|
+
* no parent branch, system prompt, tool declarations, or summaries.
|
|
33
19
|
*
|
|
34
20
|
* Its `thinkingLevel` is NOT used, and is the one place the newer API would be
|
|
35
21
|
* better. `getSessionContextSettings` starts at "off" and moves only on an
|
|
@@ -63,19 +49,28 @@
|
|
|
63
49
|
|
|
64
50
|
import type { Model } from "@earendil-works/pi-ai";
|
|
65
51
|
import {
|
|
66
|
-
buildSessionContext,
|
|
67
52
|
createAgentSession,
|
|
53
|
+
DefaultResourceLoader,
|
|
68
54
|
type ExtensionContext,
|
|
55
|
+
getAgentDir,
|
|
69
56
|
SessionManager,
|
|
70
57
|
type ToolDefinition,
|
|
71
58
|
} from "@earendil-works/pi-coding-agent";
|
|
72
59
|
import { runInChildSessionContext } from "./child-context.js";
|
|
73
|
-
import {
|
|
60
|
+
import { parentModelSessionOptions } from "./model-runtime-bridge.js";
|
|
74
61
|
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
75
62
|
|
|
63
|
+
const MENTION_SYSTEM_PROMPT =
|
|
64
|
+
"You write a task prompt for the Agent tool using only the user's current message. " +
|
|
65
|
+
"Call Agent once to start the requested task. You have no prior conversation context; " +
|
|
66
|
+
"do not invent or request inherited context. If details are missing, pass the user's words through.";
|
|
67
|
+
|
|
68
|
+
/** Internal acknowledgement: the real Agent handler has returned from manager.spawn. */
|
|
69
|
+
export const MENTION_SPAWNED = Symbol("pi-subagents:mention-spawned");
|
|
70
|
+
|
|
76
71
|
export interface MentionCloneOptions {
|
|
77
|
-
/** The MAIN session's context — what the spawn is attributed to,
|
|
78
|
-
* source of
|
|
72
|
+
/** The MAIN session's context — what the spawn is attributed to, not a
|
|
73
|
+
* source of provider history or instructions for the hidden turn. */
|
|
79
74
|
ctx: ExtensionContext;
|
|
80
75
|
/** Agent type the handle resolved to. */
|
|
81
76
|
type: SubagentType;
|
|
@@ -83,79 +78,120 @@ export interface MentionCloneOptions {
|
|
|
83
78
|
message: string;
|
|
84
79
|
/** The registered `Agent` tool, reused so the spawn is an ordinary one. */
|
|
85
80
|
agentTool: ToolDefinition;
|
|
81
|
+
/** False once the originating session or branch has been replaced. */
|
|
82
|
+
isOriginCurrent: () => boolean;
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
export interface MentionCloneResult {
|
|
89
|
-
/** True once the
|
|
86
|
+
/** True once the registered Agent handler confirmed a child was started. */
|
|
90
87
|
spawned: boolean;
|
|
88
|
+
/** The Agent handler ran but refused the spawn; direct fallback must not bypass its policy. */
|
|
89
|
+
refused?: boolean;
|
|
91
90
|
/** Why not, when it didn't. Absent on success. */
|
|
92
91
|
error?: string;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
/**
|
|
96
|
-
*
|
|
95
|
+
* Let a mention-only throwaway session make the tool call, then discard it.
|
|
97
96
|
* Never rejects: a clone that cannot run is reported so the caller can fall
|
|
98
97
|
* back to starting the agent directly.
|
|
99
98
|
*/
|
|
100
99
|
export async function runMentionClone(opts: MentionCloneOptions): Promise<MentionCloneResult> {
|
|
101
|
-
const { ctx, type, message, agentTool } = opts;
|
|
100
|
+
const { ctx, type, message, agentTool, isOriginCurrent } = opts;
|
|
102
101
|
|
|
103
102
|
let spawned = false;
|
|
103
|
+
let refused = false;
|
|
104
|
+
let attempted = false;
|
|
104
105
|
const cloneAgentTool: ToolDefinition = {
|
|
105
106
|
...agentTool,
|
|
106
|
-
execute: (_cloneToolCallId, params, signal, onUpdate, _cloneCtx) => {
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
if (
|
|
111
|
-
return
|
|
112
|
-
content: [{ type: "text" as const, text: "Already
|
|
107
|
+
execute: async (_cloneToolCallId, params, signal, onUpdate, _cloneCtx) => {
|
|
108
|
+
// The model may ask for another type or a schedule, but the user chose
|
|
109
|
+
// exactly one agent by typing its handle. Never let the hidden turn widen
|
|
110
|
+
// that decision or attempt another spawn after a failed call.
|
|
111
|
+
if (attempted) {
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text" as const, text: "Already attempted an agent for this mention. Stop here." }],
|
|
113
114
|
details: undefined,
|
|
114
115
|
isError: true,
|
|
115
|
-
}
|
|
116
|
+
};
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
118
|
+
attempted = true;
|
|
119
|
+
if (!isOriginCurrent()) {
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: "text" as const, text: "The original session changed. Do not start this agent." }],
|
|
122
|
+
details: undefined,
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const choice = params as Record<string, unknown>;
|
|
127
|
+
const selected = {
|
|
128
|
+
subagent_type: type,
|
|
129
|
+
prompt: typeof choice.prompt === "string" && choice.prompt.trim() ? choice.prompt : message,
|
|
130
|
+
...(typeof choice.description === "string" && { description: choice.description }),
|
|
131
|
+
run_in_background: true,
|
|
132
|
+
// The handler acknowledges immediately after manager.spawn returns,
|
|
133
|
+
// before UI/event side effects can throw. A tool result alone cannot
|
|
134
|
+
// distinguish a pre-spawn rejection from a post-spawn exception.
|
|
135
|
+
[MENTION_SPAWNED]: () => { spawned = true; },
|
|
136
|
+
} as typeof params;
|
|
137
|
+
// A foreground result would be delivered only into the discarded clone.
|
|
138
|
+
// Attribute the background spawn to the real session, with no dangling
|
|
139
|
+
// tool-call id. A plain text rejection is not a successful spawn.
|
|
140
|
+
// Pi 0.99 tool handlers require a tool context, not the event-handler
|
|
141
|
+
// context captured from the parent. Keep every session-bound field from
|
|
142
|
+
// the parent, and supply the real clone call's nested-tool capabilities;
|
|
143
|
+
// Agent itself does not use those capabilities. Never pretend the parent
|
|
144
|
+
// has executeTool(), which only exists during an actual tool invocation.
|
|
145
|
+
const mainToolCtx = { ...ctx, tools: _cloneCtx.tools, executeTool: _cloneCtx.executeTool };
|
|
146
|
+
const result = await agentTool.execute(undefined as never, selected, signal, onUpdate, mainToolCtx);
|
|
147
|
+
const details = result.details as { agentId?: unknown; status?: unknown } | undefined;
|
|
148
|
+
spawned ||= typeof details?.agentId === "string" &&
|
|
149
|
+
(details.status === "background" || details.status === "queued");
|
|
150
|
+
refused = !spawned;
|
|
151
|
+
return result;
|
|
130
152
|
},
|
|
131
153
|
};
|
|
132
154
|
|
|
133
155
|
let session: Awaited<ReturnType<typeof createAgentSession>>["session"] | undefined;
|
|
134
156
|
try {
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
157
|
+
// Refuse before loading or prompting when a modern host cannot bridge the
|
|
158
|
+
// parent's providers/virtual routes into this throwaway session.
|
|
159
|
+
// The hidden clone inherits exactly this model; unlike an explicit child
|
|
160
|
+
// selection, a stale parent model cannot be replaced before its request.
|
|
161
|
+
const parentModels = parentModelSessionOptions(ctx, ctx.model);
|
|
162
|
+
// An empty manager works on both old Pi hosts (which read agent state) and
|
|
163
|
+
// new ones (which project the manager). Never inspect the parent's branch:
|
|
164
|
+
// its pre-hook content cannot safely seed a separate provider request.
|
|
165
|
+
const cloneManager = SessionManager.inMemory(ctx.cwd);
|
|
145
166
|
// Pi 0.82.0 added this; below it the field is absent and the clone takes
|
|
146
167
|
// the settings level instead, which is what a session that never ran
|
|
147
168
|
// `/think` is on anyway. Same shim shape as `modelRuntime` below.
|
|
148
169
|
const thinkingLevel = (ctx as { thinkingLevel?: ThinkingLevel }).thinkingLevel;
|
|
149
|
-
|
|
150
|
-
|
|
170
|
+
// No project or inline extensions run in the hidden session. The parent's
|
|
171
|
+
// programmatic context hooks cannot be enumerated from ExtensionContext,
|
|
172
|
+
// so replaying only file-backed hooks would be an unsafe partial policy.
|
|
173
|
+
const loader = new DefaultResourceLoader({
|
|
174
|
+
cwd: ctx.cwd,
|
|
175
|
+
agentDir: getAgentDir(),
|
|
176
|
+
noExtensions: true,
|
|
177
|
+
noSkills: true,
|
|
178
|
+
noPromptTemplates: true,
|
|
179
|
+
noThemes: true,
|
|
180
|
+
noContextFiles: true,
|
|
181
|
+
systemPromptOverride: () => MENTION_SYSTEM_PROMPT,
|
|
182
|
+
appendSystemPromptOverride: () => [],
|
|
183
|
+
});
|
|
184
|
+
const created = await runInChildSessionContext(async () => {
|
|
185
|
+
await loader.reload();
|
|
186
|
+
return createAgentSession({
|
|
151
187
|
cwd: ctx.cwd,
|
|
152
188
|
// Nothing about the copy is worth persisting, and an in-memory manager
|
|
153
189
|
// is also what keeps the real session untouched.
|
|
154
|
-
sessionManager:
|
|
190
|
+
sessionManager: cloneManager,
|
|
191
|
+
resourceLoader: loader,
|
|
155
192
|
model: ctx.model as Model<never> | undefined,
|
|
156
193
|
...(thinkingLevel && { thinkingLevel }),
|
|
157
|
-
|
|
158
|
-
...(parentModelRuntime !== undefined && { modelRuntime: parentModelRuntime as never }),
|
|
194
|
+
...parentModels,
|
|
159
195
|
// An allowlist naming exactly the clone's own tool. NOT `noTools:
|
|
160
196
|
// "all"`, whose doc comment ("start with no tools enabled") reads like
|
|
161
197
|
// it spares custom tools and does not: it resolves to an EMPTY
|
|
@@ -166,24 +202,32 @@ export async function runMentionClone(opts: MentionCloneOptions): Promise<Mentio
|
|
|
166
202
|
// agent-runner's `tools: sessionTools` beside its nested `customTools`.
|
|
167
203
|
tools: [cloneAgentTool.name],
|
|
168
204
|
customTools: [cloneAgentTool],
|
|
169
|
-
} as Parameters<typeof createAgentSession>[0])
|
|
170
|
-
);
|
|
205
|
+
} as Parameters<typeof createAgentSession>[0]);
|
|
206
|
+
});
|
|
171
207
|
session = created.session;
|
|
208
|
+
// Loading resources and creating the session both yield. If the parent was
|
|
209
|
+
// replaced during either step, never make a provider request for a stale
|
|
210
|
+
// mention. The tool-level check below still fences a switch during streaming.
|
|
211
|
+
if (!isOriginCurrent()) return { spawned: false, error: "the original session changed" };
|
|
172
212
|
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// is
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
213
|
+
// A session switch during a slow provider stream may produce no tool call
|
|
214
|
+
// for minutes. The tool fence below prevents a stale spawn, but without
|
|
215
|
+
// cancelling the clone that hidden request keeps running after its parent
|
|
216
|
+
// is gone. Check while the prompt is pending and abort the throwaway turn.
|
|
217
|
+
const clone = session;
|
|
218
|
+
let abortRequested = false;
|
|
219
|
+
const staleCheck = setInterval(() => {
|
|
220
|
+
if (abortRequested || isOriginCurrent()) return;
|
|
221
|
+
abortRequested = true;
|
|
222
|
+
void clone.abort().catch(() => {});
|
|
223
|
+
}, 100);
|
|
224
|
+
try {
|
|
225
|
+
// Only the current mention text reaches the hidden provider request.
|
|
226
|
+
// The selected type is enforced by the Agent wrapper, not extra history.
|
|
227
|
+
await clone.prompt(message);
|
|
228
|
+
} finally {
|
|
229
|
+
clearInterval(staleCheck);
|
|
230
|
+
}
|
|
187
231
|
} catch (err) {
|
|
188
232
|
return { spawned, error: err instanceof Error ? err.message : String(err) };
|
|
189
233
|
} finally {
|
|
@@ -192,5 +236,7 @@ export async function runMentionClone(opts: MentionCloneOptions): Promise<Mentio
|
|
|
192
236
|
|
|
193
237
|
return spawned
|
|
194
238
|
? { spawned: true }
|
|
195
|
-
:
|
|
239
|
+
: refused
|
|
240
|
+
? { spawned: false, refused: true, error: "the Agent tool refused this mention" }
|
|
241
|
+
: { spawned: false, error: "the conversation clone did not start it" };
|
|
196
242
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ExtensionContext, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
function isSameModel(candidate: unknown, selected: { provider: string; id: string }): boolean {
|
|
5
|
+
if (!candidate || typeof candidate !== "object") return false;
|
|
6
|
+
const model = candidate as { provider?: unknown; id?: unknown };
|
|
7
|
+
return model.provider === selected.provider && model.id === selected.id;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Pi 0.80.8+ requires the parent's ModelRuntime, not just its public registry
|
|
12
|
+
* facade. ExtensionContext does not expose that runtime. Keep the one private
|
|
13
|
+
* compatibility read here until Pi provides a public accessor. Passing no
|
|
14
|
+
* runtime on a modern host silently creates a fresh one, losing extension
|
|
15
|
+
* providers, credentials and virtual routes before the first child request.
|
|
16
|
+
* Without selectedModel, check only the runtime's shape: the parent ctx.model
|
|
17
|
+
* may be stale while the child explicitly selects a valid physical model.
|
|
18
|
+
*/
|
|
19
|
+
export function parentModelSessionOptions(
|
|
20
|
+
ctx: ExtensionContext,
|
|
21
|
+
selectedModel?: { provider: string; id: string },
|
|
22
|
+
): {
|
|
23
|
+
modelRegistry: ExtensionContext["modelRegistry"];
|
|
24
|
+
modelRuntime?: ModelRuntime;
|
|
25
|
+
} {
|
|
26
|
+
const modelRegistry = ctx.modelRegistry;
|
|
27
|
+
const modernHost = typeof PiCodingAgent.ModelRuntime?.create === "function";
|
|
28
|
+
let candidate: unknown;
|
|
29
|
+
try {
|
|
30
|
+
candidate = (modelRegistry as unknown as { runtime?: unknown }).runtime;
|
|
31
|
+
} catch {
|
|
32
|
+
// Treat an inaccessible private bridge exactly like a missing one; never
|
|
33
|
+
// include accessor errors, which might contain provider configuration.
|
|
34
|
+
}
|
|
35
|
+
let compatible = false;
|
|
36
|
+
try {
|
|
37
|
+
if (candidate !== null && typeof candidate === "object") {
|
|
38
|
+
const runtime = candidate as Record<string, unknown>;
|
|
39
|
+
// Pi 0.84/0.87 already have ModelRuntime.create but no resolveModel;
|
|
40
|
+
// virtual routing is a 0.99 feature. Check the shared child-session
|
|
41
|
+
// capabilities available across all supported runtime generations.
|
|
42
|
+
compatible = typeof runtime.getAuth === "function" && typeof runtime.stream === "function" &&
|
|
43
|
+
typeof runtime.getModel === "function" && typeof runtime.streamSimple === "function";
|
|
44
|
+
if (compatible && modernHost && selectedModel) {
|
|
45
|
+
const model = (runtime.getModel as (provider: string, id: string) => unknown)(selectedModel.provider, selectedModel.id);
|
|
46
|
+
compatible = isSameModel(model, selectedModel);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
compatible = false;
|
|
51
|
+
}
|
|
52
|
+
if (modernHost && !compatible) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Cannot start a child session: Pi exposes ModelRuntime.create but the parent's model runtime is unavailable or incompatible. " +
|
|
55
|
+
"Update Pi to a host that exposes the parent runtime to extensions, or disable subagent/mention dispatch; " +
|
|
56
|
+
"a fresh runtime would lose custom providers and virtual model routes.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
modelRegistry, // older supported hosts still consume this option
|
|
61
|
+
...(compatible ? { modelRuntime: candidate as ModelRuntime } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Admit a spawn before allocating any identity; final model validation stays in runAgent. */
|
|
66
|
+
export function assertParentModelRuntimeAvailable(ctx: ExtensionContext): void {
|
|
67
|
+
parentModelSessionOptions(ctx);
|
|
68
|
+
}
|
package/src/model-scope.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function isScopeModelsEnabled(): boolean { return scopeModelsEnabled; }
|
|
|
22
22
|
export function setScopeModelsEnabled(enabled: boolean): void { scopeModelsEnabled = enabled; }
|
|
23
23
|
|
|
24
24
|
export type ModelScopeVerdict =
|
|
25
|
-
/** In scope, or nothing to validate against (feature off / no allowlist). */
|
|
25
|
+
/** In scope, or nothing to validate against (feature off / no configured allowlist). */
|
|
26
26
|
| { kind: "ok" }
|
|
27
27
|
/** Caller-supplied out-of-scope choice — refuse the spawn with this message. */
|
|
28
28
|
| { kind: "error"; message: string }
|
|
@@ -60,7 +60,9 @@ export function checkModelScope(args: {
|
|
|
60
60
|
// `Model not in scope: "undefined"`, which tells the caller nothing it can act on.
|
|
61
61
|
const modelLabel = modelInput ?? `${model.provider}/${model.id}`;
|
|
62
62
|
if (callerSupplied) {
|
|
63
|
-
const list =
|
|
63
|
+
const list = allowed.size > 0
|
|
64
|
+
? [...allowed].sort().map(m => ` ${m}`).join("\n")
|
|
65
|
+
: " (no configured models are currently available or supported)";
|
|
64
66
|
return {
|
|
65
67
|
kind: "error",
|
|
66
68
|
message: `Model not in scope: "${modelLabel}".\n\nAllowed models (from enabledModels):\n${list}`,
|
package/src/nested-tools.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
resolveEnabledTypeIn,
|
|
17
17
|
resolveTypeIn,
|
|
18
18
|
} from "./agent-types.js";
|
|
19
|
+
import { INHERIT_CONTEXT_UNAVAILABLE } from "./context-boundary.js";
|
|
19
20
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
20
21
|
import { resolveAgentInvocationConfig } from "./invocation-config.js";
|
|
21
22
|
import { resolveModel } from "./model-resolver.js";
|
|
@@ -176,7 +177,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
|
|
|
176
177
|
run_in_background: Type.Optional(Type.Boolean()),
|
|
177
178
|
resume: Type.Optional(Type.String({ description: "Resume a nested agent owned by this parent." })),
|
|
178
179
|
isolated: Type.Optional(Type.Boolean()),
|
|
179
|
-
inherit_context: Type.Optional(Type.Boolean()),
|
|
180
|
+
inherit_context: Type.Optional(Type.Boolean({ description: "Unavailable: true is rejected. Put an explicitly sanitized summary in the task prompt instead." })),
|
|
180
181
|
isolation: Type.Optional(Type.Literal("worktree")),
|
|
181
182
|
}),
|
|
182
183
|
execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
|
|
@@ -228,6 +229,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
|
|
|
228
229
|
...params,
|
|
229
230
|
agentTiers: getAgentTiersSettings(),
|
|
230
231
|
});
|
|
232
|
+
if (invocation.inheritContext) return textResult(INHERIT_CONTEXT_UNAVAILABLE, true);
|
|
231
233
|
// Leave model/thinking unset whenever an Agent tier may apply. runAgent
|
|
232
234
|
// is the sole final resolver; pre-resolving here would let a parent or
|
|
233
235
|
// legacy field bypass the selected profile.
|
package/src/types.ts
CHANGED
|
@@ -42,10 +42,12 @@ export interface AgentConfig {
|
|
|
42
42
|
*/
|
|
43
43
|
color?: string;
|
|
44
44
|
/**
|
|
45
|
-
* Tools whose
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* Tools whose first call needs human approval for the child session
|
|
46
|
+
* (`ask_tools:`), including resumed turns of this in-memory child. A reopened
|
|
47
|
+
* child must ask again. The third answer between
|
|
48
|
+
* `tools:` and `disallowed_tools:` for tools that are usually fine and
|
|
49
|
+
* occasionally not. Approval comes from the human, never from a model —
|
|
50
|
+
* see `ask-tools.ts`.
|
|
49
51
|
*/
|
|
50
52
|
askTools?: string[];
|
|
51
53
|
/**
|