@gotgenes/pi-subagents 6.17.2 → 6.18.1
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 +26 -0
- package/docs/plans/0165-decompose-resolved-spawn-config.md +157 -0
- package/docs/retro/0164-reorganize-into-domain-directories.md +36 -0
- package/docs/retro/0165-decompose-resolved-spawn-config.md +40 -0
- package/package.json +3 -2
- package/src/config/custom-agents.ts +2 -1
- package/src/handlers/lifecycle.ts +2 -1
- package/src/index.ts +1 -0
- package/src/lifecycle/agent-manager.ts +6 -3
- package/src/lifecycle/agent-runner.ts +3 -3
- package/src/lifecycle/parent-snapshot.ts +1 -0
- package/src/observation/record-observer.ts +1 -0
- package/src/service/service-adapter.ts +2 -2
- package/src/service/service.ts +1 -0
- package/src/session/context.ts +2 -0
- package/src/session/model-resolver.ts +2 -1
- package/src/session/prompts.ts +2 -2
- package/src/session/skill-loader.ts +1 -1
- package/src/tools/agent-tool.ts +6 -5
- package/src/tools/background-spawner.ts +14 -14
- package/src/tools/foreground-runner.ts +18 -17
- package/src/tools/get-result-tool.ts +1 -1
- package/src/tools/spawn-config.ts +33 -20
- package/src/ui/agent-config-editor.ts +2 -1
- package/src/ui/agent-menu.ts +1 -0
- package/src/ui/agent-widget.ts +2 -3
- package/src/ui/conversation-viewer.ts +1 -1
- package/src/ui/ui-observer.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ 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
|
+
## [6.18.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.18.0...pi-subagents-v6.18.1) (2026-05-24)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* plan decompose ResolvedSpawnConfig ([#165](https://github.com/gotgenes/pi-packages/issues/165)) ([1b14d56](https://github.com/gotgenes/pi-packages/commit/1b14d56aaada427a7344ec22adb4bbf8d7ce0bf7))
|
|
14
|
+
* **retro:** add planning stage notes for issue [#165](https://github.com/gotgenes/pi-packages/issues/165) ([8e0476a](https://github.com/gotgenes/pi-packages/commit/8e0476afa991953884455c7dd09c7ffb742cb329))
|
|
15
|
+
* **retro:** add TDD stage notes for issue [#165](https://github.com/gotgenes/pi-packages/issues/165) ([68248e5](https://github.com/gotgenes/pi-packages/commit/68248e572d38ad6e0cdb61bdd22f2b46193eaac6))
|
|
16
|
+
|
|
17
|
+
## [6.18.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.17.2...pi-subagents-v6.18.0) (2026-05-24)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* add eslint config with type-aware rules and import enforcement ([4fb3cc6](https://github.com/gotgenes/pi-packages/commit/4fb3cc678da10d350b85c464318476ba9ae99dca))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* **pi-subagents:** add missing "type": "module" to package.json ([8cfd07d](https://github.com/gotgenes/pi-packages/commit/8cfd07dfbfd44f52dc43ac7ae67d5824304825ae))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
* **retro:** add retro notes for issue [#164](https://github.com/gotgenes/pi-packages/issues/164) ([d8e2861](https://github.com/gotgenes/pi-packages/commit/d8e28615d6adabc86415f4d41ffd1bd90184fd0f))
|
|
33
|
+
|
|
8
34
|
## [6.17.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.17.1...pi-subagents-v6.17.2) (2026-05-23)
|
|
9
35
|
|
|
10
36
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 165
|
|
3
|
+
issue_title: "refactor(pi-subagents): decompose ResolvedSpawnConfig (15 fields)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Decompose ResolvedSpawnConfig into domain-aligned sub-interfaces
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`ResolvedSpawnConfig` in `tools/spawn-config.ts` has 15 fields mixing identity, execution, and presentation concerns.
|
|
11
|
+
Each consumer uses a different subset but receives the full bag — violating ISP and making the real dependencies of `foreground-runner` and `background-spawner` invisible.
|
|
12
|
+
|
|
13
|
+
## Goals
|
|
14
|
+
|
|
15
|
+
- Split `ResolvedSpawnConfig` into three focused interfaces: `SpawnIdentity`, `SpawnExecution`, `SpawnPresentation`.
|
|
16
|
+
- Each consumer declares its real dependencies explicitly.
|
|
17
|
+
- Preserve existing behavior — pure structural refactor with no behavioral changes.
|
|
18
|
+
|
|
19
|
+
## Non-Goals
|
|
20
|
+
|
|
21
|
+
- Extracting `ParentSessionInfo` from `AgentSpawnConfig` — that's #166.
|
|
22
|
+
- Changing how `resolveSpawnConfig` computes values internally.
|
|
23
|
+
- Modifying the `AgentSpawnConfig` interface passed to `AgentManager`.
|
|
24
|
+
|
|
25
|
+
## Background
|
|
26
|
+
|
|
27
|
+
`resolveSpawnConfig` is called by `agent-tool.ts` and produces a single flat config object.
|
|
28
|
+
Three consumers read from it:
|
|
29
|
+
|
|
30
|
+
1. `agent-tool.ts` — reads `inheritContext` (to build snapshot), `runInBackground` (to branch), and `detailBase` (for resume result).
|
|
31
|
+
2. `foreground-runner.ts` — reads identity fields for fallback messages, execution fields for spawn options, and `detailBase` for result formatting.
|
|
32
|
+
3. `background-spawner.ts` — reads identity fields for launch message, execution fields for spawn options, and `detailBase` for result formatting.
|
|
33
|
+
|
|
34
|
+
Two fields (`modelName`, `agentTags`) are never accessed by any external consumer — they're intermediate values used only inside `resolveSpawnConfig` to build `detailBase`.
|
|
35
|
+
They belong on `SpawnPresentation` for transparency but could also be made internal-only.
|
|
36
|
+
|
|
37
|
+
The `code-design` skill's ISP and dependency-width guidance both apply: clients should not depend on properties they don't use, and a shared bag where each consumer only touches a subset hides real dependencies.
|
|
38
|
+
|
|
39
|
+
## Design Overview
|
|
40
|
+
|
|
41
|
+
### New interfaces
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
/** Identity: who is being spawned. */
|
|
45
|
+
export interface SpawnIdentity {
|
|
46
|
+
subagentType: string;
|
|
47
|
+
rawType: SubagentType;
|
|
48
|
+
fellBack: boolean;
|
|
49
|
+
displayName: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Execution: how the agent will run. */
|
|
53
|
+
export interface SpawnExecution {
|
|
54
|
+
prompt: string;
|
|
55
|
+
description: string;
|
|
56
|
+
model: Model<any> | undefined;
|
|
57
|
+
effectiveMaxTurns: number | undefined;
|
|
58
|
+
thinking: ThinkingLevel | undefined;
|
|
59
|
+
inheritContext: boolean;
|
|
60
|
+
runInBackground: boolean;
|
|
61
|
+
isolated: boolean;
|
|
62
|
+
isolation: IsolationMode | undefined;
|
|
63
|
+
agentInvocation: AgentInvocation;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Presentation: display/UI values derived from identity + execution. */
|
|
67
|
+
export interface SpawnPresentation {
|
|
68
|
+
modelName: string | undefined;
|
|
69
|
+
agentTags: string[];
|
|
70
|
+
detailBase: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Fully resolved config — now a composition of domain-aligned sub-interfaces. */
|
|
74
|
+
export interface ResolvedSpawnConfig {
|
|
75
|
+
identity: SpawnIdentity;
|
|
76
|
+
execution: SpawnExecution;
|
|
77
|
+
presentation: SpawnPresentation;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Consumer interaction pattern
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// agent-tool.ts — uses execution for routing, presentation for resume
|
|
85
|
+
const config = resolveSpawnConfig(params, registry, modelInfo, settings);
|
|
86
|
+
if ("error" in config) return textResult(config.error);
|
|
87
|
+
const snapshot = buildSnapshot(config.execution.inheritContext);
|
|
88
|
+
if (config.execution.runInBackground) { /* ... */ }
|
|
89
|
+
return buildDetails(config.presentation.detailBase, record);
|
|
90
|
+
|
|
91
|
+
// foreground-runner.ts — destructures what it needs
|
|
92
|
+
const { identity, execution, presentation } = params.config;
|
|
93
|
+
record = await manager.spawnAndWait(snapshot, identity.subagentType, execution.prompt, { ... });
|
|
94
|
+
const fallbackNote = identity.fellBack ? `Note: Unknown agent type "${identity.rawType}"...` : "";
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This follows Tell-Don't-Ask — callers pick the sub-object relevant to their concern rather than reaching through a flat bag.
|
|
98
|
+
The one-level nesting (`config.execution.inheritContext`) is acceptable because it names the domain the field belongs to.
|
|
99
|
+
|
|
100
|
+
### Test factory migration
|
|
101
|
+
|
|
102
|
+
Both test files (`foreground-runner.test.ts`, `background-spawner.test.ts`) have `makeConfig()` factories that construct the full 15-field flat object.
|
|
103
|
+
These will be updated to construct the nested structure.
|
|
104
|
+
The `spawn-config.test.ts` assertions will shift from `result.subagentType` to `result.identity.subagentType` etc.
|
|
105
|
+
|
|
106
|
+
## Module-Level Changes
|
|
107
|
+
|
|
108
|
+
| File | Change |
|
|
109
|
+
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
+
| `src/tools/spawn-config.ts` | Add `SpawnIdentity`, `SpawnExecution`, `SpawnPresentation` interfaces. Change `ResolvedSpawnConfig` to nest them. Update `resolveSpawnConfig` return to build nested structure. |
|
|
111
|
+
| `src/tools/agent-tool.ts` | Update config access: `config.execution.inheritContext`, `config.execution.runInBackground`, `config.presentation.detailBase`. |
|
|
112
|
+
| `src/tools/foreground-runner.ts` | Destructure `config` into `identity`, `execution`, `presentation`. Update all field accesses. |
|
|
113
|
+
| `src/tools/background-spawner.ts` | Destructure `config` into `identity`, `execution`, `presentation`. Update all field accesses. |
|
|
114
|
+
| `test/tools/spawn-config.test.ts` | Update all assertions to use nested paths (`result.identity.subagentType`, etc.). |
|
|
115
|
+
| `test/tools/foreground-runner.test.ts` | Update `makeConfig()` factory to build nested structure. |
|
|
116
|
+
| `test/tools/background-spawner.test.ts` | Update `makeConfig()` factory to build nested structure. |
|
|
117
|
+
|
|
118
|
+
## Test Impact Analysis
|
|
119
|
+
|
|
120
|
+
1. No new unit tests are needed — this is a structural refactor, not new behavior.
|
|
121
|
+
2. No existing tests become redundant — all current `spawn-config.test.ts` assertions still verify the same computation.
|
|
122
|
+
3. All existing tests must be updated to match the new nested access paths, but they continue to exercise the same logic.
|
|
123
|
+
|
|
124
|
+
## TDD Order
|
|
125
|
+
|
|
126
|
+
1. **Red→Green: introduce sub-interfaces and nest `ResolvedSpawnConfig`** — change `spawn-config.ts` to export the three sub-interfaces and restructure `ResolvedSpawnConfig`.
|
|
127
|
+
Update `resolveSpawnConfig` to return the nested shape.
|
|
128
|
+
Update `spawn-config.test.ts` assertions to match.
|
|
129
|
+
Commit: `refactor(pi-subagents): introduce SpawnIdentity, SpawnExecution, SpawnPresentation`
|
|
130
|
+
|
|
131
|
+
2. **Green: update `agent-tool.ts`** — migrate the three field accesses (`inheritContext`, `runInBackground`, `detailBase`) to nested paths.
|
|
132
|
+
Run `pnpm run check` to confirm types pass.
|
|
133
|
+
Commit: `refactor(pi-subagents): update agent-tool to use nested spawn config`
|
|
134
|
+
|
|
135
|
+
3. **Green: update `foreground-runner.ts` and its test** — destructure config and update all field accesses.
|
|
136
|
+
Update `makeConfig()` factory in `foreground-runner.test.ts`.
|
|
137
|
+
Commit: `refactor(pi-subagents): update foreground-runner to use nested spawn config`
|
|
138
|
+
|
|
139
|
+
4. **Green: update `background-spawner.ts` and its test** — destructure config and update all field accesses.
|
|
140
|
+
Update `makeConfig()` factory in `background-spawner.test.ts`.
|
|
141
|
+
Commit: `refactor(pi-subagents): update background-spawner to use nested spawn config`
|
|
142
|
+
|
|
143
|
+
5. **Verify: full suite** — run `pnpm vitest run` and `pnpm run check` to confirm no regressions.
|
|
144
|
+
Commit (if any lint/type cleanup needed): `chore(pi-subagents): post-decomposition cleanup`
|
|
145
|
+
|
|
146
|
+
## Risks and Mitigations
|
|
147
|
+
|
|
148
|
+
| Risk | Mitigation |
|
|
149
|
+
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
150
|
+
| Step 1 breaks type checking for all consumers simultaneously | Steps 2–4 must land in the same branch before pushing; or use a transitional type alias that spreads the sub-interfaces flat, removing it in the final step. |
|
|
151
|
+
| Test factories diverge from production shape | Each step updates the test factory in the same commit as the source change. |
|
|
152
|
+
| `modelName` and `agentTags` on `SpawnPresentation` look unused | They are unused by external consumers today but provide inspection affordance for debugging/logging. Keep them; #166 or later work may consume them. |
|
|
153
|
+
|
|
154
|
+
## Open Questions
|
|
155
|
+
|
|
156
|
+
- None — the issue's proposed shape aligns with actual field usage patterns.
|
|
157
|
+
The only observation is that `modelName` and `agentTags` are only consumed internally, but exposing them on `SpawnPresentation` is harmless and aids debuggability.
|
|
@@ -44,3 +44,39 @@ Updated `docs/architecture/architecture.md` to reflect the completed restructuri
|
|
|
44
44
|
This eliminates all `../` relative cross-directory imports from `src/`.
|
|
45
45
|
Future file moves in `src/` now only require updating the `#src/domain/name` string — no relative depth arithmetic.
|
|
46
46
|
- Biome auto-fixed 14 files (import sorting / trailing whitespace) during the `#src/` conversion step; committed via `git add -A` after the pre-commit hook run.
|
|
47
|
+
|
|
48
|
+
## Stage: Final Retrospective (2026-05-23T17:10:00Z)
|
|
49
|
+
|
|
50
|
+
### Session summary
|
|
51
|
+
|
|
52
|
+
Shipped #164 (6 commits, `pi-subagents-v6.17.2`), filed #174 (ESLint for type-aware rules + import path enforcement), and reviewed the full issue lifecycle across planning, implementation, and shipping sessions.
|
|
53
|
+
|
|
54
|
+
### Observations
|
|
55
|
+
|
|
56
|
+
#### What went well
|
|
57
|
+
|
|
58
|
+
- The user's mid-implementation redirect to use `#src/` aliases in `src/` files was the highest-impact intervention across the entire issue.
|
|
59
|
+
It eliminated the `../` depth-arithmetic problem, simplified the final commit to a one-liner `sed` command across 40 files, and directly motivated #174.
|
|
60
|
+
- `pnpm run check` (`tsc --noEmit`) caught every missed consumer immediately — no broken commit ever landed.
|
|
61
|
+
- The four-step dependency ordering (config → session → lifecycle+observation → service) kept every commit green despite the circular dependency between `lifecycle` and `observation`.
|
|
62
|
+
|
|
63
|
+
#### What caused friction (agent side)
|
|
64
|
+
|
|
65
|
+
- `wrong-abstraction` — Used ~60 individual `Edit` tool calls across steps 1–4 for what was a mechanical find-and-replace.
|
|
66
|
+
The fifth commit proved `sed` handles bulk import rewrites across 40 files in a single command.
|
|
67
|
+
Impact: hundreds of unnecessary tool calls and significant token waste across four commits.
|
|
68
|
+
- `missing-context` — The plan's consumer tables missed 4 files (`src/ui/widget-renderer.ts`, `src/session-config.ts`, `src/service-adapter.ts`, `test/parent-snapshot.test.ts` `vi.mock` path).
|
|
69
|
+
The plan manually traced imports instead of using `grep` to enumerate all consumers of each moving module.
|
|
70
|
+
Impact: mid-step rework in steps 1, 2, and 3 to fix un-updated imports caught by `tsc`.
|
|
71
|
+
- `missing-context` — Did not recognize that `src/` files should use `#src/` aliases (same as `test/` files) even though #157 set up the aliases for exactly this purpose.
|
|
72
|
+
Impact: all four domain-move commits used relative `../` imports, requiring a fifth unplanned commit to convert them.
|
|
73
|
+
User-caught.
|
|
74
|
+
- `wrong-abstraction` — The agent lacks access to LSP-level refactoring tools ("Move to file", "Rename symbol") that a human developer would use for this kind of reorganization.
|
|
75
|
+
A human with an LSP would have completed the entire issue in minutes with zero missed consumers.
|
|
76
|
+
This is a fundamental capability gap — the agent compensated with low-level text manipulation, which is error-prone and token-expensive.
|
|
77
|
+
|
|
78
|
+
#### What caused friction (user side)
|
|
79
|
+
|
|
80
|
+
- The `#src/` alias convention was established in #157 but the user didn't flag it during the planning session.
|
|
81
|
+
Had this been raised during planning, all four domain-move commits would have used `#src/` from the start.
|
|
82
|
+
The user did catch it during implementation, which was still early enough to save the final result.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 165
|
|
3
|
+
issue_title: "refactor(pi-subagents): decompose ResolvedSpawnConfig (15 fields)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #165 — decompose ResolvedSpawnConfig (15 fields)
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-24T13:41:41Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 5-step TDD plan to decompose the 15-field `ResolvedSpawnConfig` into three nested sub-interfaces (`SpawnIdentity`, `SpawnExecution`, `SpawnPresentation`).
|
|
13
|
+
Also improved skill descriptions for `colgrep` and `markdown-conventions` to signal decision-relevant content rather than tool reference material.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- The proposed decomposition in the issue aligns well with actual field usage patterns — no adjustments needed.
|
|
18
|
+
- `modelName` and `agentTags` are never accessed by external consumers; they're intermediate computation exposed on the return type.
|
|
19
|
+
Keeping them on `SpawnPresentation` is harmless and aids debuggability.
|
|
20
|
+
- Step 1 (interface change + return restructure) will break type checking for all consumers simultaneously.
|
|
21
|
+
The plan addresses this by landing steps 1–4 in rapid succession on the same branch.
|
|
22
|
+
- Both test files have `makeConfig()` factories that must be updated in lock-step with their respective source files.
|
|
23
|
+
- Issue #164 (directory reorganization) is closed, so import paths are already in their final `#src/<domain>/` form.
|
|
24
|
+
|
|
25
|
+
## Stage: Implementation — TDD (2026-05-24T14:32:58Z)
|
|
26
|
+
|
|
27
|
+
### Session summary
|
|
28
|
+
|
|
29
|
+
Completed all 4 TDD cycles plus full-suite verification in one session.
|
|
30
|
+
The decomposition touched 7 files (4 source, 3 test) and kept the test count flat at 805 — no new tests needed for a pure structural refactor.
|
|
31
|
+
|
|
32
|
+
### Observations
|
|
33
|
+
|
|
34
|
+
- The `Partial<ResolvedSpawnConfig>` spread pattern in `makeConfig` factories doesn't deep-merge into nested sub-objects.
|
|
35
|
+
Two tests (`foreground-runner.test.ts` and `background-spawner.test.ts`) used flat field overrides (`{ fellBack: true }`, `{ description: "my task" }`) that silently stopped working after nesting.
|
|
36
|
+
Fixed by writing out the full nested sub-object at the override call site.
|
|
37
|
+
Future factories for nested config types should either deep-merge or avoid the `Partial<T>` spread pattern — see the `testing` skill's warning about this.
|
|
38
|
+
- Step 1 breaking all consumers simultaneously was handled smoothly by completing all steps before pushing, as planned.
|
|
39
|
+
No transitional alias was needed.
|
|
40
|
+
- The `background-spawner.test.ts` description-override test was the only unexpected friction point — the flat spread issue wasn't caught by the plan.
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-subagents",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.18.1",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"exports": {
|
|
5
6
|
".": "./src/service.ts"
|
|
6
7
|
},
|
|
@@ -66,6 +67,6 @@
|
|
|
66
67
|
"test": "vitest run",
|
|
67
68
|
"test:watch": "vitest",
|
|
68
69
|
"lint:md": "rumdl check *.md docs/**/*.md",
|
|
69
|
-
"lint": "biome check . && pnpm run lint:md"
|
|
70
|
+
"lint": "biome check . && eslint . && pnpm run lint:md"
|
|
70
71
|
}
|
|
71
72
|
}
|
|
@@ -51,7 +51,7 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
51
51
|
continue;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const { frontmatter: fm, body } = parseFrontmatter
|
|
54
|
+
const { frontmatter: fm, body } = parseFrontmatter(content);
|
|
55
55
|
|
|
56
56
|
agents.set(name, {
|
|
57
57
|
name,
|
|
@@ -95,6 +95,7 @@ function nonNegativeInt(val: unknown): number | undefined {
|
|
|
95
95
|
*/
|
|
96
96
|
function parseCsvField(val: unknown): string[] | undefined {
|
|
97
97
|
if (val === undefined || val === null) return undefined;
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- val is already narrowed past null/undefined; String() is the intended coercion here
|
|
98
99
|
const s = String(val).trim();
|
|
99
100
|
if (!s || s === "none") return undefined;
|
|
100
101
|
const items = s.split(",").map(t => t.trim()).filter(Boolean);
|
|
@@ -52,11 +52,12 @@ export class SessionLifecycleHandler {
|
|
|
52
52
|
// 3. Abort all agents — stop running work
|
|
53
53
|
// 4. Dispose notifications — cancel pending nudges/timers
|
|
54
54
|
// 5. Dispose manager — final cleanup
|
|
55
|
-
|
|
55
|
+
handleSessionShutdown(): Promise<void> {
|
|
56
56
|
this.unpublishService();
|
|
57
57
|
this.runtime.clearSessionContext();
|
|
58
58
|
this.manager.abortAll();
|
|
59
59
|
this.disposeNotifications();
|
|
60
60
|
this.manager.dispose();
|
|
61
|
+
return Promise.resolve();
|
|
61
62
|
}
|
|
62
63
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
1
2
|
/**
|
|
2
3
|
* pi-agents — A pi extension providing Claude Code-style autonomous sub-agents.
|
|
3
4
|
*
|
|
@@ -235,6 +235,7 @@ export class AgentManager {
|
|
|
235
235
|
onSessionCreated: (session) => {
|
|
236
236
|
// Capture the session file path early so it's available for display
|
|
237
237
|
// before the run completes (e.g. in background agent status messages).
|
|
238
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- sessionManager is typed as always present but Pi SDK may not provide it
|
|
238
239
|
const outputFile = session.sessionManager?.getSessionFile?.() ?? undefined;
|
|
239
240
|
// Set the execution-state collaborator — born complete at session creation.
|
|
240
241
|
record.execution = { session, outputFile };
|
|
@@ -282,7 +283,7 @@ export class AgentManager {
|
|
|
282
283
|
}
|
|
283
284
|
return responseText;
|
|
284
285
|
})
|
|
285
|
-
.catch((err) => {
|
|
286
|
+
.catch((err: unknown) => {
|
|
286
287
|
record.markError(err);
|
|
287
288
|
|
|
288
289
|
unsubRecordObserver?.();
|
|
@@ -313,7 +314,7 @@ export class AgentManager {
|
|
|
313
314
|
while (this.queue.length > 0 && this.runningBackground < this._getMaxConcurrent()) {
|
|
314
315
|
const next = this.queue.shift()!;
|
|
315
316
|
const record = this.agents.get(next.id);
|
|
316
|
-
if (
|
|
317
|
+
if (record?.status !== "queued") continue;
|
|
317
318
|
try {
|
|
318
319
|
this.startAgent(next.id, record, next.args);
|
|
319
320
|
} catch (err) {
|
|
@@ -402,6 +403,7 @@ export class AgentManager {
|
|
|
402
403
|
|
|
403
404
|
/** Dispose a record's session and remove it from the map. */
|
|
404
405
|
private removeRecord(id: string, record: AgentRecord): void {
|
|
406
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dispose may not exist on all session implementations
|
|
405
407
|
record.session?.dispose?.();
|
|
406
408
|
this.agents.delete(id);
|
|
407
409
|
this.pendingSteers.delete(id);
|
|
@@ -464,12 +466,13 @@ export class AgentManager {
|
|
|
464
466
|
async waitForAll(): Promise<void> {
|
|
465
467
|
// Loop because drainQueue respects the concurrency limit — as running
|
|
466
468
|
// agents finish they start queued ones, which need awaiting too.
|
|
469
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop with explicit break
|
|
467
470
|
while (true) {
|
|
468
471
|
this.drainQueue();
|
|
469
472
|
const pending = [...this.agents.values()]
|
|
470
473
|
.filter(r => r.status === "running" || r.status === "queued")
|
|
471
474
|
.map(r => r.promise)
|
|
472
|
-
.filter(
|
|
475
|
+
.filter((p): p is Promise<string> => p != null);
|
|
473
476
|
if (pending.length === 0) break;
|
|
474
477
|
await Promise.allSettled(pending);
|
|
475
478
|
}
|
|
@@ -246,7 +246,7 @@ function forwardAbortSignal(
|
|
|
246
246
|
signal?: AbortSignal,
|
|
247
247
|
): () => void {
|
|
248
248
|
if (!signal) return () => {};
|
|
249
|
-
const onAbort = () => session.abort();
|
|
249
|
+
const onAbort = (): void => { void session.abort(); };
|
|
250
250
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
251
251
|
return () => signal.removeEventListener("abort", onAbort);
|
|
252
252
|
}
|
|
@@ -375,12 +375,12 @@ export async function runAgent(
|
|
|
375
375
|
if (maxTurns != null) {
|
|
376
376
|
if (!softLimitReached && turnCount >= maxTurns) {
|
|
377
377
|
softLimitReached = true;
|
|
378
|
-
session.steer(
|
|
378
|
+
void session.steer(
|
|
379
379
|
"You have reached your turn limit. Wrap up immediately — provide your final answer now.",
|
|
380
380
|
);
|
|
381
381
|
} else if (softLimitReached && turnCount >= maxTurns + (options.graceTurns ?? 5)) {
|
|
382
382
|
aborted = true;
|
|
383
|
-
session.abort();
|
|
383
|
+
void session.abort();
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
}
|
|
@@ -41,6 +41,7 @@ export function buildParentSnapshot(
|
|
|
41
41
|
systemPrompt: ctx.getSystemPrompt(),
|
|
42
42
|
model: ctx.model,
|
|
43
43
|
modelRegistry: ctx.modelRegistry,
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: converts empty string to undefined as well as null/undefined
|
|
44
45
|
parentContext: parentContext || undefined,
|
|
45
46
|
};
|
|
46
47
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
1
2
|
/**
|
|
2
3
|
* record-observer.ts — Subscribes to session events and updates AgentRecord stats.
|
|
3
4
|
*
|
|
@@ -26,7 +26,7 @@ export interface AgentManagerLike {
|
|
|
26
26
|
/** Create a SubagentsService backed by the given dependencies. */
|
|
27
27
|
export function createSubagentsService(
|
|
28
28
|
manager: AgentManagerLike,
|
|
29
|
-
resolveModel: (input: string, registry: ModelRegistry) => unknown
|
|
29
|
+
resolveModel: (input: string, registry: ModelRegistry) => unknown,
|
|
30
30
|
getCtx: () => { pi: unknown; ctx: unknown } | undefined,
|
|
31
31
|
getModelRegistry: () => ModelRegistry | undefined,
|
|
32
32
|
): SubagentsService {
|
|
@@ -85,7 +85,7 @@ export function createSubagentsService(
|
|
|
85
85
|
|
|
86
86
|
async steer(id: string, message: string): Promise<boolean> {
|
|
87
87
|
const record = manager.getRecord(id);
|
|
88
|
-
if (
|
|
88
|
+
if (record?.status !== "running") {
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
91
|
const session = record.session;
|
package/src/service/service.ts
CHANGED
|
@@ -100,5 +100,6 @@ export function getSubagentsService(): SubagentsService | undefined {
|
|
|
100
100
|
|
|
101
101
|
/** Remove the SubagentsService from globalThis (call on shutdown/reload). */
|
|
102
102
|
export function unpublishSubagentsService(): void {
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Symbol-keyed global property; Map.delete() is not applicable
|
|
103
104
|
delete (globalThis as Record<symbol, unknown>)[SERVICE_KEY];
|
|
104
105
|
}
|
package/src/session/context.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
1
2
|
/**
|
|
2
3
|
* context.ts — Extract parent conversation context for subagent inheritance.
|
|
3
4
|
*/
|
|
@@ -19,6 +20,7 @@ export function extractText(content: unknown[]): string {
|
|
|
19
20
|
*/
|
|
20
21
|
export function buildParentContext(ctx: ExtensionContext): string {
|
|
21
22
|
const entries = ctx.sessionManager.getBranch();
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- getBranch() may return undefined at runtime despite its type
|
|
22
24
|
if (!entries || entries.length === 0) return "";
|
|
23
25
|
|
|
24
26
|
const parts: string[] = [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-redundant-type-constituents -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
1
2
|
/**
|
|
2
3
|
* Model resolution: exact match ("provider/modelId") with fuzzy fallback.
|
|
3
4
|
*/
|
|
@@ -16,7 +17,7 @@ export interface ModelRegistry {
|
|
|
16
17
|
|
|
17
18
|
/** Successful model resolution — `model` is the resolved or inherited model instance. */
|
|
18
19
|
export interface ModelResolutionResult {
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
model: any;
|
|
21
22
|
error?: undefined;
|
|
22
23
|
}
|
package/src/session/prompts.ts
CHANGED
|
@@ -57,7 +57,7 @@ Platform: ${env.platform}`;
|
|
|
57
57
|
extraSections.length > 0 ? "\n\n" + extraSections.join("\n") : "";
|
|
58
58
|
|
|
59
59
|
if (config.promptMode === "append") {
|
|
60
|
-
const identity = parentSystemPrompt
|
|
60
|
+
const identity = parentSystemPrompt ?? genericBase;
|
|
61
61
|
|
|
62
62
|
const bridge = `<sub_agent_context>
|
|
63
63
|
You are operating as a sub-agent invoked to handle a specific task.
|
|
@@ -72,7 +72,7 @@ You are operating as a sub-agent invoked to handle a specific task.
|
|
|
72
72
|
- Be concise but complete
|
|
73
73
|
</sub_agent_context>`;
|
|
74
74
|
|
|
75
|
-
const customSection = config.systemPrompt
|
|
75
|
+
const customSection = config.systemPrompt.trim()
|
|
76
76
|
? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
|
|
77
77
|
: "";
|
|
78
78
|
|
|
@@ -69,7 +69,7 @@ function findSkillDirectory(root: string, name: string): string | undefined {
|
|
|
69
69
|
const current = queue.shift();
|
|
70
70
|
if (current === undefined) continue;
|
|
71
71
|
|
|
72
|
-
let entries: Dirent
|
|
72
|
+
let entries: Dirent[];
|
|
73
73
|
try {
|
|
74
74
|
entries = readdirSync(current, { withFileTypes: true });
|
|
75
75
|
} catch (err) {
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
1
2
|
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
4
|
import { Type } from "@sinclair/typebox";
|
|
@@ -170,7 +171,7 @@ Guidelines:
|
|
|
170
171
|
const displayName = args.subagent_type
|
|
171
172
|
? getDisplayName(args.subagent_type as string, registry)
|
|
172
173
|
: "Agent";
|
|
173
|
-
const desc = (args.description as string) ?? "";
|
|
174
|
+
const desc = (args.description as string | undefined) ?? "";
|
|
174
175
|
return new Text(
|
|
175
176
|
"▸ " +
|
|
176
177
|
theme.fg("toolTitle", theme.bold(displayName)) +
|
|
@@ -300,7 +301,7 @@ Guidelines:
|
|
|
300
301
|
if ("error" in config) return textResult(config.error);
|
|
301
302
|
|
|
302
303
|
// ---- Boundary extraction (after config so inheritContext is resolved) ----
|
|
303
|
-
const snapshot = buildSnapshot(config.inheritContext);
|
|
304
|
+
const snapshot = buildSnapshot(config.execution.inheritContext);
|
|
304
305
|
const { parentSessionFile, parentSessionId } = getSessionInfo();
|
|
305
306
|
|
|
306
307
|
// ---- Resume existing agent ----
|
|
@@ -325,13 +326,13 @@ Guidelines:
|
|
|
325
326
|
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
326
327
|
}
|
|
327
328
|
return textResult(
|
|
328
|
-
record.result?.trim()
|
|
329
|
-
buildDetails(config.detailBase, record),
|
|
329
|
+
record.result?.trim() ?? record.error?.trim() ?? "No output.",
|
|
330
|
+
buildDetails(config.presentation.detailBase, record),
|
|
330
331
|
);
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
// ---- Background execution ----
|
|
334
|
-
if (config.runInBackground) {
|
|
335
|
+
if (config.execution.runInBackground) {
|
|
335
336
|
return spawnBackground(
|
|
336
337
|
manager,
|
|
337
338
|
widget,
|
|
@@ -40,23 +40,23 @@ export function spawnBackground(
|
|
|
40
40
|
agentActivity: AgentActivityAccess,
|
|
41
41
|
params: BackgroundParams,
|
|
42
42
|
) {
|
|
43
|
-
const {
|
|
44
|
-
const bgState = new AgentActivityTracker(
|
|
43
|
+
const { identity, execution, presentation } = params.config;
|
|
44
|
+
const bgState = new AgentActivityTracker(execution.effectiveMaxTurns);
|
|
45
45
|
|
|
46
46
|
let id: string;
|
|
47
47
|
try {
|
|
48
|
-
id = manager.spawn(params.snapshot,
|
|
48
|
+
id = manager.spawn(params.snapshot, identity.subagentType, execution.prompt, {
|
|
49
49
|
parentSessionFile: params.parentSessionFile,
|
|
50
50
|
parentSessionId: params.parentSessionId,
|
|
51
|
-
description:
|
|
52
|
-
model:
|
|
53
|
-
maxTurns:
|
|
54
|
-
isolated:
|
|
55
|
-
inheritContext:
|
|
56
|
-
thinkingLevel:
|
|
51
|
+
description: execution.description,
|
|
52
|
+
model: execution.model,
|
|
53
|
+
maxTurns: execution.effectiveMaxTurns,
|
|
54
|
+
isolated: execution.isolated,
|
|
55
|
+
inheritContext: execution.inheritContext,
|
|
56
|
+
thinkingLevel: execution.thinking,
|
|
57
57
|
isBackground: true,
|
|
58
|
-
isolation:
|
|
59
|
-
invocation:
|
|
58
|
+
isolation: execution.isolation,
|
|
59
|
+
invocation: execution.agentInvocation,
|
|
60
60
|
toolCallId: params.toolCallId,
|
|
61
61
|
onSessionCreated: (session) => {
|
|
62
62
|
bgState.setSession(session);
|
|
@@ -77,8 +77,8 @@ export function spawnBackground(
|
|
|
77
77
|
return textResult(
|
|
78
78
|
`Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
79
79
|
`Agent ID: ${id}\n` +
|
|
80
|
-
`Type: ${
|
|
81
|
-
`Description: ${
|
|
80
|
+
`Type: ${identity.displayName}\n` +
|
|
81
|
+
`Description: ${execution.description}\n` +
|
|
82
82
|
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
83
83
|
(isQueued
|
|
84
84
|
? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n`
|
|
@@ -87,7 +87,7 @@ export function spawnBackground(
|
|
|
87
87
|
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
88
88
|
`Do not duplicate this agent's work.`,
|
|
89
89
|
{
|
|
90
|
-
...
|
|
90
|
+
...presentation.detailBase,
|
|
91
91
|
toolUses: 0,
|
|
92
92
|
tokens: "",
|
|
93
93
|
durationMs: 0,
|
|
@@ -56,19 +56,19 @@ export async function runForeground(
|
|
|
56
56
|
signal: AbortSignal | undefined,
|
|
57
57
|
onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
|
|
58
58
|
) {
|
|
59
|
-
const {
|
|
59
|
+
const { identity, execution, presentation } = params.config;
|
|
60
60
|
let spinnerFrame = 0;
|
|
61
61
|
const startedAt = Date.now();
|
|
62
62
|
let fgId: string | undefined;
|
|
63
63
|
|
|
64
|
-
const fgState = new AgentActivityTracker(
|
|
64
|
+
const fgState = new AgentActivityTracker(execution.effectiveMaxTurns);
|
|
65
65
|
let unsubUI: (() => void) | undefined;
|
|
66
66
|
let recordRef: AgentRecord | undefined;
|
|
67
67
|
|
|
68
68
|
const streamUpdate = () => {
|
|
69
69
|
const toolUses = recordRef?.toolUses ?? 0;
|
|
70
70
|
const details: AgentDetails = {
|
|
71
|
-
...
|
|
71
|
+
...presentation.detailBase,
|
|
72
72
|
toolUses,
|
|
73
73
|
tokens: recordRef ? formatLifetimeTokens(recordRef) : "",
|
|
74
74
|
turnCount: fgState.turnCount,
|
|
@@ -80,6 +80,7 @@ export async function runForeground(
|
|
|
80
80
|
};
|
|
81
81
|
onUpdate?.({
|
|
82
82
|
content: [{ type: "text", text: `${toolUses} tool uses...` }],
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Pi SDK ToolCallUpdate details type is not exported
|
|
83
84
|
details: details as any,
|
|
84
85
|
});
|
|
85
86
|
};
|
|
@@ -96,17 +97,17 @@ export async function runForeground(
|
|
|
96
97
|
try {
|
|
97
98
|
record = await manager.spawnAndWait(
|
|
98
99
|
params.snapshot,
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
identity.subagentType,
|
|
101
|
+
execution.prompt,
|
|
101
102
|
{
|
|
102
|
-
description:
|
|
103
|
-
model:
|
|
104
|
-
maxTurns:
|
|
105
|
-
isolated:
|
|
106
|
-
inheritContext:
|
|
107
|
-
thinkingLevel:
|
|
108
|
-
isolation:
|
|
109
|
-
invocation:
|
|
103
|
+
description: execution.description,
|
|
104
|
+
model: execution.model,
|
|
105
|
+
maxTurns: execution.effectiveMaxTurns,
|
|
106
|
+
isolated: execution.isolated,
|
|
107
|
+
inheritContext: execution.inheritContext,
|
|
108
|
+
thinkingLevel: execution.thinking,
|
|
109
|
+
isolation: execution.isolation,
|
|
110
|
+
invocation: execution.agentInvocation,
|
|
110
111
|
signal,
|
|
111
112
|
parentSessionFile: params.parentSessionFile,
|
|
112
113
|
parentSessionId: params.parentSessionId,
|
|
@@ -136,10 +137,10 @@ export async function runForeground(
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
const tokenText = formatLifetimeTokens(record);
|
|
139
|
-
const details = buildDetails(
|
|
140
|
+
const details = buildDetails(presentation.detailBase, record, fgState, { tokens: tokenText });
|
|
140
141
|
|
|
141
|
-
const fallbackNote =
|
|
142
|
-
? `Note: Unknown agent type "${
|
|
142
|
+
const fallbackNote = identity.fellBack
|
|
143
|
+
? `Note: Unknown agent type "${identity.rawType}" — using general-purpose.\n\n`
|
|
143
144
|
: "";
|
|
144
145
|
|
|
145
146
|
if (record.status === "error") {
|
|
@@ -151,7 +152,7 @@ export async function runForeground(
|
|
|
151
152
|
if (tokenText) statsParts.push(tokenText);
|
|
152
153
|
return textResult(
|
|
153
154
|
`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
|
|
154
|
-
(record.result?.trim()
|
|
155
|
+
(record.result?.trim() ?? "No output."),
|
|
155
156
|
details,
|
|
156
157
|
);
|
|
157
158
|
}
|
|
@@ -79,7 +79,7 @@ export function createGetResultTool(
|
|
|
79
79
|
} else if (record.status === "error") {
|
|
80
80
|
output += `Error: ${record.error}`;
|
|
81
81
|
} else {
|
|
82
|
-
output += record.result?.trim()
|
|
82
|
+
output += record.result?.trim() ?? "No output.";
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// Mark result as consumed — suppresses the completion notification
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
1
2
|
/**
|
|
2
3
|
* spawn-config.ts — Pure config resolution for the Agent tool.
|
|
3
4
|
*
|
|
@@ -25,12 +26,16 @@ export interface ModelInfo {
|
|
|
25
26
|
modelRegistry: unknown;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
export interface
|
|
29
|
+
/** Identity: who is being spawned. */
|
|
30
|
+
export interface SpawnIdentity {
|
|
30
31
|
subagentType: string;
|
|
31
32
|
rawType: SubagentType;
|
|
32
33
|
fellBack: boolean;
|
|
33
34
|
displayName: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Execution: how the agent will run. */
|
|
38
|
+
export interface SpawnExecution {
|
|
34
39
|
prompt: string;
|
|
35
40
|
description: string;
|
|
36
41
|
model: Model<any> | undefined;
|
|
@@ -40,12 +45,23 @@ export interface ResolvedSpawnConfig {
|
|
|
40
45
|
runInBackground: boolean;
|
|
41
46
|
isolated: boolean;
|
|
42
47
|
isolation: IsolationMode | undefined;
|
|
43
|
-
modelName: string | undefined;
|
|
44
48
|
agentInvocation: AgentInvocation;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Presentation: display/UI values derived from identity and execution. */
|
|
52
|
+
export interface SpawnPresentation {
|
|
53
|
+
modelName: string | undefined;
|
|
45
54
|
agentTags: string[];
|
|
46
55
|
detailBase: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">;
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
/** Fully resolved config for spawning an agent — composed of domain-aligned sub-interfaces. */
|
|
59
|
+
export interface ResolvedSpawnConfig {
|
|
60
|
+
identity: SpawnIdentity;
|
|
61
|
+
execution: SpawnExecution;
|
|
62
|
+
presentation: SpawnPresentation;
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
/** Error result when model resolution fails. */
|
|
50
66
|
export interface SpawnConfigError {
|
|
51
67
|
error: string;
|
|
@@ -125,22 +141,19 @@ export function resolveSpawnConfig(
|
|
|
125
141
|
};
|
|
126
142
|
|
|
127
143
|
return {
|
|
128
|
-
subagentType,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
modelName,
|
|
142
|
-
agentInvocation,
|
|
143
|
-
agentTags,
|
|
144
|
-
detailBase,
|
|
144
|
+
identity: { subagentType, rawType, fellBack, displayName },
|
|
145
|
+
execution: {
|
|
146
|
+
prompt: params.prompt as string,
|
|
147
|
+
description: params.description as string,
|
|
148
|
+
model,
|
|
149
|
+
effectiveMaxTurns,
|
|
150
|
+
thinking,
|
|
151
|
+
inheritContext,
|
|
152
|
+
runInBackground,
|
|
153
|
+
isolated,
|
|
154
|
+
isolation,
|
|
155
|
+
agentInvocation,
|
|
156
|
+
},
|
|
157
|
+
presentation: { modelName, agentTags, detailBase },
|
|
145
158
|
};
|
|
146
159
|
}
|
|
@@ -115,7 +115,7 @@ export function createAgentConfigEditor(
|
|
|
115
115
|
const fmFields: string[] = [];
|
|
116
116
|
fmFields.push(`description: ${cfg.description}`);
|
|
117
117
|
if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
|
|
118
|
-
fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ")
|
|
118
|
+
fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") ?? "all"}`);
|
|
119
119
|
if (cfg.model) fmFields.push(`model: ${cfg.model}`);
|
|
120
120
|
if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
|
|
121
121
|
if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
|
|
@@ -174,6 +174,7 @@ export function createAgentConfigEditor(
|
|
|
174
174
|
ui.notify(`Disabled ${name} (${targetPath})`, "info");
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
177
178
|
async function enableAgent(ui: MenuUI, name: string) {
|
|
178
179
|
const file = fileOps.findAgentFile(name, agentDirs());
|
|
179
180
|
if (!file) return;
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
1
2
|
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
3
|
import { AgentTypeRegistry } from "#src/config/agent-types";
|
|
3
4
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-redundant-type-constituents -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
1
2
|
/**
|
|
2
3
|
* agent-widget.ts — Persistent widget showing running/completed agents above the editor.
|
|
3
4
|
*
|
|
@@ -76,9 +77,7 @@ export class AgentWidget {
|
|
|
76
77
|
/** Ensure the widget update timer is running. */
|
|
77
78
|
// fallow-ignore-next-line unused-class-member
|
|
78
79
|
ensureTimer() {
|
|
79
|
-
|
|
80
|
-
this.widgetInterval = setInterval(() => this.update(), 80);
|
|
81
|
-
}
|
|
80
|
+
this.widgetInterval ??= setInterval(() => this.update(), 80);
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
/** Check if a finished agent should still be shown in the widget. */
|
|
@@ -299,7 +299,7 @@ export class ConversationViewer implements Component {
|
|
|
299
299
|
} else if (isBashExecution(msg)) {
|
|
300
300
|
if (needsSeparator) lines.push(th.fg("dim", "───"));
|
|
301
301
|
lines.push(truncateToWidth(th.fg("muted", ` $ ${msg.command}`), width));
|
|
302
|
-
if (msg.output
|
|
302
|
+
if (msg.output.trim()) {
|
|
303
303
|
const out = msg.output.length > 500
|
|
304
304
|
? msg.output.slice(0, 500) + "... (truncated)"
|
|
305
305
|
: msg.output;
|
package/src/ui/ui-observer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
1
2
|
/**
|
|
2
3
|
* ui-observer.ts — Subscribes to session events and updates AgentActivityTracker state.
|
|
3
4
|
*
|