@jopqior/pi-subagents 1.0.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 +2705 -0
- package/LICENSE +21 -0
- package/README.md +503 -0
- package/dist/public.d.ts +331 -0
- package/dist/settings.d.ts +82 -0
- package/docs/architecture/architecture.md +1566 -0
- package/docs/architecture/client-server-opportunities.md +127 -0
- package/docs/architecture/history/phase-1-api-boundary.md +8 -0
- package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
- package/docs/architecture/history/phase-11-closure-to-class.md +100 -0
- package/docs/architecture/history/phase-12-complexity-test-fixtures.md +55 -0
- package/docs/architecture/history/phase-13-remaining-smells.md +88 -0
- package/docs/architecture/history/phase-14-strip-policy.md +49 -0
- package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
- package/docs/architecture/history/phase-16-invert-dependencies.md +144 -0
- package/docs/architecture/history/phase-17-core-consolidation.md +214 -0
- package/docs/architecture/history/phase-18-reconsider-ui.md +166 -0
- package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
- package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
- package/docs/architecture/history/phase-20-result-delivery.md +245 -0
- package/docs/architecture/history/phase-21-classification-model-boundary.md +107 -0
- package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
- package/docs/architecture/history/phase-4-implement-service.md +8 -0
- package/docs/architecture/history/phase-5-decompose-index.md +42 -0
- package/docs/architecture/history/phase-7-encapsulation.md +173 -0
- package/docs/architecture/history/phase-8-testability.md +103 -0
- package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
- package/docs/comparison-with-upstream.md +77 -0
- package/docs/configuration.md +364 -0
- package/docs/decisions/0001-deferred-patches.md +80 -0
- package/docs/decisions/0002-extensions-on-a-minimal-core.md +125 -0
- package/docs/decisions/0003-publish-bundled-type-declarations.md +71 -0
- package/docs/decisions/0004-reconsider-ui-direction.md +279 -0
- package/docs/decisions/0005-subagent-record-admission-policy.md +106 -0
- package/docs/decisions/0006-inherited-prompt-is-identity-only.md +104 -0
- package/docs/decisions/0007-transcript-viewer-is-not-an-overlay.md +228 -0
- package/docs/decisions/0008-inherited-region-is-shared-parts.md +81 -0
- package/docs/decisions/0009-portable-inheritance-is-provider-scoped.md +116 -0
- package/package.json +91 -0
- package/src/config/agent-types.ts +135 -0
- package/src/config/custom-agents.ts +151 -0
- package/src/config/default-agents.ts +121 -0
- package/src/config/invocation-config.ts +167 -0
- package/src/config/thinking-level.ts +58 -0
- package/src/debug.ts +14 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/interrupt.ts +58 -0
- package/src/handlers/lifecycle.ts +71 -0
- package/src/handlers/widget-events.ts +49 -0
- package/src/index.ts +292 -0
- package/src/layered-settings.ts +105 -0
- package/src/lifecycle/child-lifecycle.ts +115 -0
- package/src/lifecycle/child-shutdown.ts +105 -0
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/create-subagent-session.ts +335 -0
- package/src/lifecycle/parent-snapshot.ts +119 -0
- package/src/lifecycle/run-listeners.ts +37 -0
- package/src/lifecycle/selection-scope.ts +116 -0
- package/src/lifecycle/spawn-selection.ts +259 -0
- package/src/lifecycle/subagent-manager.ts +546 -0
- package/src/lifecycle/subagent-session.ts +347 -0
- package/src/lifecycle/subagent-state.ts +404 -0
- package/src/lifecycle/subagent.ts +885 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/lifecycle/usage.ts +60 -0
- package/src/lifecycle/workspace-bracket.ts +76 -0
- package/src/lifecycle/workspace.ts +46 -0
- package/src/observation/composite-subagent-observer.ts +74 -0
- package/src/observation/notification.ts +430 -0
- package/src/observation/outcome-delivery.ts +239 -0
- package/src/observation/record-observer.ts +78 -0
- package/src/observation/renderer.ts +161 -0
- package/src/observation/subagent-events-observer.ts +148 -0
- package/src/runtime.ts +137 -0
- package/src/service/service-adapter.ts +201 -0
- package/src/service/service.ts +246 -0
- package/src/session/ask-parent-tool.ts +69 -0
- package/src/session/content-items.ts +53 -0
- package/src/session/context.ts +80 -0
- package/src/session/conversation.ts +49 -0
- package/src/session/env.ts +40 -0
- package/src/session/model-resolver.ts +126 -0
- package/src/session/notify-parent-tool.ts +83 -0
- package/src/session/package-exclusions.ts +75 -0
- package/src/session/prompts.ts +231 -0
- package/src/session/provider-inheritance.ts +56 -0
- package/src/session/selection-catalogue.ts +143 -0
- package/src/session/session-config.ts +202 -0
- package/src/session/session-dir.ts +38 -0
- package/src/settings.ts +447 -0
- package/src/tools/agent-tool.ts +305 -0
- package/src/tools/background-spawner.ts +83 -0
- package/src/tools/foreground-runner.ts +159 -0
- package/src/tools/get-result-renderer.ts +119 -0
- package/src/tools/get-result-report.ts +84 -0
- package/src/tools/get-result-tool.ts +192 -0
- package/src/tools/helpers.ts +118 -0
- package/src/tools/result-renderer.ts +153 -0
- package/src/tools/spawn-config.ts +192 -0
- package/src/tools/steer-tool.ts +109 -0
- package/src/types.ts +143 -0
- package/src/ui/agent-widget.ts +333 -0
- package/src/ui/bounded-lines.ts +45 -0
- package/src/ui/display.ts +180 -0
- package/src/ui/glyphs.ts +62 -0
- package/src/ui/session-navigation.ts +150 -0
- package/src/ui/session-navigator.ts +255 -0
- package/src/ui/subagents-settings.ts +179 -0
- package/src/ui/transcript-content.ts +374 -0
- package/src/ui/widget-renderer.ts +301 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selection-catalogue.ts — Authenticated choices and validation for per-spawn selection.
|
|
3
|
+
*
|
|
4
|
+
* The selection path's own view of the spawning parent's model registry: it
|
|
5
|
+
* reads only authenticated availability (`getAvailable()`, never a `getAll()`
|
|
6
|
+
* fallback — a model the session cannot authenticate is not a choice a human
|
|
7
|
+
* can be offered), snapshots the choices for the duration of one selection,
|
|
8
|
+
* and validates the provider's answer against that snapshot plus a fresh
|
|
9
|
+
* availability read before the pair reaches the child factory.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
13
|
+
import { parseThinkingLevel, type SubagentThinkingLevel } from "#src/config/thinking-level";
|
|
14
|
+
import type { SpawnSelection } from "#src/service/service";
|
|
15
|
+
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
16
|
+
|
|
17
|
+
/** The validated pair a gated run carries into the factory. */
|
|
18
|
+
export interface ValidatedSpawnSelection {
|
|
19
|
+
/** The catalogue's own model object, canonicalized from the provider's answer. */
|
|
20
|
+
model: Model<any>;
|
|
21
|
+
/** A level the selected model supports — mandatory, never defaulted. */
|
|
22
|
+
thinkingLevel: SubagentThinkingLevel;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read the authenticated choices for one selection.
|
|
27
|
+
*
|
|
28
|
+
* Missing availability support or an empty catalogue is an error, not a
|
|
29
|
+
* fallback: the ordinary resolver's `getAll()` fallback serves a different
|
|
30
|
+
* constituency (silent inheritance), and a gated run must not offer models the
|
|
31
|
+
* session cannot authenticate.
|
|
32
|
+
*/
|
|
33
|
+
export function readSelectionChoices(registry: ModelRegistry): Model<any>[] {
|
|
34
|
+
const available = registry.getAvailable?.();
|
|
35
|
+
if (available === undefined) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"The model registry does not expose authenticated availability (getAvailable); a model selection cannot be offered.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (available.length === 0) {
|
|
41
|
+
throw new Error("No authenticated models are available to select from.");
|
|
42
|
+
}
|
|
43
|
+
return available;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate the provider's answer for one selection: the model must be in the
|
|
48
|
+
* snapshot (canonicalized to the catalogue's own object) and still available
|
|
49
|
+
* now, and the thinking level must be one the selected model supports.
|
|
50
|
+
*
|
|
51
|
+
* Anything else — an unknown model, a level the model rejects, an omitted
|
|
52
|
+
* level, a forged result — rejects rather than clamping or defaulting.
|
|
53
|
+
*/
|
|
54
|
+
export async function validateSpawnSelection(
|
|
55
|
+
selection: SpawnSelection,
|
|
56
|
+
choices: readonly Model<any>[],
|
|
57
|
+
registry: ModelRegistry,
|
|
58
|
+
): Promise<ValidatedSpawnSelection> {
|
|
59
|
+
const model = canonicalizeModel(selection.model, choices);
|
|
60
|
+
revalidateAvailability(model, registry);
|
|
61
|
+
const thinkingLevel = await validateThinkingLevel(selection.thinkingLevel, model);
|
|
62
|
+
return { model, thinkingLevel };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The signature of pi-ai's supported-level helper, feature-detected at use. */
|
|
66
|
+
type SupportedLevelsHelper = (model: Model<any>) => string[];
|
|
67
|
+
|
|
68
|
+
/** Memoized feature detection of the supported-level helper on the installed peer. */
|
|
69
|
+
let supportedLevelsLoader: Promise<SupportedLevelsHelper | undefined> | undefined;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve pi-ai's `getSupportedThinkingLevels` through the package namespace.
|
|
73
|
+
*
|
|
74
|
+
* The core's peer floor predates the helper, so an unconditional named import
|
|
75
|
+
* would be unavailable on an older supported peer; absence denies only gated
|
|
76
|
+
* execution (the caller reports an error) and never the ordinary path.
|
|
77
|
+
*/
|
|
78
|
+
function loadSupportedLevelsHelper(): Promise<SupportedLevelsHelper | undefined> {
|
|
79
|
+
supportedLevelsLoader ??= import("@earendil-works/pi-ai").then((ns) => {
|
|
80
|
+
const helper = (ns as Record<string, unknown>).getSupportedThinkingLevels;
|
|
81
|
+
return typeof helper === "function" ? (helper as SupportedLevelsHelper) : undefined;
|
|
82
|
+
});
|
|
83
|
+
return supportedLevelsLoader;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Canonicalize the answered model to the catalogue entry with that identity. */
|
|
87
|
+
function canonicalizeModel(model: unknown, choices: readonly Model<any>[]): Model<any> {
|
|
88
|
+
const identity = modelIdentity(model);
|
|
89
|
+
const found = choices.find(
|
|
90
|
+
(candidate) => candidate.provider === identity.provider && candidate.id === identity.id,
|
|
91
|
+
);
|
|
92
|
+
if (!found) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Selected model is not in the available catalogue: "${identity.provider}/${identity.id}".`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Extract a provider/id identity, rejecting answers that do not name a model. */
|
|
101
|
+
function modelIdentity(model: unknown): { provider: string; id: string } {
|
|
102
|
+
if (typeof model !== "object" || model === null) {
|
|
103
|
+
throw new Error("The selection did not name a model.");
|
|
104
|
+
}
|
|
105
|
+
const { provider, id } = model as { provider?: unknown; id?: unknown };
|
|
106
|
+
if (typeof provider !== "string" || typeof id !== "string") {
|
|
107
|
+
throw new Error("The selection did not name a model.");
|
|
108
|
+
}
|
|
109
|
+
return { provider, id };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Re-read availability and confirm the chosen model is still one of the choices. */
|
|
113
|
+
function revalidateAvailability(model: Model<any>, registry: ModelRegistry): void {
|
|
114
|
+
const fresh = registry.getAvailable?.();
|
|
115
|
+
if (!fresh?.some((candidate) => candidate.provider === model.provider && candidate.id === model.id)) {
|
|
116
|
+
throw new Error(`Selected model "${model.provider}/${model.id}" is no longer available.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Validate the answered level against the vocabulary and the selected model. */
|
|
121
|
+
async function validateThinkingLevel(
|
|
122
|
+
level: unknown,
|
|
123
|
+
model: Model<any>,
|
|
124
|
+
): Promise<SubagentThinkingLevel> {
|
|
125
|
+
const parsed = parseThinkingLevel(level);
|
|
126
|
+
if (parsed === undefined) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`The selection did not include a valid thinking level: ${typeof level === "string" ? `"${level}"` : String(level)}.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const supported = await loadSupportedLevelsHelper();
|
|
132
|
+
if (supported === undefined) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"pi-ai's getSupportedThinkingLevels is unavailable on the installed peer; the selected thinking level cannot be validated.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (!supported(model).includes(parsed)) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Model "${model.provider}/${model.id}" does not support thinking level "${parsed}".`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return parsed;
|
|
143
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-config.ts — Pure configuration assembler for agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* `assembleSessionConfig()` is the pure assembly core called by
|
|
5
|
+
* `createSubagentSession()`. It accepts resolved inputs (agent type, narrow
|
|
6
|
+
* context, run options, env info) and returns everything the factory needs to
|
|
7
|
+
* create the SDK session — without importing or constructing any Pi SDK types.
|
|
8
|
+
*
|
|
9
|
+
* The only async IO in the assembly phase (`detectEnv`) is handled by the caller
|
|
10
|
+
* before invoking this function, keeping the assembler synchronous.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
14
|
+
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
15
|
+
import type { EnvInfo } from "#src/session/env";
|
|
16
|
+
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
17
|
+
import type { InheritedPrompt } from "#src/session/prompts";
|
|
18
|
+
import type {
|
|
19
|
+
AgentPromptConfig,
|
|
20
|
+
PromptInheritance,
|
|
21
|
+
SubagentType,
|
|
22
|
+
ThinkingLevel,
|
|
23
|
+
} from "#src/types";
|
|
24
|
+
|
|
25
|
+
// ── Public interfaces ────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* IO collaborators injected into `assembleSessionConfig`.
|
|
29
|
+
*
|
|
30
|
+
* Bundling the IO-touching (or promptly testable) function into a single
|
|
31
|
+
* interface keeps the assembler free of direct module imports and makes it
|
|
32
|
+
* trivially testable without `vi.mock()` — callers inject real implementations
|
|
33
|
+
* at the edge (`create-subagent-session.ts`) or stubs in tests.
|
|
34
|
+
*/
|
|
35
|
+
export interface AssemblerIO {
|
|
36
|
+
buildAgentPrompt: (
|
|
37
|
+
config: AgentPromptConfig,
|
|
38
|
+
cwd: string,
|
|
39
|
+
env: EnvInfo,
|
|
40
|
+
inherited?: InheritedPrompt,
|
|
41
|
+
) => string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Narrow context the assembler reads from the parent session.
|
|
46
|
+
* Tests construct plain objects satisfying this interface — no SDK mocking needed.
|
|
47
|
+
*
|
|
48
|
+
* The assembler never inspects model internals, only passes them through;
|
|
49
|
+
* `resolveDefaultModel` reads `provider`/`id` from `getAvailable()` for the
|
|
50
|
+
* availability check.
|
|
51
|
+
*/
|
|
52
|
+
export interface AssemblerContext {
|
|
53
|
+
/** Parent working directory (overridable via options.cwd). */
|
|
54
|
+
cwd: string;
|
|
55
|
+
/** Parent's effective system prompt (for append-mode agents). */
|
|
56
|
+
parentSystemPrompt: string;
|
|
57
|
+
/** Parent's operator-authored parts, for a child on a re-homing provider. */
|
|
58
|
+
parentPortablePrompt?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Which prompt-inheritance strategy the child's provider calls for.
|
|
61
|
+
* Absent resolves every child to `"full"`.
|
|
62
|
+
*/
|
|
63
|
+
resolvePromptInheritance?: (provider: string | undefined) => PromptInheritance;
|
|
64
|
+
/** Parent's current model instance (fallback when agent config has no model). */
|
|
65
|
+
parentModel?: Model<any>;
|
|
66
|
+
/** Model registry for resolving config.model strings. */
|
|
67
|
+
modelRegistry: ModelRegistry;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Narrow slice of per-spawn execution fields consumed by the assembler.
|
|
72
|
+
* All fields are optional — callers pass only what they have.
|
|
73
|
+
*/
|
|
74
|
+
export interface AssemblerOptions {
|
|
75
|
+
/** Override working directory (e.g. for worktree isolation). */
|
|
76
|
+
cwd?: string;
|
|
77
|
+
/** Explicit model override — wins over agentConfig.model and parent model. */
|
|
78
|
+
model?: Model<any>;
|
|
79
|
+
/** Explicit thinking level — wins over agentConfig.thinking. */
|
|
80
|
+
thinkingLevel?: ThinkingLevel;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Assembled configuration returned to `createSubagentSession()`.
|
|
85
|
+
* Contains everything needed to create the SDK session and filter tools —
|
|
86
|
+
* with no SDK object references.
|
|
87
|
+
*/
|
|
88
|
+
export interface SessionConfig {
|
|
89
|
+
/** Resolved working directory (`options.cwd ?? ctx.cwd`). */
|
|
90
|
+
effectiveCwd: string;
|
|
91
|
+
/** Fully-assembled system prompt string (ready for `systemPromptOverride`). */
|
|
92
|
+
systemPrompt: string;
|
|
93
|
+
/** Built-in tool name allowlist for this agent type. */
|
|
94
|
+
toolNames: string[];
|
|
95
|
+
/**
|
|
96
|
+
* Resolved model instance (undefined → use parent model as passed to SDK).
|
|
97
|
+
* The assembler passes it through without inspection.
|
|
98
|
+
*/
|
|
99
|
+
model: Model<any> | undefined;
|
|
100
|
+
/** Resolved thinking level (undefined → inherit from session). */
|
|
101
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
102
|
+
/** Per-agent configured max turns (from agentConfig.maxTurns). */
|
|
103
|
+
agentMaxTurns: number | undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the default model from the agent config's model string.
|
|
110
|
+
*
|
|
111
|
+
* Priority: parentModel is the fallback; if `configModel` is a "provider/modelId"
|
|
112
|
+
* string that resolves against the registry AND is in the available set, return
|
|
113
|
+
* that model instead.
|
|
114
|
+
*/
|
|
115
|
+
function resolveDefaultModel(
|
|
116
|
+
parentModel: Model<any> | undefined,
|
|
117
|
+
registry: AssemblerContext["modelRegistry"],
|
|
118
|
+
configModel?: string,
|
|
119
|
+
): Model<any> | undefined {
|
|
120
|
+
if (configModel) {
|
|
121
|
+
const slashIdx = configModel.indexOf("/");
|
|
122
|
+
if (slashIdx !== -1) {
|
|
123
|
+
const provider = configModel.slice(0, slashIdx);
|
|
124
|
+
const modelId = configModel.slice(slashIdx + 1);
|
|
125
|
+
|
|
126
|
+
const available = registry.getAvailable?.();
|
|
127
|
+
const availableKeys = available
|
|
128
|
+
? new Set(available.map((m) => `${m.provider}/${m.id}`))
|
|
129
|
+
: undefined;
|
|
130
|
+
const isAvailable = (p: string, id: string) =>
|
|
131
|
+
!availableKeys || availableKeys.has(`${p}/${id}`);
|
|
132
|
+
|
|
133
|
+
const found = registry.find(provider, modelId);
|
|
134
|
+
if (found && isAvailable(provider, modelId)) return found;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return parentModel;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Public function ──────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Assemble all configuration needed to create an agent session.
|
|
144
|
+
*
|
|
145
|
+
* Synchronous and side-effect-free — all IO is delegated through the `io`
|
|
146
|
+
* parameter. The caller is responsible for resolving `EnvInfo` beforehand
|
|
147
|
+
* via `detectEnv()`.
|
|
148
|
+
*
|
|
149
|
+
* @param type The subagent type name (case-insensitive registry lookup).
|
|
150
|
+
* @param ctx Narrow context from the parent session.
|
|
151
|
+
* @param options Per-call overrides (cwd, model, thinkingLevel).
|
|
152
|
+
* @param env Pre-resolved environment info from `detectEnv()`.
|
|
153
|
+
* @param registry Agent config lookup — provides resolveAgentConfig and getToolNamesForType.
|
|
154
|
+
* @param io IO collaborators (skill loader, memory builder, prompt builder).
|
|
155
|
+
*/
|
|
156
|
+
export function assembleSessionConfig(
|
|
157
|
+
type: SubagentType,
|
|
158
|
+
ctx: AssemblerContext,
|
|
159
|
+
options: AssemblerOptions,
|
|
160
|
+
env: EnvInfo,
|
|
161
|
+
registry: AgentConfigLookup,
|
|
162
|
+
io: AssemblerIO,
|
|
163
|
+
): SessionConfig {
|
|
164
|
+
const agentConfig = registry.resolveAgentConfig(type);
|
|
165
|
+
|
|
166
|
+
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
167
|
+
|
|
168
|
+
const toolNames = registry.getToolNamesForType(type);
|
|
169
|
+
|
|
170
|
+
// Model resolution: explicit option > config model string > parent model.
|
|
171
|
+
// Resolved before the prompt because the child's provider is what selects a
|
|
172
|
+
// prompt-inheritance strategy; the two computations are otherwise
|
|
173
|
+
// independent, so the order is free.
|
|
174
|
+
const model =
|
|
175
|
+
options.model ??
|
|
176
|
+
resolveDefaultModel(ctx.parentModel, ctx.modelRegistry, agentConfig.model);
|
|
177
|
+
|
|
178
|
+
// Build system prompt from the resolved agent config. The strategy is keyed
|
|
179
|
+
// on the child's own provider, so a per-spawn model override moves the child
|
|
180
|
+
// between transports and takes the right strategy with it.
|
|
181
|
+
const systemPrompt = io.buildAgentPrompt(agentConfig, effectiveCwd, env, {
|
|
182
|
+
systemPrompt: ctx.parentSystemPrompt,
|
|
183
|
+
cwd: ctx.cwd,
|
|
184
|
+
strategy: ctx.resolvePromptInheritance?.(model?.provider) ?? "full",
|
|
185
|
+
portablePrompt: ctx.parentPortablePrompt,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Thinking level: explicit option > agent config > undefined (inherit)
|
|
189
|
+
const thinkingLevel = options.thinkingLevel ?? agentConfig.thinking;
|
|
190
|
+
|
|
191
|
+
// Per-agent max turns (combined with per-call maxTurns and defaultMaxTurns by SubagentSession.runTurnLoop)
|
|
192
|
+
const agentMaxTurns = agentConfig.maxTurns;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
effectiveCwd,
|
|
196
|
+
systemPrompt,
|
|
197
|
+
toolNames,
|
|
198
|
+
model,
|
|
199
|
+
thinkingLevel,
|
|
200
|
+
agentMaxTurns,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-dir.ts — Pure function for deriving subagent session directories.
|
|
3
|
+
*
|
|
4
|
+
* Subagent sessions are nested under the parent session's basename so they are
|
|
5
|
+
* discoverable via the parent session path without cluttering the main session list.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { basename, dirname, join } from "node:path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Derive the session directory for a subagent from the parent session file.
|
|
13
|
+
*
|
|
14
|
+
* Layout: `<parent-dir>/<parent-basename>/tasks/`
|
|
15
|
+
*
|
|
16
|
+
* Example:
|
|
17
|
+
* parent: `~/.pi/agent/sessions/--project--/2026-05-20T12-00-00Z_.jsonl`
|
|
18
|
+
* result: `~/.pi/agent/sessions/--project--/2026-05-20T12-00-00Z_/tasks`
|
|
19
|
+
*
|
|
20
|
+
* Falls back to a temp directory when the parent session is not persisted
|
|
21
|
+
* (e.g. API/headless mode where the parent uses `SessionManager.inMemory()`).
|
|
22
|
+
*/
|
|
23
|
+
export function deriveSubagentSessionDir(
|
|
24
|
+
parentSessionFile: string | undefined,
|
|
25
|
+
cwd: string,
|
|
26
|
+
): string {
|
|
27
|
+
if (parentSessionFile) {
|
|
28
|
+
const dir = dirname(parentSessionFile);
|
|
29
|
+
const base = basename(parentSessionFile, ".jsonl");
|
|
30
|
+
return join(dir, base, "tasks");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback: use a temp directory keyed by uid and cwd so different
|
|
34
|
+
// projects don't collide when the parent session is not persisted.
|
|
35
|
+
const encoded = cwd.replace(/[/\\]/g, "-").replace(/^[A-Za-z]:-/, "").replace(/^-+/, "");
|
|
36
|
+
const root = join(tmpdir(), `pi-subagents-${process.getuid?.() ?? 0}`);
|
|
37
|
+
return join(root, encoded, "tasks");
|
|
38
|
+
}
|