@gotgenes/pi-subagents 7.4.0 → 7.5.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 +28 -0
- package/README.md +50 -17
- package/docs/architecture/architecture.md +3 -1
- package/docs/plans/0215-decompose-build-parent-context.md +166 -0
- package/docs/retro/0215-decompose-build-parent-context.md +35 -0
- package/package.json +1 -1
- package/src/lifecycle/agent-runner.ts +11 -0
- package/src/lifecycle/permission-bridge.ts +63 -0
- package/src/session/context.ts +25 -24
- package/src/ui/agent-menu.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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
|
+
## [7.5.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.5.0...pi-subagents-v7.5.1) (2026-05-26)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* plan decompose buildParentContext ([#215](https://github.com/gotgenes/pi-packages/issues/215)) ([9103609](https://github.com/gotgenes/pi-packages/commit/910360991b50c320927c1457bfef6b7cb5624b7b))
|
|
14
|
+
* **retro:** add planning stage notes for issue [#215](https://github.com/gotgenes/pi-packages/issues/215) ([5c534d5](https://github.com/gotgenes/pi-packages/commit/5c534d5efb640ef1d72d6ccf7bf2e15ac2acf755))
|
|
15
|
+
* **retro:** add TDD stage notes for issue [#215](https://github.com/gotgenes/pi-packages/issues/215) ([79064d0](https://github.com/gotgenes/pi-packages/commit/79064d072c36c2f92013dbfba58ce1de1ab01bce))
|
|
16
|
+
|
|
17
|
+
## [7.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.4.0...pi-subagents-v7.5.0) (2026-05-26)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* add permission bridge for cross-extension registration ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([1827720](https://github.com/gotgenes/pi-packages/commit/18277203f7ee10e56f90a0d4db587a4aa95376ab))
|
|
23
|
+
* register child sessions with permission system ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([0487828](https://github.com/gotgenes/pi-packages/commit/04878286d7da6660362360482fb916b1b3743ce3))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* resolve pre-existing lint errors in pi-autoformat and pi-permission-system ([68fd516](https://github.com/gotgenes/pi-packages/commit/68fd516e33ddbb9a5e37ef19e949ee9ecdc37252))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Documentation
|
|
32
|
+
|
|
33
|
+
* document permission-bridge in architecture ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([d0120ab](https://github.com/gotgenes/pi-packages/commit/d0120abdf049e2aeba14ba75071ed55b24e23dbe))
|
|
34
|
+
* update subagent integration docs for native permission bridge ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([0bd456b](https://github.com/gotgenes/pi-packages/commit/0bd456befa8ea6918e74f4393d844868795edc77))
|
|
35
|
+
|
|
8
36
|
## [7.4.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.3.2...pi-subagents-v7.4.0) (2026-05-25)
|
|
9
37
|
|
|
10
38
|
|
package/README.md
CHANGED
|
@@ -421,27 +421,57 @@ disallowed_tools: write, edit
|
|
|
421
421
|
|
|
422
422
|
This is useful for creating agents that inherit extension tools but should not have write access.
|
|
423
423
|
|
|
424
|
+
## Permission System Integration
|
|
425
|
+
|
|
426
|
+
When [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system) is installed, this extension integrates automatically:
|
|
427
|
+
|
|
428
|
+
- **Per-agent permission policies** — define `permission:` in agent YAML frontmatter to set allow/ask/deny rules per agent type.
|
|
429
|
+
The permission system resolves the agent name from the `<active_agent>` tag in the child system prompt.
|
|
430
|
+
- **Tool filtering** — the permission system's `before_agent_start` handler removes denied tools from the child session before the agent starts.
|
|
431
|
+
- **`ask`-state forwarding** — when a child session triggers an `ask` permission, the prompt forwards to the parent session's UI.
|
|
432
|
+
The parent approves or denies, and the child resumes.
|
|
433
|
+
- **Deterministic child detection** — every child session registers with the permission system's `SubagentSessionRegistry` before `bindExtensions()` fires, so detection does not rely on env vars or filesystem heuristics.
|
|
434
|
+
|
|
435
|
+
No configuration is required.
|
|
436
|
+
When `@gotgenes/pi-permission-system` is not installed, the registration calls are silent no-ops.
|
|
437
|
+
|
|
424
438
|
## Architecture
|
|
425
439
|
|
|
440
|
+
See `docs/architecture/architecture.md` for the full architecture document with domain decomposition, Mermaid diagrams, and improvement roadmap.
|
|
441
|
+
|
|
426
442
|
```text
|
|
427
443
|
src/
|
|
428
|
-
index.ts
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
444
|
+
index.ts # Extension entry: tool/command registration, rendering
|
|
445
|
+
runtime.ts # Session-scoped state bag with methods
|
|
446
|
+
types.ts # Shared type definitions
|
|
447
|
+
settings.ts # Persistent settings (concurrency, turn limits)
|
|
448
|
+
config/ # Agent type registry and configuration
|
|
449
|
+
agent-types.ts # Unified agent registry (defaults + custom)
|
|
450
|
+
default-agents.ts # Embedded default agent configs
|
|
451
|
+
custom-agents.ts # Load user-defined agents from .pi/agents/*.md
|
|
452
|
+
invocation-config.ts # Per-call merge of tool params + agent config
|
|
453
|
+
session/ # Pure session assembly
|
|
454
|
+
session-config.ts # Session configuration assembler
|
|
455
|
+
prompts.ts # Config-driven system prompt builder
|
|
456
|
+
context.ts # Parent conversation context for inherit_context
|
|
457
|
+
skill-loader.ts # Preload skills from Pi-standard + Agent Skills spec
|
|
458
|
+
env.ts # Environment detection (git, platform)
|
|
459
|
+
model-resolver.ts # Fuzzy model matching
|
|
460
|
+
lifecycle/ # Agent execution and state tracking
|
|
461
|
+
agent-manager.ts # Spawn, queue, abort, resume, concurrency
|
|
462
|
+
agent-runner.ts # Session creation, turn loop, tool filtering
|
|
463
|
+
agent-record.ts # Status state machine
|
|
464
|
+
parent-snapshot.ts # Immutable spawn-time parent state
|
|
465
|
+
permission-bridge.ts # Optional bridge to pi-permission-system registry
|
|
466
|
+
worktree.ts # Git worktree isolation
|
|
467
|
+
observation/ # Progress tracking and notification
|
|
468
|
+
record-observer.ts # Session-event stats observer
|
|
469
|
+
notification.ts # Completion nudges
|
|
470
|
+
service/ # Cross-extension API boundary
|
|
471
|
+
service.ts # SubagentsService interface + Symbol.for() accessors
|
|
472
|
+
service-adapter.ts # SubagentsService wrapper around AgentManager
|
|
473
|
+
tools/ # LLM-facing tools
|
|
474
|
+
ui/ # Widget, conversation viewer, /agents menu
|
|
445
475
|
```
|
|
446
476
|
|
|
447
477
|
## Deviations from upstream
|
|
@@ -458,6 +488,9 @@ Each has a corresponding upstream PR:
|
|
|
458
488
|
3. **`<active_agent>` system-prompt tag** (`src/prompts.ts`) — `buildAgentPrompt` prepends `<active_agent name="${config.name}"/>` to every assembled child system prompt (both `replace` and `append` modes).
|
|
459
489
|
Downstream extensions like [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system) parse this tag to resolve per-agent `permission:` frontmatter inside the child session.
|
|
460
490
|
Upstream PR: [tintinweb/pi-subagents#73](https://github.com/tintinweb/pi-subagents/pull/73).
|
|
491
|
+
4. **Permission-system registration** (`src/lifecycle/permission-bridge.ts`) — `runAgent` registers every child session with `@gotgenes/pi-permission-system`'s `SubagentSessionRegistry` before `bindExtensions()` and unregisters in the `finally` block.
|
|
492
|
+
This enables deterministic child detection and `ask`-state forwarding to the parent UI.
|
|
493
|
+
No upstream equivalent — this feature is specific to the `@gotgenes` fork.
|
|
461
494
|
|
|
462
495
|
The upstream `vitest` suite plus tests added for each patch all pass on every commit.
|
|
463
496
|
|
|
@@ -259,6 +259,7 @@ src/
|
|
|
259
259
|
│ ├── agent-record.ts status state machine
|
|
260
260
|
│ ├── parent-snapshot.ts immutable spawn-time parent state
|
|
261
261
|
│ ├── execution-state.ts session/output phase state
|
|
262
|
+
│ ├── permission-bridge.ts optional bridge to pi-permission-system registry
|
|
262
263
|
│ ├── worktree.ts git worktree isolation
|
|
263
264
|
│ ├── worktree-state.ts worktree phase state
|
|
264
265
|
│ └── usage.ts token usage tracking
|
|
@@ -333,7 +334,8 @@ They declare this package as an optional peer dependency and use dynamic import
|
|
|
333
334
|
|
|
334
335
|
- The three tools: `Agent`, `get_subagent_result`, `steer_subagent`.
|
|
335
336
|
- `AgentManager` — spawn, queue, abort, resume, concurrency control.
|
|
336
|
-
- `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3).
|
|
337
|
+
- `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3), permission-system registration.
|
|
338
|
+
- `permission-bridge` — optional cross-extension bridge to `@gotgenes/pi-permission-system`; registers each child session with `SubagentSessionRegistry` before `bindExtensions()` so the permission system detects in-process children deterministically.
|
|
337
339
|
- `session-config` — pure configuration assembler (extracted from `agent-runner`).
|
|
338
340
|
- `SubagentRuntime` — session-scoped state bag with methods.
|
|
339
341
|
- `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 215
|
|
3
|
+
issue_title: "Decompose buildParentContext (cognitive 30) (Phase 13, Step 2)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Decompose `buildParentContext`
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`buildParentContext` in `src/session/context.ts` is the only remaining fallow refactoring target in the package.
|
|
11
|
+
The function has a cognitive complexity of 30, driven by a loop with three type-check branches (`message`, `compaction`, default), each with sub-branches for role (`user` vs `assistant`) and content type (`string` vs array).
|
|
12
|
+
The architecture roadmap (Phase 13, Step 2) targets cognitive complexity < 10 and function body < 15 LOC.
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Extract per-entry-type formatters: `formatMessageEntry(entry)` and `formatCompactionEntry(entry)`.
|
|
17
|
+
- Reduce `buildParentContext` to a loop + filter + join orchestrator (< 15 LOC).
|
|
18
|
+
- Achieve cognitive complexity < 10 for all functions in the file.
|
|
19
|
+
- Add unit tests for the extracted formatters and the orchestrator.
|
|
20
|
+
|
|
21
|
+
## Non-Goals
|
|
22
|
+
|
|
23
|
+
- Changing the public API surface (`buildParentContext`, `extractText`) — signatures stay the same.
|
|
24
|
+
- Moving `extractText` to another module (noted as a follow-up in prior plans but out of scope).
|
|
25
|
+
- Refactoring callers (`parent-snapshot.ts`) — they are already tested via mocks.
|
|
26
|
+
|
|
27
|
+
## Background
|
|
28
|
+
|
|
29
|
+
### Current file: `src/session/context.ts`
|
|
30
|
+
|
|
31
|
+
The file exports two functions:
|
|
32
|
+
|
|
33
|
+
1. `extractText(content: unknown[]): string` — filters an array of content blocks to `TextContent` items and joins their `.text` values.
|
|
34
|
+
Used by `agent-runner.ts`, `message-formatters.ts`, and `buildParentContext` itself.
|
|
35
|
+
2. `buildParentContext(ctx: SessionContext): string` — iterates session branch entries, formatting `message` entries (user/assistant) and `compaction` entries into a text representation prefixed with a header.
|
|
36
|
+
|
|
37
|
+
The file also defines three local types (`MessageEntry`, `CompactionEntry`, `BranchEntry`) and one helper (`isTextContent`).
|
|
38
|
+
|
|
39
|
+
### Callers
|
|
40
|
+
|
|
41
|
+
- `buildParentContext` is called only from `parent-snapshot.ts` (where it is mocked in tests).
|
|
42
|
+
- `extractText` is called from `agent-runner.ts`, `message-formatters.ts`, and internally within `buildParentContext`.
|
|
43
|
+
|
|
44
|
+
### Existing tests
|
|
45
|
+
|
|
46
|
+
There are no direct unit tests for `context.ts`.
|
|
47
|
+
`parent-snapshot.test.ts` mocks `buildParentContext` entirely, so the formatting logic is currently untested.
|
|
48
|
+
|
|
49
|
+
## Design Overview
|
|
50
|
+
|
|
51
|
+
### Extracted formatters
|
|
52
|
+
|
|
53
|
+
Each formatter takes a typed entry and returns `string | undefined` (undefined when the entry should be skipped):
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
function formatMessageEntry(entry: MessageEntry): string | undefined {
|
|
57
|
+
const msg = entry.message;
|
|
58
|
+
const text =
|
|
59
|
+
typeof msg.content === "string"
|
|
60
|
+
? msg.content
|
|
61
|
+
: extractText(msg.content);
|
|
62
|
+
if (!text.trim()) return undefined;
|
|
63
|
+
if (msg.role === "user") return `[User]: ${text.trim()}`;
|
|
64
|
+
if (msg.role === "assistant") return `[Assistant]: ${text.trim()}`;
|
|
65
|
+
return undefined; // skip toolResult and other roles
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatCompactionEntry(entry: CompactionEntry): string | undefined {
|
|
69
|
+
return entry.summary ? `[Summary]: ${entry.summary}` : undefined;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Simplified orchestrator
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
export function buildParentContext(ctx: SessionContext): string {
|
|
77
|
+
const entries = ctx.sessionManager.getBranch();
|
|
78
|
+
if (!entries || entries.length === 0) return "";
|
|
79
|
+
|
|
80
|
+
const parts = (entries as BranchEntry[])
|
|
81
|
+
.map(formatBranchEntry)
|
|
82
|
+
.filter((p): p is string => p !== undefined);
|
|
83
|
+
|
|
84
|
+
if (parts.length === 0) return "";
|
|
85
|
+
|
|
86
|
+
return `# Parent Conversation Context
|
|
87
|
+
The following is the conversation history from the parent session that spawned you.
|
|
88
|
+
Use this context to understand what has been discussed and decided so far.
|
|
89
|
+
|
|
90
|
+
${parts.join("\n\n")}
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
# Your Task (below)
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A thin dispatcher (`formatBranchEntry`) routes by `type`:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
function formatBranchEntry(entry: BranchEntry): string | undefined {
|
|
102
|
+
if (entry.type === "message") return formatMessageEntry(entry as MessageEntry);
|
|
103
|
+
if (entry.type === "compaction") return formatCompactionEntry(entry as CompactionEntry);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Complexity analysis
|
|
109
|
+
|
|
110
|
+
- `formatMessageEntry`: 3 branches (string-vs-array, empty check, role) — estimated cognitive complexity ~4.
|
|
111
|
+
- `formatCompactionEntry`: 1 branch — estimated cognitive complexity ~1.
|
|
112
|
+
- `formatBranchEntry`: 2 branches — estimated cognitive complexity ~2.
|
|
113
|
+
- `buildParentContext`: 2 branches (empty entries, empty parts) — estimated cognitive complexity ~3.
|
|
114
|
+
|
|
115
|
+
All well under the < 10 target.
|
|
116
|
+
|
|
117
|
+
## Module-Level Changes
|
|
118
|
+
|
|
119
|
+
### `src/session/context.ts`
|
|
120
|
+
|
|
121
|
+
1. Add `formatMessageEntry(entry: MessageEntry): string | undefined` — private helper.
|
|
122
|
+
2. Add `formatCompactionEntry(entry: CompactionEntry): string | undefined` — private helper.
|
|
123
|
+
3. Add `formatBranchEntry(entry: BranchEntry): string | undefined` — private dispatcher.
|
|
124
|
+
4. Simplify `buildParentContext` body to use `map(formatBranchEntry).filter(...)`.
|
|
125
|
+
5. No changes to exports — `buildParentContext` and `extractText` signatures are unchanged.
|
|
126
|
+
6. No changes to local types (`MessageEntry`, `CompactionEntry`, `BranchEntry`) or `isTextContent`.
|
|
127
|
+
|
|
128
|
+
### `test/session/context.test.ts` (new)
|
|
129
|
+
|
|
130
|
+
Unit tests for:
|
|
131
|
+
|
|
132
|
+
- `extractText` — string extraction from mixed content arrays.
|
|
133
|
+
- `buildParentContext` — end-to-end formatting with user, assistant, compaction, and skipped entries.
|
|
134
|
+
|
|
135
|
+
The formatters are private, so they are tested indirectly through `buildParentContext`.
|
|
136
|
+
|
|
137
|
+
## Test Impact Analysis
|
|
138
|
+
|
|
139
|
+
1. The new `context.test.ts` enables direct testing of formatting logic that was previously untested (mocked away in `parent-snapshot.test.ts`).
|
|
140
|
+
2. No existing tests become redundant — `parent-snapshot.test.ts` tests snapshot assembly, not formatting.
|
|
141
|
+
3. No existing tests need modification — the public API is unchanged.
|
|
142
|
+
|
|
143
|
+
## TDD Order
|
|
144
|
+
|
|
145
|
+
1. **Red → Green:** Add `test/session/context.test.ts` with tests for `extractText` — empty array, text-only, mixed content types, no text content.
|
|
146
|
+
Commit: `test: add extractText unit tests (#215)`
|
|
147
|
+
|
|
148
|
+
2. **Red → Green:** Add tests for `buildParentContext` — empty branch, user messages, assistant messages, compaction entries with/without summary, mixed entry types, entries with empty text (skipped), non-message/non-compaction entries (skipped), string vs array content.
|
|
149
|
+
Commit: `test: add buildParentContext unit tests (#215)`
|
|
150
|
+
|
|
151
|
+
3. **Refactor:** Extract `formatMessageEntry`, `formatCompactionEntry`, and `formatBranchEntry` from `buildParentContext`.
|
|
152
|
+
Simplify `buildParentContext` to map/filter/join.
|
|
153
|
+
All tests from steps 1–2 must still pass.
|
|
154
|
+
Commit: `refactor: decompose buildParentContext into per-entry formatters (#215)`
|
|
155
|
+
|
|
156
|
+
## Risks and Mitigations
|
|
157
|
+
|
|
158
|
+
| Risk | Mitigation |
|
|
159
|
+
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
160
|
+
| Behavioral regression in formatting | Steps 1–2 lock in current behavior with tests before refactoring |
|
|
161
|
+
| Extracted helpers expose implementation details | Helpers are private (not exported); tested indirectly via public API |
|
|
162
|
+
| `eslint-disable` comment for `no-unnecessary-condition` on `getBranch()` check may need adjustment | Preserve the comment — runtime nullability is documented |
|
|
163
|
+
|
|
164
|
+
## Open Questions
|
|
165
|
+
|
|
166
|
+
None — the decomposition target and strategy are specified by the architecture roadmap.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 215
|
|
3
|
+
issue_title: "Decompose buildParentContext (cognitive 30) (Phase 13, Step 2)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #215 — Decompose buildParentContext
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-25T12:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 3-step TDD plan to decompose `buildParentContext` in `src/session/context.ts`.
|
|
13
|
+
Steps 1–2 add tests locking current behavior for `extractText` and `buildParentContext`; step 3 extracts three private helpers (`formatMessageEntry`, `formatCompactionEntry`, `formatBranchEntry`) and simplifies the orchestrator to map/filter/join.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- No existing unit tests cover `context.ts` — `parent-snapshot.test.ts` mocks `buildParentContext` entirely, so the formatting logic is currently untested.
|
|
18
|
+
- The decomposition is straightforward with no design ambiguity; the architecture roadmap specifies the exact extraction targets.
|
|
19
|
+
- All extracted helpers remain private (not exported), keeping the public API surface unchanged.
|
|
20
|
+
- The `eslint-disable` comment on the `getBranch()` nullability check must be preserved through the refactoring step.
|
|
21
|
+
|
|
22
|
+
## Stage: Implementation — TDD (2026-05-25T22:36:00Z)
|
|
23
|
+
|
|
24
|
+
### Session summary
|
|
25
|
+
|
|
26
|
+
Completed all 3 TDD steps: 2 test-only commits locking `extractText` (5 tests) and `buildParentContext` (14 tests) behavior, then a refactor commit extracting `formatMessageEntry`, `formatCompactionEntry`, and `formatBranchEntry`.
|
|
27
|
+
Test count increased from 939 to 958 (+19).
|
|
28
|
+
All checks green: full suite, `pnpm run check`, `pnpm run lint`, `pnpm fallow dead-code`.
|
|
29
|
+
|
|
30
|
+
### Observations
|
|
31
|
+
|
|
32
|
+
- Because `extractText` and `buildParentContext` already existed, both test steps passed immediately (no red phase) — this is correct for behavior-locking tests before a refactor.
|
|
33
|
+
- The `makeCtx` helper in the test file creates a minimal `SessionContext` satisfying only `sessionManager.getBranch()`; the extra required fields (`cwd`, `model`, `modelRegistry`, `getSystemPrompt`) are satisfied with stubs.
|
|
34
|
+
- The `eslint-disable` comment on the `getBranch()` nullability check was preserved unchanged through the refactor.
|
|
35
|
+
- No deviations from the plan.
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
12
12
|
import type { ParentSessionInfo } from "#src/lifecycle/agent-manager";
|
|
13
13
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
14
|
+
import { registerChildSession, unregisterChildSession } from "#src/lifecycle/permission-bridge";
|
|
14
15
|
import { extractAssistantContent } from "#src/session/content-items";
|
|
15
16
|
import { extractText } from "#src/session/context";
|
|
16
17
|
import type { EnvInfo } from "#src/session/env";
|
|
@@ -347,6 +348,15 @@ export async function runAgent(
|
|
|
347
348
|
session.setActiveToolsByName(filtered);
|
|
348
349
|
}
|
|
349
350
|
|
|
351
|
+
// Register with pi-permission-system's SubagentSessionRegistry before
|
|
352
|
+
// bindExtensions() so isSubagentExecutionContext() hits the registry on the
|
|
353
|
+
// first check during child extension initialization. Unregistered in the
|
|
354
|
+
// finally block below to guarantee cleanup on both success and error paths.
|
|
355
|
+
registerChildSession(sessionDir, {
|
|
356
|
+
agentName: type,
|
|
357
|
+
parentSessionId: options.context.parentSession?.parentSessionId,
|
|
358
|
+
});
|
|
359
|
+
|
|
350
360
|
// Bind extensions so that session_start fires and extensions can initialize
|
|
351
361
|
// (e.g. loading credentials, setting up state). Placed after tool filtering
|
|
352
362
|
// so extension-provided skills/prompts from extendResourcesFromExtensions()
|
|
@@ -406,6 +416,7 @@ export async function runAgent(
|
|
|
406
416
|
unsubTurns();
|
|
407
417
|
collector.unsubscribe();
|
|
408
418
|
cleanupAbort();
|
|
419
|
+
unregisterChildSession(sessionDir);
|
|
409
420
|
}
|
|
410
421
|
|
|
411
422
|
const responseText =
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* permission-bridge.ts — Cross-extension bridge to @gotgenes/pi-permission-system.
|
|
3
|
+
*
|
|
4
|
+
* pi-subagents does not import pi-permission-system directly. Instead it
|
|
5
|
+
* accesses the published PermissionsService via a process-global Symbol.for()
|
|
6
|
+
* key, the same mechanism pi-permission-system uses to publish itself.
|
|
7
|
+
*
|
|
8
|
+
* When pi-permission-system is not installed, getPermissionsService() returns
|
|
9
|
+
* undefined and all registration calls are silent no-ops.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The two PermissionsService methods pi-subagents needs.
|
|
14
|
+
*
|
|
15
|
+
* Follows ISP — does not expose the full PermissionsService surface
|
|
16
|
+
* (checkPermission, getToolPermission, etc.) to avoid coupling.
|
|
17
|
+
*/
|
|
18
|
+
interface PermissionsServiceConsumer {
|
|
19
|
+
registerSubagentSession(
|
|
20
|
+
sessionKey: string,
|
|
21
|
+
info: { parentSessionId?: string; agentName: string },
|
|
22
|
+
): void;
|
|
23
|
+
unregisterSubagentSession(sessionKey: string): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PERMISSION_SERVICE_KEY = Symbol.for(
|
|
27
|
+
"@gotgenes/pi-permission-system:service",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function getPermissionsService(): PermissionsServiceConsumer | undefined {
|
|
31
|
+
return (globalThis as Record<symbol, unknown>)[
|
|
32
|
+
PERMISSION_SERVICE_KEY
|
|
33
|
+
] as PermissionsServiceConsumer | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register a child session with pi-permission-system's SubagentSessionRegistry.
|
|
38
|
+
*
|
|
39
|
+
* Must be called after deriving sessionDir but before session.bindExtensions()
|
|
40
|
+
* so isSubagentExecutionContext() hits the registry on the first check during
|
|
41
|
+
* child extension initialization.
|
|
42
|
+
*
|
|
43
|
+
* @param sessionKey - The session directory path (unique per session).
|
|
44
|
+
* @param info - Agent name and optional parent session ID for forwarding.
|
|
45
|
+
*/
|
|
46
|
+
export function registerChildSession(
|
|
47
|
+
sessionKey: string,
|
|
48
|
+
info: { parentSessionId?: string; agentName: string },
|
|
49
|
+
): void {
|
|
50
|
+
getPermissionsService()?.registerSubagentSession(sessionKey, info);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Unregister a child session from pi-permission-system's SubagentSessionRegistry.
|
|
55
|
+
*
|
|
56
|
+
* Must be called in a finally block so cleanup happens on both success and
|
|
57
|
+
* error paths.
|
|
58
|
+
*
|
|
59
|
+
* @param sessionKey - The session directory path used during registration.
|
|
60
|
+
*/
|
|
61
|
+
export function unregisterChildSession(sessionKey: string): void {
|
|
62
|
+
getPermissionsService()?.unregisterSubagentSession(sessionKey);
|
|
63
|
+
}
|
package/src/session/context.ts
CHANGED
|
@@ -30,6 +30,28 @@ export function extractText(content: unknown[]): string {
|
|
|
30
30
|
.join("\n");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Format a message entry (user/assistant); returns undefined for roles to skip. */
|
|
34
|
+
function formatMessageEntry(entry: MessageEntry): string | undefined {
|
|
35
|
+
const msg = entry.message;
|
|
36
|
+
const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
37
|
+
if (!text.trim()) return undefined;
|
|
38
|
+
if (msg.role === "user") return `[User]: ${text.trim()}`;
|
|
39
|
+
if (msg.role === "assistant") return `[Assistant]: ${text.trim()}`;
|
|
40
|
+
return undefined; // skip toolResult and other roles
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Format a compaction entry; returns undefined when no summary is present. */
|
|
44
|
+
function formatCompactionEntry(entry: CompactionEntry): string | undefined {
|
|
45
|
+
return entry.summary ? `[Summary]: ${entry.summary}` : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Dispatch a branch entry to the appropriate formatter. */
|
|
49
|
+
function formatBranchEntry(entry: BranchEntry): string | undefined {
|
|
50
|
+
if (entry.type === "message") return formatMessageEntry(entry as MessageEntry);
|
|
51
|
+
if (entry.type === "compaction") return formatCompactionEntry(entry as CompactionEntry);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
/**
|
|
34
56
|
* Build a text representation of the parent conversation context.
|
|
35
57
|
* Used when inherit_context is true to give the subagent visibility
|
|
@@ -40,30 +62,9 @@ export function buildParentContext(ctx: SessionContext): string {
|
|
|
40
62
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- getBranch() may return undefined at runtime despite its type
|
|
41
63
|
if (!entries || entries.length === 0) return "";
|
|
42
64
|
|
|
43
|
-
const parts
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (rawEntry.type === "message") {
|
|
47
|
-
const entry = rawEntry as MessageEntry;
|
|
48
|
-
const msg = entry.message;
|
|
49
|
-
if (msg.role === "user") {
|
|
50
|
-
const text = typeof msg.content === "string"
|
|
51
|
-
? msg.content
|
|
52
|
-
: extractText(msg.content);
|
|
53
|
-
if (text.trim()) parts.push(`[User]: ${text.trim()}`);
|
|
54
|
-
} else if (msg.role === "assistant") {
|
|
55
|
-
const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
56
|
-
if (text.trim()) parts.push(`[Assistant]: ${text.trim()}`);
|
|
57
|
-
}
|
|
58
|
-
// Skip toolResult messages — too verbose for context
|
|
59
|
-
} else if (rawEntry.type === "compaction") {
|
|
60
|
-
// Include compaction summaries — they're already condensed
|
|
61
|
-
const entry = rawEntry as CompactionEntry;
|
|
62
|
-
if (entry.summary) {
|
|
63
|
-
parts.push(`[Summary]: ${entry.summary}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
65
|
+
const parts = (entries as BranchEntry[])
|
|
66
|
+
.map(formatBranchEntry)
|
|
67
|
+
.filter((p): p is string => p !== undefined);
|
|
67
68
|
|
|
68
69
|
if (parts.length === 0) return "";
|
|
69
70
|
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -72,9 +72,9 @@ export class AgentsMenuHandler {
|
|
|
72
72
|
private readonly registry: AgentTypeRegistry,
|
|
73
73
|
private readonly agentActivity: AgentActivityReader,
|
|
74
74
|
private readonly settings: AgentMenuSettings,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
fileOps: AgentFileOps,
|
|
76
|
+
personalAgentsDir: string,
|
|
77
|
+
projectAgentsDir: string,
|
|
78
78
|
) {
|
|
79
79
|
this.editor = new AgentConfigEditor(
|
|
80
80
|
fileOps,
|