@gotgenes/pi-subagents 4.0.0 → 4.1.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/docs/plans/0053-extract-model-resolution-from-execute.md +181 -0
- package/package.json +2 -2
- package/src/index.ts +9 -11
- package/src/model-resolver.ts +39 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v4.0.0...pi-subagents-v4.1.0) (2026-05-18)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add resolveInvocationModel to model-resolver ([462b519](https://github.com/gotgenes/pi-packages/commit/462b5194fdfdf86d8d2d166a99472c651e00b76b))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* remove quotes from rumdl glob patterns in lint:md script ([a8a0c62](https://github.com/gotgenes/pi-packages/commit/a8a0c62feb2fc45cf68cd7d777259dc159de671b))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
* plan extract model resolution from Agent.execute ([#53](https://github.com/gotgenes/pi-packages/issues/53)) ([4c07a47](https://github.com/gotgenes/pi-packages/commit/4c07a474f9f25043a2fa3a4f2829e97eb9bb7666))
|
|
24
|
+
* **retro:** add retro notes for issue [#48](https://github.com/gotgenes/pi-packages/issues/48) ([f244c04](https://github.com/gotgenes/pi-packages/commit/f244c04c64f768e724e89d77962f2fb63715b998))
|
|
25
|
+
|
|
8
26
|
## [4.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v3.0.0...pi-subagents-v4.0.0) (2026-05-17)
|
|
9
27
|
|
|
10
28
|
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 53
|
|
3
|
+
issue_title: "refactor: extract model resolution from Agent.execute"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extract model resolution from Agent.execute
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
The `Agent` tool's `execute` callback in `index.ts` contains inline model-resolution logic (~lines 660–670) that determines which model an agent runs with.
|
|
11
|
+
This block checks `resolvedConfig.modelInput`, calls `resolveModel()`, distinguishes error strings from resolved model instances, and silently falls back to the parent model for config-specified models that fail resolution.
|
|
12
|
+
The logic is not independently testable — it is only exercised through integration-level agent spawning.
|
|
13
|
+
|
|
14
|
+
A second, simpler call site in `getModelLabel()` (~line 1043) also calls `resolveModel()` inline but only checks whether the model resolves; it does not need the same fallback semantics.
|
|
15
|
+
|
|
16
|
+
## Goals
|
|
17
|
+
|
|
18
|
+
- Extract the inline model-resolution block from `Agent.execute` into a named, unit-testable function in `model-resolver.ts`.
|
|
19
|
+
- Keep the existing `resolveModel()` function unchanged — the new function composes it.
|
|
20
|
+
- No behavior change: model-resolution priority and fallback semantics remain identical.
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Changing the `resolveModel()` fuzzy-matching algorithm.
|
|
25
|
+
- Refactoring the `getModelLabel()` call site (~line 1043) — it has different semantics (display-only, no fallback) and does not benefit from the same extraction.
|
|
26
|
+
- Refactoring `service-adapter.ts` model resolution — it already uses a clean injected-dependency pattern.
|
|
27
|
+
- Changing any public API surface.
|
|
28
|
+
|
|
29
|
+
## Background
|
|
30
|
+
|
|
31
|
+
### Existing modules
|
|
32
|
+
|
|
33
|
+
| Module | Role |
|
|
34
|
+
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `model-resolver.ts` | Exports `resolveModel(input, registry)` — returns a `Model` on success or an error string on failure. |
|
|
36
|
+
| `invocation-config.ts` | Exports `resolveAgentInvocationConfig()` — merges tool params with agent config. Returns `modelInput` (the raw string) and `modelFromParams` (whether the string came from tool params vs. agent config). |
|
|
37
|
+
| `service-adapter.ts` | Already receives `resolveModel` as a dependency via `AdapterDeps`. Its model resolution is simpler (always throw on failure). |
|
|
38
|
+
| `index.ts` | `Agent.execute` contains the inline block. Uses both `modelInput` and `modelFromParams` to decide: (a) return error to user if params-specified model fails, or (b) silently fall back to parent model if config-specified model fails. |
|
|
39
|
+
|
|
40
|
+
### Relevant constraint from AGENTS.md
|
|
41
|
+
|
|
42
|
+
> Keep modules focused and composable (one concern per file).
|
|
43
|
+
|
|
44
|
+
The new function belongs in `model-resolver.ts` alongside `resolveModel()` since it composes the latter with invocation-level fallback policy.
|
|
45
|
+
|
|
46
|
+
## Design Overview
|
|
47
|
+
|
|
48
|
+
### New function signature
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
interface ModelResolutionResult {
|
|
52
|
+
model: unknown;
|
|
53
|
+
error?: undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ModelResolutionError {
|
|
57
|
+
model?: undefined;
|
|
58
|
+
error: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type ModelResolution = ModelResolutionResult | ModelResolutionError;
|
|
62
|
+
|
|
63
|
+
function resolveInvocationModel(
|
|
64
|
+
parentModel: unknown,
|
|
65
|
+
modelInput: string | undefined,
|
|
66
|
+
modelFromParams: boolean,
|
|
67
|
+
registry: ModelRegistry,
|
|
68
|
+
): ModelResolution;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Decision model
|
|
72
|
+
|
|
73
|
+
The function encapsulates the existing three-branch logic:
|
|
74
|
+
|
|
75
|
+
1. **No `modelInput`** → return `{ model: parentModel }` (inherit parent).
|
|
76
|
+
2. **`modelInput` resolves** → return `{ model: resolved }`.
|
|
77
|
+
3. **`modelInput` fails to resolve**:
|
|
78
|
+
- If `modelFromParams` (user typed it) → return `{ error: errorMessage }` so the caller can surface it.
|
|
79
|
+
- If `!modelFromParams` (agent config specified it) → return `{ model: parentModel }` (silent fallback).
|
|
80
|
+
|
|
81
|
+
### Result shape rationale
|
|
82
|
+
|
|
83
|
+
A discriminated union (`ModelResolution`) with `model` and `error` fields avoids the existing `typeof resolved === "string"` type-narrowing smell.
|
|
84
|
+
The caller in `index.ts` becomes:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const resolution = resolveInvocationModel(
|
|
88
|
+
ctx.model,
|
|
89
|
+
resolvedConfig.modelInput,
|
|
90
|
+
resolvedConfig.modelFromParams,
|
|
91
|
+
ctx.modelRegistry,
|
|
92
|
+
);
|
|
93
|
+
if (resolution.error) return textResult(resolution.error);
|
|
94
|
+
const model = resolution.model;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Edge cases
|
|
98
|
+
|
|
99
|
+
- `modelInput` is `undefined` → short-circuit, return parent model.
|
|
100
|
+
- `modelInput` is an empty string → delegates to `resolveModel()`, which currently matches vacuously (documented in existing tests); no change in behavior.
|
|
101
|
+
|
|
102
|
+
## Module-Level Changes
|
|
103
|
+
|
|
104
|
+
### `src/model-resolver.ts`
|
|
105
|
+
|
|
106
|
+
- Add `ModelResolutionResult`, `ModelResolutionError`, and `ModelResolution` type exports.
|
|
107
|
+
- Add `resolveInvocationModel()` export.
|
|
108
|
+
- No changes to existing `resolveModel()`, `ModelEntry`, or `ModelRegistry`.
|
|
109
|
+
|
|
110
|
+
### `src/index.ts`
|
|
111
|
+
|
|
112
|
+
- Update import to include `resolveInvocationModel`.
|
|
113
|
+
- Replace the inline model-resolution block in `Agent.execute` (~lines 660–670) with a call to `resolveInvocationModel()` and a check on the result.
|
|
114
|
+
- Remove the now-unused destructuring of `modelFromParams` from `resolvedConfig` at the call site (it is consumed internally by `resolveInvocationModel` via the parameter).
|
|
115
|
+
|
|
116
|
+
### `test/model-resolver.test.ts`
|
|
117
|
+
|
|
118
|
+
- Add a new `describe("resolveInvocationModel")` block with tests covering all three branches plus edge cases.
|
|
119
|
+
|
|
120
|
+
## Test Impact Analysis
|
|
121
|
+
|
|
122
|
+
### New unit tests enabled
|
|
123
|
+
|
|
124
|
+
The extraction enables direct testing of the three-branch fallback logic (inherit, resolve, fallback-on-config-failure) that was previously only exercisable through full agent spawning.
|
|
125
|
+
Specifically:
|
|
126
|
+
|
|
127
|
+
- Parent model inheritance when no `modelInput` is provided.
|
|
128
|
+
- Successful resolution returns the resolved model.
|
|
129
|
+
- User-specified model failure returns an error.
|
|
130
|
+
- Config-specified model failure silently falls back to parent.
|
|
131
|
+
|
|
132
|
+
### Existing tests that stay as-is
|
|
133
|
+
|
|
134
|
+
- All existing `resolveModel` tests in `test/model-resolver.test.ts` — they test the lower-level function which is unchanged.
|
|
135
|
+
- Integration-level tests in `test/agent-runner.test.ts` and `test/agent-manager.test.ts` — they exercise model usage through the full agent lifecycle.
|
|
136
|
+
- `test/invocation-config.test.ts` — unchanged module.
|
|
137
|
+
- `test/service-adapter.test.ts` — uses its own injected `resolveModel` dependency, unaffected.
|
|
138
|
+
|
|
139
|
+
### Tests that become redundant
|
|
140
|
+
|
|
141
|
+
None.
|
|
142
|
+
The inline block was not directly tested anywhere — it was only reached through integration paths that test much more than model resolution.
|
|
143
|
+
|
|
144
|
+
## TDD Order
|
|
145
|
+
|
|
146
|
+
1. **Red → Green: parent model inheritance.**
|
|
147
|
+
Test: `resolveInvocationModel` returns `{ model: parentModel }` when `modelInput` is `undefined`.
|
|
148
|
+
Commit: `test: add resolveInvocationModel tests for parent model inheritance`
|
|
149
|
+
|
|
150
|
+
2. **Red → Green: successful model resolution.**
|
|
151
|
+
Test: returns `{ model: resolvedModel }` when `resolveModel` succeeds (both params-specified and config-specified).
|
|
152
|
+
Commit: `test: add resolveInvocationModel tests for successful resolution`
|
|
153
|
+
|
|
154
|
+
3. **Red → Green: user-specified model failure.**
|
|
155
|
+
Test: returns `{ error: message }` when `modelFromParams` is `true` and `resolveModel` returns an error string.
|
|
156
|
+
Commit: `test: add resolveInvocationModel tests for param model failure`
|
|
157
|
+
|
|
158
|
+
4. **Red → Green: config-specified model silent fallback.**
|
|
159
|
+
Test: returns `{ model: parentModel }` when `modelFromParams` is `false` and `resolveModel` returns an error string.
|
|
160
|
+
Commit: `test: add resolveInvocationModel tests for config model fallback`
|
|
161
|
+
|
|
162
|
+
5. **Green: implement `resolveInvocationModel` in `model-resolver.ts`.**
|
|
163
|
+
All four test cases go green.
|
|
164
|
+
Commit: `feat: add resolveInvocationModel to model-resolver`
|
|
165
|
+
|
|
166
|
+
6. **Refactor: replace inline block in `index.ts`.**
|
|
167
|
+
Replace the inline model-resolution block in `Agent.execute` with a call to `resolveInvocationModel`.
|
|
168
|
+
Run full test suite to confirm no regressions.
|
|
169
|
+
Commit: `refactor: use resolveInvocationModel in Agent.execute (#53)`
|
|
170
|
+
|
|
171
|
+
## Risks and Mitigations
|
|
172
|
+
|
|
173
|
+
| Risk | Mitigation |
|
|
174
|
+
| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
175
|
+
| Subtle behavior difference in the extracted function vs. the inline block | TDD steps 1–4 encode the exact current semantics; step 6 is a pure mechanical substitution. |
|
|
176
|
+
| `resolveModel` return type is `any \| string` — fragile narrowing | The new function encapsulates the `typeof` check behind a discriminated union, reducing but not eliminating the `any`. Fixing the `any` is out of scope (would require Pi SDK model type changes). |
|
|
177
|
+
| Second call site (`getModelLabel`) might seem like it should also use the new function | Explicitly listed as a non-goal — it has display-only semantics with no fallback behavior. |
|
|
178
|
+
|
|
179
|
+
## Open Questions
|
|
180
|
+
|
|
181
|
+
None — the extraction is mechanical and the issue's acceptance criteria are unambiguous.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-subagents",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./src/service.ts"
|
|
6
6
|
},
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"check": "tsc --noEmit",
|
|
60
60
|
"test": "vitest run",
|
|
61
61
|
"test:watch": "vitest",
|
|
62
|
-
"lint:md": "rumdl check
|
|
62
|
+
"lint:md": "rumdl check *.md docs/**/*.md",
|
|
63
63
|
"lint": "biome check . && pnpm run lint:md"
|
|
64
64
|
}
|
|
65
65
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTu
|
|
|
20
20
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveType } from "./agent-types.js";
|
|
21
21
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
22
22
|
import { resolveAgentInvocationConfig } from "./invocation-config.js";
|
|
23
|
-
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
23
|
+
import { type ModelRegistry, resolveInvocationModel, resolveModel } from "./model-resolver.js";
|
|
24
24
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
25
25
|
import { publishSubagentsService, unpublishSubagentsService } from "./service.js";
|
|
26
26
|
import { createSubagentsService } from "./service-adapter.js";
|
|
@@ -657,16 +657,14 @@ Guidelines:
|
|
|
657
657
|
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
658
658
|
|
|
659
659
|
// Resolve model from agent config first; tool-call params only fill gaps.
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
}
|
|
660
|
+
const resolution = resolveInvocationModel(
|
|
661
|
+
ctx.model,
|
|
662
|
+
resolvedConfig.modelInput,
|
|
663
|
+
resolvedConfig.modelFromParams,
|
|
664
|
+
ctx.modelRegistry,
|
|
665
|
+
);
|
|
666
|
+
if (resolution.error) return textResult(resolution.error);
|
|
667
|
+
const model = resolution.model;
|
|
670
668
|
|
|
671
669
|
const thinking = resolvedConfig.thinking;
|
|
672
670
|
const inheritContext = resolvedConfig.inheritContext;
|
package/src/model-resolver.ts
CHANGED
|
@@ -14,6 +14,45 @@ export interface ModelRegistry {
|
|
|
14
14
|
getAvailable?(): any[];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Successful model resolution — `model` is the resolved or inherited model instance. */
|
|
18
|
+
export interface ModelResolutionResult {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
model: any;
|
|
21
|
+
error?: undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Failed model resolution when the model was user-specified (params) — surface the error. */
|
|
25
|
+
export interface ModelResolutionError {
|
|
26
|
+
model?: undefined;
|
|
27
|
+
error: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Discriminated union returned by `resolveInvocationModel`. */
|
|
31
|
+
export type ModelResolution = ModelResolutionResult | ModelResolutionError;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the effective model for an agent invocation.
|
|
35
|
+
*
|
|
36
|
+
* Encapsulates the three-branch fallback policy used in `Agent.execute`:
|
|
37
|
+
* 1. No `modelInput` → inherit `parentModel`.
|
|
38
|
+
* 2. `modelInput` resolves → return the resolved model.
|
|
39
|
+
* 3. `modelInput` fails:
|
|
40
|
+
* - `modelFromParams` true → return `{ error }` so the caller can surface it.
|
|
41
|
+
* - `modelFromParams` false → silent fallback to `parentModel`.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveInvocationModel(
|
|
44
|
+
parentModel: unknown,
|
|
45
|
+
modelInput: string | undefined,
|
|
46
|
+
modelFromParams: boolean,
|
|
47
|
+
registry: ModelRegistry,
|
|
48
|
+
): ModelResolution {
|
|
49
|
+
if (!modelInput) return { model: parentModel };
|
|
50
|
+
const resolved = resolveModel(modelInput, registry);
|
|
51
|
+
if (typeof resolved !== "string") return { model: resolved };
|
|
52
|
+
if (modelFromParams) return { error: resolved };
|
|
53
|
+
return { model: parentModel };
|
|
54
|
+
}
|
|
55
|
+
|
|
17
56
|
/**
|
|
18
57
|
* Resolve a model string to a Model instance.
|
|
19
58
|
* Tries exact match first ("provider/modelId"), then fuzzy match against all available models.
|