pi-extended-teams 2.1.16
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/README.md +105 -0
- package/assets/pi-extended-teams-agent-navigation.png +0 -0
- package/assets/pi-extended-teams-in-action.png +0 -0
- package/extensions/agents/read-agent-report.ts +181 -0
- package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
- package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
- package/extensions/agents/read-agent.test.ts +3077 -0
- package/extensions/agents/read-agent.ts +1119 -0
- package/extensions/agents/write-agent.test.ts +513 -0
- package/extensions/agents/write-agent.ts +392 -0
- package/extensions/events/register-events.test.ts +465 -0
- package/extensions/events/register-events.ts +409 -0
- package/extensions/index.test.ts +1659 -0
- package/extensions/index.ts +1228 -0
- package/extensions/internal/agent-session-files.test.ts +164 -0
- package/extensions/internal/agent-session-files.ts +320 -0
- package/extensions/internal/debug.ts +44 -0
- package/extensions/internal/model-selection.ts +82 -0
- package/extensions/internal/pi-command.test.ts +191 -0
- package/extensions/internal/pi-command.ts +224 -0
- package/extensions/internal/pi-runtime-api.test.ts +43 -0
- package/extensions/internal/pi-runtime-api.ts +78 -0
- package/extensions/internal/schema.ts +19 -0
- package/extensions/internal/session-context-reference.test.ts +290 -0
- package/extensions/internal/session-context-reference.ts +430 -0
- package/extensions/internal/session-files.test.ts +225 -0
- package/extensions/internal/session-files.ts +273 -0
- package/extensions/internal/session-usage.ts +59 -0
- package/extensions/resources/spawn-resource-plan.test.ts +233 -0
- package/extensions/resources/spawn-resource-plan.ts +247 -0
- package/extensions/runtime/active-agent-sleep.test.ts +157 -0
- package/extensions/runtime/active-agent-sleep.ts +117 -0
- package/extensions/runtime/nested-read-agents.ts +22 -0
- package/extensions/runtime/pending-child-controller.test.ts +169 -0
- package/extensions/runtime/pending-child-controller.ts +280 -0
- package/extensions/runtime/types.ts +68 -0
- package/extensions/team/contracts.test.ts +111 -0
- package/extensions/team/lifecycle.test.ts +1095 -0
- package/extensions/team/lifecycle.ts +502 -0
- package/extensions/team/recipient-closure.test.ts +97 -0
- package/extensions/team/recipient-closure.ts +120 -0
- package/extensions/team/roster.test.ts +129 -0
- package/extensions/team/roster.ts +165 -0
- package/extensions/team/team-contracts.json +43 -0
- package/extensions/team/writer-screens.test.ts +50 -0
- package/extensions/team/writer-screens.ts +156 -0
- package/extensions/tools/agent-communication-tools.test.ts +306 -0
- package/extensions/tools/agent-communication-tools.ts +183 -0
- package/extensions/tools/coordination-tools.test.ts +670 -0
- package/extensions/tools/coordination-tools.ts +304 -0
- package/extensions/tools/delegation-guard.test.ts +95 -0
- package/extensions/tools/delegation-guard.ts +65 -0
- package/extensions/tools/file-claim-tools.test.ts +104 -0
- package/extensions/tools/file-claim-tools.ts +68 -0
- package/extensions/tools/model-tools.ts +53 -0
- package/extensions/tools/predefined-tools.test.ts +647 -0
- package/extensions/tools/predefined-tools.ts +331 -0
- package/extensions/tools/read-helper.test.ts +35 -0
- package/extensions/tools/task-runtime-tools.test.ts +334 -0
- package/extensions/tools/task-runtime-tools.ts +227 -0
- package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
- package/extensions/tools/team-tools.ts +1270 -0
- package/extensions/ui/agent-follow-view.test.ts +520 -0
- package/extensions/ui/agent-follow-view.ts +779 -0
- package/extensions/ui/agent-navigation.test.ts +52 -0
- package/extensions/ui/agent-navigation.ts +55 -0
- package/extensions/ui/ansi.ts +16 -0
- package/extensions/ui/extensions-command.test.ts +336 -0
- package/extensions/ui/extensions-command.ts +282 -0
- package/extensions/ui/favorite-models-command.test.ts +280 -0
- package/extensions/ui/favorite-models-command.ts +423 -0
- package/extensions/ui/frame.ts +77 -0
- package/extensions/ui/input.ts +17 -0
- package/extensions/ui/read-agent-status.test.ts +117 -0
- package/extensions/ui/read-agent-status.ts +125 -0
- package/extensions/ui/renderers.ts +243 -0
- package/extensions/ui/status-widget.test.ts +143 -0
- package/extensions/ui/status-widget.ts +440 -0
- package/extensions/ui-frame.test.ts +69 -0
- package/package.json +75 -0
- package/skills/teams.md +256 -0
- package/src/adapters/terminal-registry.ts +78 -0
- package/src/adapters/tmux-adapter.test.ts +276 -0
- package/src/adapters/tmux-adapter.ts +200 -0
- package/src/orchestration/index.ts +437 -0
- package/src/orchestration/orchestrator.test.ts +396 -0
- package/src/orchestration/types.ts +125 -0
- package/src/utils/atomic-json.ts +21 -0
- package/src/utils/claims.test.ts +137 -0
- package/src/utils/claims.ts +169 -0
- package/src/utils/hooks.test.ts +75 -0
- package/src/utils/hooks.ts +35 -0
- package/src/utils/lifecycle-tombstone.test.ts +105 -0
- package/src/utils/lifecycle-tombstone.ts +268 -0
- package/src/utils/lock.race.child.ts +44 -0
- package/src/utils/lock.race.test.ts +198 -0
- package/src/utils/lock.test.ts +90 -0
- package/src/utils/lock.ts +186 -0
- package/src/utils/messaging.test.ts +337 -0
- package/src/utils/messaging.ts +441 -0
- package/src/utils/model-resolution.test.ts +231 -0
- package/src/utils/model-resolution.ts +322 -0
- package/src/utils/models.test.ts +8 -0
- package/src/utils/models.ts +115 -0
- package/src/utils/paths.ts +81 -0
- package/src/utils/predefined-teams/types.ts +48 -0
- package/src/utils/predefined-teams.save-template.test.ts +74 -0
- package/src/utils/predefined-teams.test.ts +441 -0
- package/src/utils/predefined-teams.ts +471 -0
- package/src/utils/read-helper-queue.ts +99 -0
- package/src/utils/report-events.test.ts +154 -0
- package/src/utils/report-events.ts +222 -0
- package/src/utils/runtime.test.ts +314 -0
- package/src/utils/runtime.ts +261 -0
- package/src/utils/security.test.ts +43 -0
- package/src/utils/settings.test.ts +480 -0
- package/src/utils/settings.ts +645 -0
- package/src/utils/shared-memory.test.ts +80 -0
- package/src/utils/shared-memory.ts +81 -0
- package/src/utils/tasks.race.test.ts +44 -0
- package/src/utils/tasks.test.ts +229 -0
- package/src/utils/tasks.ts +396 -0
- package/src/utils/teams.ts +231 -0
- package/src/utils/terminal-adapter.ts +103 -0
- package/src/utils/thinking-levels.test.ts +56 -0
- package/src/utils/thinking-levels.ts +47 -0
- package/src/utils/workflow-metadata.test.ts +11 -0
- package/src/utils/workflow-metadata.ts +88 -0
- package/src/utils/write-queue.test.ts +251 -0
- package/src/utils/write-queue.ts +202 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-extended-teams settings
|
|
3
|
+
*
|
|
4
|
+
* Loads configuration that drives agent roles, per-role and per-category model
|
|
5
|
+
* selection, the watchdog buffer, the write-agent cap, and which pi extensions
|
|
6
|
+
* spawned agents may load.
|
|
7
|
+
*
|
|
8
|
+
* Locations (project overrides global, both over built-in defaults):
|
|
9
|
+
* - Global: ~/.pi/agent/pi-extended-teams/settings.json
|
|
10
|
+
* - Project: <project>/.pi/pi-extended-teams.json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { normalizeQualifiedModel } from "./model-resolution";
|
|
17
|
+
import { THINKING_LEVEL_NAMES, type ThinkingLevelName } from "./thinking-levels";
|
|
18
|
+
|
|
19
|
+
export { THINKING_LEVEL_NAMES, type ThinkingLevelName } from "./thinking-levels";
|
|
20
|
+
|
|
21
|
+
export type AgentRole = "read" | "write";
|
|
22
|
+
|
|
23
|
+
export const AGENT_ROLES: AgentRole[] = ["read", "write"];
|
|
24
|
+
|
|
25
|
+
const THINKING_LEVELS = new Set<string>(THINKING_LEVEL_NAMES);
|
|
26
|
+
|
|
27
|
+
export const FAVORITE_MODEL_SLOTS = [
|
|
28
|
+
"read-collect",
|
|
29
|
+
"read-review",
|
|
30
|
+
"read-analyze",
|
|
31
|
+
"read-critical",
|
|
32
|
+
"write-patch",
|
|
33
|
+
"write-feature",
|
|
34
|
+
"write-system",
|
|
35
|
+
"write-critical",
|
|
36
|
+
] as const;
|
|
37
|
+
export type CanonicalFavoriteModelSlot = (typeof FAVORITE_MODEL_SLOTS)[number];
|
|
38
|
+
|
|
39
|
+
export const LEGACY_FAVORITE_MODEL_SLOT_ALIASES = {
|
|
40
|
+
"reading-fast": "read-collect",
|
|
41
|
+
"reading-default": "read-review",
|
|
42
|
+
"reading-hard": "read-critical",
|
|
43
|
+
"writing-basic": "write-patch",
|
|
44
|
+
"writing-hard": "write-system",
|
|
45
|
+
} as const satisfies Record<string, CanonicalFavoriteModelSlot>;
|
|
46
|
+
export type LegacyFavoriteModelSlot = keyof typeof LEGACY_FAVORITE_MODEL_SLOT_ALIASES;
|
|
47
|
+
/** Canonical tier or a minor-release compatibility alias accepted at public boundaries. */
|
|
48
|
+
export type FavoriteModelSlot = CanonicalFavoriteModelSlot | LegacyFavoriteModelSlot;
|
|
49
|
+
export const LEGACY_FAVORITE_MODEL_SLOTS = Object.keys(LEGACY_FAVORITE_MODEL_SLOT_ALIASES) as LegacyFavoriteModelSlot[];
|
|
50
|
+
export const ACCEPTED_FAVORITE_MODEL_SLOTS = [
|
|
51
|
+
...FAVORITE_MODEL_SLOTS,
|
|
52
|
+
...LEGACY_FAVORITE_MODEL_SLOTS,
|
|
53
|
+
] as readonly FavoriteModelSlot[];
|
|
54
|
+
const CANONICAL_FAVORITE_MODEL_SLOT_SET = new Set<string>(FAVORITE_MODEL_SLOTS);
|
|
55
|
+
const FAVORITE_MODEL_SLOT_SET = new Set<string>(ACCEPTED_FAVORITE_MODEL_SLOTS);
|
|
56
|
+
|
|
57
|
+
/** Per-role default model/thinking. `null`/omitted means "inherit". */
|
|
58
|
+
export interface RoleModelConfig {
|
|
59
|
+
model: string | null;
|
|
60
|
+
thinking: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A favorite model slot selected by spawn-time workload intent. */
|
|
64
|
+
export interface FavoriteModelConfig {
|
|
65
|
+
model: string | null;
|
|
66
|
+
thinking: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A named preset bundling a role with optional model/thinking overrides. */
|
|
70
|
+
export interface CategoryConfig {
|
|
71
|
+
role?: AgentRole;
|
|
72
|
+
model: string | null;
|
|
73
|
+
thinking: string | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface WatchdogConfig {
|
|
77
|
+
/** Grace buffer (seconds) added on top of the heartbeat interval. */
|
|
78
|
+
bufferSeconds: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface WriteAgentsConfig {
|
|
82
|
+
/** Maximum concurrent write agents. */
|
|
83
|
+
maxConcurrent: number;
|
|
84
|
+
/** When at capacity, queue the spawn instead of rejecting it. */
|
|
85
|
+
queueOverflow: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ReadAgentsConfig {
|
|
89
|
+
/** Maximum concurrent in-process read agents for a team. */
|
|
90
|
+
maxConcurrent: number;
|
|
91
|
+
/** When at capacity, queue the spawn instead of rejecting it. */
|
|
92
|
+
queueOverflow: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ReadHelpersConfig {
|
|
96
|
+
/** Maximum concurrent lead-run read helpers for a team. */
|
|
97
|
+
maxConcurrent: number;
|
|
98
|
+
/** When at capacity, keep helper requests queued instead of rejecting them. */
|
|
99
|
+
queueOverflow: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ExtensionsConfig {
|
|
103
|
+
/** `null` inherits every effective Pi extension; an array is an explicit selection. */
|
|
104
|
+
allow: string[] | null;
|
|
105
|
+
/** Extensions explicitly kept out of spawned agents. */
|
|
106
|
+
block: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DebugConfig {
|
|
110
|
+
/** Write spawn decisions and terminal launch results to the team debug log. */
|
|
111
|
+
enabled: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface AgentSessionsConfig {
|
|
115
|
+
/** Expose spawned-agent transcripts in Pi's normal /resume session store. */
|
|
116
|
+
showInResume: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface PiExtendedTeamsSettings {
|
|
120
|
+
watchdog: WatchdogConfig;
|
|
121
|
+
writeAgents: WriteAgentsConfig;
|
|
122
|
+
readAgents: ReadAgentsConfig;
|
|
123
|
+
readHelpers: ReadHelpersConfig;
|
|
124
|
+
roles: Record<AgentRole, RoleModelConfig>;
|
|
125
|
+
favoriteModels: Partial<Record<FavoriteModelSlot, FavoriteModelConfig>>;
|
|
126
|
+
categories: Record<string, CategoryConfig>;
|
|
127
|
+
extensions: ExtensionsConfig;
|
|
128
|
+
agentSessions: AgentSessionsConfig;
|
|
129
|
+
debug: DebugConfig;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const DEFAULT_WRITE_AGENT_MAX_CONCURRENT = 100;
|
|
133
|
+
export const DEFAULT_READ_AGENT_MAX_CONCURRENT = 25;
|
|
134
|
+
export const DEFAULT_READ_HELPER_MAX_CONCURRENT = 10;
|
|
135
|
+
|
|
136
|
+
export const DEFAULT_SETTINGS: PiExtendedTeamsSettings = {
|
|
137
|
+
watchdog: { bufferSeconds: 30 },
|
|
138
|
+
writeAgents: { maxConcurrent: DEFAULT_WRITE_AGENT_MAX_CONCURRENT, queueOverflow: true },
|
|
139
|
+
readAgents: { maxConcurrent: DEFAULT_READ_AGENT_MAX_CONCURRENT, queueOverflow: true },
|
|
140
|
+
readHelpers: { maxConcurrent: DEFAULT_READ_HELPER_MAX_CONCURRENT, queueOverflow: true },
|
|
141
|
+
roles: {
|
|
142
|
+
read: { model: null, thinking: null },
|
|
143
|
+
write: { model: null, thinking: null },
|
|
144
|
+
},
|
|
145
|
+
favoriteModels: {},
|
|
146
|
+
categories: {},
|
|
147
|
+
extensions: { allow: null, block: [] },
|
|
148
|
+
agentSessions: { showInResume: false },
|
|
149
|
+
debug: { enabled: false },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export function globalSettingsPath(homeDir: string = os.homedir()): string {
|
|
153
|
+
return path.join(homeDir, ".pi", "agent", "pi-extended-teams", "settings.json");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function projectSettingsPath(projectDir: string): string {
|
|
157
|
+
return path.join(projectDir, ".pi", "pi-extended-teams.json");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface ProjectExtensionAllowOverride {
|
|
161
|
+
filePath: string;
|
|
162
|
+
allow: string[] | null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Read only the project-local `extensions.allow` layer, without merging it with
|
|
167
|
+
* global/default settings. Callers must establish project trust before using it.
|
|
168
|
+
*/
|
|
169
|
+
export function readProjectExtensionAllowOverride(projectDir: string): ProjectExtensionAllowOverride | null {
|
|
170
|
+
const filePath = projectSettingsPath(projectDir);
|
|
171
|
+
const raw = readJson(filePath);
|
|
172
|
+
const extensions = raw?.extensions;
|
|
173
|
+
if (!extensions || typeof extensions !== "object" || Array.isArray(extensions)) return null;
|
|
174
|
+
if (extensions.allow === null) return { filePath, allow: null };
|
|
175
|
+
const allow = toStringList(extensions.allow);
|
|
176
|
+
return allow === undefined ? null : { filePath, allow };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readJson(filePath: string): any | null {
|
|
180
|
+
if (!fs.existsSync(filePath)) return null;
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toStringOrNull(value: unknown): string | null {
|
|
189
|
+
if (typeof value !== "string") return null;
|
|
190
|
+
const trimmed = value.trim();
|
|
191
|
+
return trimmed ? trimmed : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function toStringList(value: unknown): string[] | undefined {
|
|
195
|
+
if (!Array.isArray(value)) return undefined;
|
|
196
|
+
return Array.from(
|
|
197
|
+
new Set(value.map((v) => String(v).trim()).filter(Boolean))
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeRole(value: unknown): AgentRole | undefined {
|
|
202
|
+
return value === "read" || value === "write" ? value : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function isCanonicalFavoriteModelSlot(value: unknown): value is CanonicalFavoriteModelSlot {
|
|
206
|
+
return typeof value === "string" && CANONICAL_FAVORITE_MODEL_SLOT_SET.has(value);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function isFavoriteModelSlot(value: unknown): value is FavoriteModelSlot {
|
|
210
|
+
return typeof value === "string" && FAVORITE_MODEL_SLOT_SET.has(value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function normalizeFavoriteModelSlot(value: unknown): CanonicalFavoriteModelSlot | null {
|
|
214
|
+
if (isCanonicalFavoriteModelSlot(value)) return value;
|
|
215
|
+
if (typeof value !== "string") return null;
|
|
216
|
+
return LEGACY_FAVORITE_MODEL_SLOT_ALIASES[value as LegacyFavoriteModelSlot] ?? null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Project a value read from persisted state to its canonical slot without
|
|
221
|
+
* mutating storage or changing the contract for absent/unknown values.
|
|
222
|
+
*/
|
|
223
|
+
export function canonicalPersistedModelSlot<T>(value: T): T | CanonicalFavoriteModelSlot {
|
|
224
|
+
return normalizeFavoriteModelSlot(value) ?? value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function roleForFavoriteModelSlot(slot: FavoriteModelSlot): AgentRole {
|
|
228
|
+
const canonical = normalizeFavoriteModelSlot(slot);
|
|
229
|
+
if (!canonical) throw new Error(`Unknown favorite model slot "${String(slot)}".`);
|
|
230
|
+
return canonical.startsWith("write-") ? "write" : "read";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface ResolvedFavoriteModelLevel {
|
|
234
|
+
slot: CanonicalFavoriteModelSlot;
|
|
235
|
+
role: AgentRole;
|
|
236
|
+
model: string;
|
|
237
|
+
thinking: ThinkingLevelName;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function normalizeThinking(value: unknown): string | null {
|
|
241
|
+
const s = toStringOrNull(value);
|
|
242
|
+
return s && THINKING_LEVELS.has(s) ? s : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function mergeRoleConfig(base: RoleModelConfig, raw: any): RoleModelConfig {
|
|
246
|
+
if (!raw || typeof raw !== "object") return base;
|
|
247
|
+
return {
|
|
248
|
+
model: "model" in raw ? toStringOrNull(raw.model) : base.model,
|
|
249
|
+
thinking: "thinking" in raw ? normalizeThinking(raw.thinking) : base.thinking,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function favoriteModelConfigFromRaw(
|
|
254
|
+
raw: any,
|
|
255
|
+
base?: FavoriteModelConfig
|
|
256
|
+
): FavoriteModelConfig | null {
|
|
257
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
258
|
+
return {
|
|
259
|
+
// Keep configured and spawn-time model identities aligned.
|
|
260
|
+
model: "model" in raw ? normalizeQualifiedModel(String(raw.model ?? "")) : (base?.model ?? null),
|
|
261
|
+
thinking: "thinking" in raw ? normalizeThinking(raw.thinking) : (base?.thinking ?? null),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Apply one raw settings object on top of an accumulator, in place.
|
|
267
|
+
*/
|
|
268
|
+
function applyLayer(acc: PiExtendedTeamsSettings, raw: any, options: { favoriteModels?: boolean } = {}): void {
|
|
269
|
+
if (!raw || typeof raw !== "object") return;
|
|
270
|
+
const includeFavoriteModels = options.favoriteModels !== false;
|
|
271
|
+
|
|
272
|
+
if (raw.watchdog && typeof raw.watchdog === "object") {
|
|
273
|
+
const b = Number(raw.watchdog.bufferSeconds);
|
|
274
|
+
if (Number.isFinite(b) && b >= 0) acc.watchdog.bufferSeconds = b;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (raw.writeAgents && typeof raw.writeAgents === "object") {
|
|
278
|
+
const m = Number(raw.writeAgents.maxConcurrent);
|
|
279
|
+
if (Number.isFinite(m) && m >= 1) acc.writeAgents.maxConcurrent = Math.floor(m);
|
|
280
|
+
if (typeof raw.writeAgents.queueOverflow === "boolean") {
|
|
281
|
+
acc.writeAgents.queueOverflow = raw.writeAgents.queueOverflow;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (raw.readAgents && typeof raw.readAgents === "object") {
|
|
286
|
+
const m = Number(raw.readAgents.maxConcurrent);
|
|
287
|
+
if (Number.isFinite(m) && m >= 1) acc.readAgents.maxConcurrent = Math.floor(m);
|
|
288
|
+
if (typeof raw.readAgents.queueOverflow === "boolean") {
|
|
289
|
+
acc.readAgents.queueOverflow = raw.readAgents.queueOverflow;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (raw.readHelpers && typeof raw.readHelpers === "object") {
|
|
294
|
+
const m = Number(raw.readHelpers.maxConcurrent);
|
|
295
|
+
if (Number.isFinite(m) && m >= 1) acc.readHelpers.maxConcurrent = Math.floor(m);
|
|
296
|
+
if (typeof raw.readHelpers.queueOverflow === "boolean") {
|
|
297
|
+
acc.readHelpers.queueOverflow = raw.readHelpers.queueOverflow;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (raw.roles && typeof raw.roles === "object") {
|
|
302
|
+
acc.roles.read = mergeRoleConfig(acc.roles.read, raw.roles.read);
|
|
303
|
+
acc.roles.write = mergeRoleConfig(acc.roles.write, raw.roles.write);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (includeFavoriteModels && raw.favoriteModels && typeof raw.favoriteModels === "object") {
|
|
307
|
+
// Read aliases first, then canonical tiers, so canonical keys win regardless
|
|
308
|
+
// of JSON property order when both forms are present.
|
|
309
|
+
for (const legacySlot of LEGACY_FAVORITE_MODEL_SLOTS) {
|
|
310
|
+
const config = favoriteModelConfigFromRaw(raw.favoriteModels[legacySlot]);
|
|
311
|
+
if (config) acc.favoriteModels[LEGACY_FAVORITE_MODEL_SLOT_ALIASES[legacySlot]] = config;
|
|
312
|
+
}
|
|
313
|
+
for (const canonicalSlot of FAVORITE_MODEL_SLOTS) {
|
|
314
|
+
const config = favoriteModelConfigFromRaw(
|
|
315
|
+
raw.favoriteModels[canonicalSlot],
|
|
316
|
+
acc.favoriteModels[canonicalSlot]
|
|
317
|
+
);
|
|
318
|
+
if (config) acc.favoriteModels[canonicalSlot] = config;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (raw.categories && typeof raw.categories === "object") {
|
|
323
|
+
for (const [name, value] of Object.entries<any>(raw.categories)) {
|
|
324
|
+
if (!value || typeof value !== "object") continue;
|
|
325
|
+
acc.categories[name] = {
|
|
326
|
+
role: normalizeRole(value.role),
|
|
327
|
+
model: toStringOrNull(value.model),
|
|
328
|
+
thinking: normalizeThinking(value.thinking),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (raw.extensions && typeof raw.extensions === "object") {
|
|
334
|
+
if (raw.extensions.allow === null) {
|
|
335
|
+
acc.extensions.allow = null;
|
|
336
|
+
} else {
|
|
337
|
+
const allow = toStringList(raw.extensions.allow);
|
|
338
|
+
if (allow) acc.extensions.allow = allow;
|
|
339
|
+
}
|
|
340
|
+
const block = toStringList(raw.extensions.block);
|
|
341
|
+
if (block) acc.extensions.block = block;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (raw.agentSessions && typeof raw.agentSessions === "object"
|
|
345
|
+
&& typeof raw.agentSessions.showInResume === "boolean") {
|
|
346
|
+
acc.agentSessions.showInResume = raw.agentSessions.showInResume;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (typeof raw.debug === "boolean") {
|
|
350
|
+
acc.debug.enabled = raw.debug;
|
|
351
|
+
} else if (raw.debug && typeof raw.debug === "object" && typeof raw.debug.enabled === "boolean") {
|
|
352
|
+
acc.debug.enabled = raw.debug.enabled;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Load merged settings: defaults < global < project.
|
|
358
|
+
*/
|
|
359
|
+
export function loadSettings(options?: {
|
|
360
|
+
projectDir?: string;
|
|
361
|
+
homeDir?: string;
|
|
362
|
+
}): PiExtendedTeamsSettings {
|
|
363
|
+
const homeDir = options?.homeDir ?? os.homedir();
|
|
364
|
+
const acc: PiExtendedTeamsSettings = structuredClone(DEFAULT_SETTINGS);
|
|
365
|
+
|
|
366
|
+
applyLayer(acc, readJson(globalSettingsPath(homeDir)));
|
|
367
|
+
if (options?.projectDir) {
|
|
368
|
+
// Favorite model slots are intentionally global-only: /agents-favorite-models
|
|
369
|
+
// writes ~/.pi/agent/pi-extended-teams/settings.json, so spawn resolution must
|
|
370
|
+
// not be shadowed by project-local slot values the picker cannot update.
|
|
371
|
+
applyLayer(acc, readJson(projectSettingsPath(options.projectDir)), { favoriteModels: false });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return acc;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function readRawSettingsObject(filePath: string): Record<string, any> {
|
|
378
|
+
const raw = readJson(filePath);
|
|
379
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function writeRawSettingsObject(filePath: string, raw: Record<string, any>): void {
|
|
383
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
384
|
+
fs.writeFileSync(filePath, `${JSON.stringify(raw, null, 2)}\n`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function mergeRawFavoriteModelValues(base: any, override: any): any {
|
|
388
|
+
const baseIsObject = base && typeof base === "object" && !Array.isArray(base);
|
|
389
|
+
const overrideIsObject = override && typeof override === "object" && !Array.isArray(override);
|
|
390
|
+
return baseIsObject && overrideIsObject ? { ...base, ...override } : override;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function canonicalizeFavoriteModelKeys(
|
|
394
|
+
favoriteModels: Record<string, any>,
|
|
395
|
+
options: { preserveUnknown: boolean }
|
|
396
|
+
): Record<string, any> {
|
|
397
|
+
const next: Record<string, any> = {};
|
|
398
|
+
if (options.preserveUnknown) {
|
|
399
|
+
for (const [slot, config] of Object.entries(favoriteModels)) {
|
|
400
|
+
if (!isFavoriteModelSlot(slot)) next[slot] = config;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const legacySlot of LEGACY_FAVORITE_MODEL_SLOTS) {
|
|
404
|
+
if (Object.prototype.hasOwnProperty.call(favoriteModels, legacySlot)) {
|
|
405
|
+
next[LEGACY_FAVORITE_MODEL_SLOT_ALIASES[legacySlot]] = favoriteModels[legacySlot];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const canonicalSlot of FAVORITE_MODEL_SLOTS) {
|
|
409
|
+
if (Object.prototype.hasOwnProperty.call(favoriteModels, canonicalSlot)) {
|
|
410
|
+
next[canonicalSlot] = mergeRawFavoriteModelValues(next[canonicalSlot], favoriteModels[canonicalSlot]);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return next;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function setGlobalFavoriteModel(
|
|
417
|
+
slot: FavoriteModelSlot,
|
|
418
|
+
config: FavoriteModelConfig,
|
|
419
|
+
options?: { homeDir?: string }
|
|
420
|
+
): void {
|
|
421
|
+
const canonicalSlot = normalizeFavoriteModelSlot(slot);
|
|
422
|
+
if (!canonicalSlot) throw new Error(`Unknown favorite model slot "${slot}".`);
|
|
423
|
+
const model = toStringOrNull(config.model);
|
|
424
|
+
const thinking = normalizeThinking(config.thinking);
|
|
425
|
+
if (!model) throw new Error(`Favorite model slot "${canonicalSlot}" requires a fully qualified provider/model.`);
|
|
426
|
+
if (!thinking) throw new Error(`Favorite model slot "${canonicalSlot}" requires a valid thinking level.`);
|
|
427
|
+
|
|
428
|
+
const filePath = globalSettingsPath(options?.homeDir ?? os.homedir());
|
|
429
|
+
const raw = readRawSettingsObject(filePath);
|
|
430
|
+
const existing = raw.favoriteModels && typeof raw.favoriteModels === "object" && !Array.isArray(raw.favoriteModels)
|
|
431
|
+
? raw.favoriteModels
|
|
432
|
+
: {};
|
|
433
|
+
const favoriteModels = canonicalizeFavoriteModelKeys(existing, { preserveUnknown: true });
|
|
434
|
+
favoriteModels[canonicalSlot] = { model, thinking };
|
|
435
|
+
raw.favoriteModels = favoriteModels;
|
|
436
|
+
writeRawSettingsObject(filePath, raw);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function clearGlobalFavoriteModels(options?: {
|
|
440
|
+
homeDir?: string;
|
|
441
|
+
slot?: FavoriteModelSlot;
|
|
442
|
+
}): void {
|
|
443
|
+
const canonicalSlot = options?.slot ? normalizeFavoriteModelSlot(options.slot) : null;
|
|
444
|
+
if (options?.slot && !canonicalSlot) {
|
|
445
|
+
throw new Error(`Unknown favorite model slot "${options.slot}".`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const filePath = globalSettingsPath(options?.homeDir ?? os.homedir());
|
|
449
|
+
const raw = readRawSettingsObject(filePath);
|
|
450
|
+
const existing = raw.favoriteModels && typeof raw.favoriteModels === "object" && !Array.isArray(raw.favoriteModels)
|
|
451
|
+
? raw.favoriteModels
|
|
452
|
+
: {};
|
|
453
|
+
const favoriteModels = canonicalizeFavoriteModelKeys(existing, { preserveUnknown: true });
|
|
454
|
+
|
|
455
|
+
if (canonicalSlot) delete favoriteModels[canonicalSlot];
|
|
456
|
+
else for (const slot of FAVORITE_MODEL_SLOTS) delete favoriteModels[slot];
|
|
457
|
+
|
|
458
|
+
raw.favoriteModels = favoriteModels;
|
|
459
|
+
writeRawSettingsObject(filePath, raw);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function replaceGlobalFavoriteModels(
|
|
463
|
+
favoriteModels: Partial<Record<FavoriteModelSlot, FavoriteModelConfig>>,
|
|
464
|
+
options?: { homeDir?: string }
|
|
465
|
+
): void {
|
|
466
|
+
const canonicalInput = canonicalizeFavoriteModelKeys(favoriteModels, { preserveUnknown: false });
|
|
467
|
+
const next: Partial<Record<CanonicalFavoriteModelSlot, FavoriteModelConfig>> = {};
|
|
468
|
+
for (const slot of FAVORITE_MODEL_SLOTS) {
|
|
469
|
+
const config = canonicalInput[slot];
|
|
470
|
+
if (!config) continue;
|
|
471
|
+
const model = toStringOrNull(config.model);
|
|
472
|
+
const thinking = normalizeThinking(config.thinking);
|
|
473
|
+
if (!model || !thinking) continue;
|
|
474
|
+
next[slot] = { model, thinking };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const filePath = globalSettingsPath(options?.homeDir ?? os.homedir());
|
|
478
|
+
const raw = readRawSettingsObject(filePath);
|
|
479
|
+
raw.favoriteModels = next;
|
|
480
|
+
writeRawSettingsObject(filePath, raw);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Persist the spawned-agent extension policy without replacing unrelated settings. */
|
|
484
|
+
export function replaceGlobalExtensionAllow(
|
|
485
|
+
allow: string[] | null,
|
|
486
|
+
options?: { homeDir?: string }
|
|
487
|
+
): void {
|
|
488
|
+
const filePath = globalSettingsPath(options?.homeDir ?? os.homedir());
|
|
489
|
+
const raw = readRawSettingsObject(filePath);
|
|
490
|
+
const extensions = raw.extensions && typeof raw.extensions === "object" && !Array.isArray(raw.extensions)
|
|
491
|
+
? { ...raw.extensions }
|
|
492
|
+
: {};
|
|
493
|
+
extensions.allow = allow === null ? null : (toStringList(allow) ?? []);
|
|
494
|
+
raw.extensions = extensions;
|
|
495
|
+
writeRawSettingsObject(filePath, raw);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function requireConfiguredFavoriteModel(
|
|
499
|
+
settings: PiExtendedTeamsSettings,
|
|
500
|
+
slot: unknown
|
|
501
|
+
): { slot: CanonicalFavoriteModelSlot; config: FavoriteModelConfig } | null {
|
|
502
|
+
if (slot === undefined || slot === null || slot === "") return null;
|
|
503
|
+
const canonicalSlot = normalizeFavoriteModelSlot(slot);
|
|
504
|
+
if (!canonicalSlot) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Unknown favorite model slot "${String(slot)}". Use one of: ${FAVORITE_MODEL_SLOTS.join(", ")}.`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const config = settings.favoriteModels[canonicalSlot]
|
|
511
|
+
?? (typeof slot === "string" && isFavoriteModelSlot(slot) ? settings.favoriteModels[slot] : undefined);
|
|
512
|
+
if (!config?.model || !config.thinking) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`Favorite model slot "${canonicalSlot}" is not configured. Run /agents-favorite-models set ${canonicalSlot} <provider/model> <thinking>.`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return { slot: canonicalSlot, config };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function requireFavoriteModelLevel(
|
|
521
|
+
settings: PiExtendedTeamsSettings,
|
|
522
|
+
slot: unknown
|
|
523
|
+
): ResolvedFavoriteModelLevel {
|
|
524
|
+
const favorite = requireConfiguredFavoriteModel(settings, slot);
|
|
525
|
+
if (!favorite?.config.model || !favorite.config.thinking) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`Favorite model slot "${String(slot ?? "")}" is not configured. Run /agents-favorite-models to define levels before spawning agents.`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
slot: favorite.slot,
|
|
532
|
+
role: roleForFavoriteModelSlot(favorite.slot),
|
|
533
|
+
model: favorite.config.model,
|
|
534
|
+
thinking: favorite.config.thinking as ThinkingLevelName,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export interface ResolveModelInput {
|
|
539
|
+
role: AgentRole;
|
|
540
|
+
/** Optional category name referencing settings.categories. */
|
|
541
|
+
category?: string;
|
|
542
|
+
/** Favorite model slot selected at spawn time. */
|
|
543
|
+
modelSlot?: string | null;
|
|
544
|
+
/** Explicit fully-qualified model passed at spawn time. */
|
|
545
|
+
explicitModel?: string | null;
|
|
546
|
+
/** Explicit thinking level passed at spawn time. */
|
|
547
|
+
explicitThinking?: string | null;
|
|
548
|
+
/** Team default model. */
|
|
549
|
+
teamDefaultModel?: string | null;
|
|
550
|
+
/** Current session model, used as the final fallback. */
|
|
551
|
+
currentModel?: string | null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export interface ResolvedModel {
|
|
555
|
+
model: string | null;
|
|
556
|
+
thinking: string | null;
|
|
557
|
+
/** Where the model came from, for diagnostics. */
|
|
558
|
+
modelSource: "explicit" | "favorite-slot" | "category" | "role" | "team" | "current" | "none";
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Resolve a member's effective model/thinking.
|
|
563
|
+
*
|
|
564
|
+
* Model precedence: explicit -> favorite slot -> category -> role default -> team default -> current
|
|
565
|
+
* Thinking precedence: explicit -> favorite slot -> category -> role default (else none)
|
|
566
|
+
*
|
|
567
|
+
* Today read and write typically resolve to the same model (role defaults are
|
|
568
|
+
* null = inherit). The precedence chain lets that diverge purely via settings,
|
|
569
|
+
* without code changes.
|
|
570
|
+
*/
|
|
571
|
+
export function resolveModel(
|
|
572
|
+
settings: PiExtendedTeamsSettings,
|
|
573
|
+
input: ResolveModelInput
|
|
574
|
+
): ResolvedModel {
|
|
575
|
+
const category = input.category ? settings.categories[input.category] : undefined;
|
|
576
|
+
if (input.category && !category) {
|
|
577
|
+
throw new Error(
|
|
578
|
+
`Unknown category "${input.category}". Define it under categories in settings.json or omit it.`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const roleDefaults = settings.roles[input.role];
|
|
583
|
+
|
|
584
|
+
const explicitModel = toStringOrNull(input.explicitModel ?? null);
|
|
585
|
+
const explicitThinkingInput = toStringOrNull(input.explicitThinking ?? null);
|
|
586
|
+
const favorite = requireConfiguredFavoriteModel(settings, input.modelSlot);
|
|
587
|
+
if (favorite && (explicitModel || explicitThinkingInput)) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`model_slot cannot be combined with explicit model or thinking. Slot "${favorite.slot}" already defines both.`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const teamDefault = toStringOrNull(input.teamDefaultModel ?? null);
|
|
594
|
+
const current = toStringOrNull(input.currentModel ?? null);
|
|
595
|
+
|
|
596
|
+
let model: string | null;
|
|
597
|
+
let modelSource: ResolvedModel["modelSource"];
|
|
598
|
+
if (explicitModel) {
|
|
599
|
+
model = explicitModel;
|
|
600
|
+
modelSource = "explicit";
|
|
601
|
+
} else if (favorite?.config.model) {
|
|
602
|
+
model = favorite.config.model;
|
|
603
|
+
modelSource = "favorite-slot";
|
|
604
|
+
} else if (category?.model) {
|
|
605
|
+
model = category.model;
|
|
606
|
+
modelSource = "category";
|
|
607
|
+
} else if (roleDefaults.model) {
|
|
608
|
+
model = roleDefaults.model;
|
|
609
|
+
modelSource = "role";
|
|
610
|
+
} else if (teamDefault) {
|
|
611
|
+
model = teamDefault;
|
|
612
|
+
modelSource = "team";
|
|
613
|
+
} else if (current) {
|
|
614
|
+
model = current;
|
|
615
|
+
modelSource = "current";
|
|
616
|
+
} else {
|
|
617
|
+
model = null;
|
|
618
|
+
modelSource = "none";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const explicitThinking = normalizeThinking(input.explicitThinking);
|
|
622
|
+
const thinking =
|
|
623
|
+
explicitThinking ??
|
|
624
|
+
favorite?.config.thinking ??
|
|
625
|
+
category?.thinking ??
|
|
626
|
+
roleDefaults.thinking ??
|
|
627
|
+
null;
|
|
628
|
+
|
|
629
|
+
return { model, thinking, modelSource };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Effective role for a category: category.role wins, else the requested role.
|
|
634
|
+
*/
|
|
635
|
+
export function resolveRole(
|
|
636
|
+
settings: PiExtendedTeamsSettings,
|
|
637
|
+
requestedRole: AgentRole,
|
|
638
|
+
category?: string
|
|
639
|
+
): AgentRole {
|
|
640
|
+
if (category) {
|
|
641
|
+
const cat = settings.categories[category];
|
|
642
|
+
if (cat?.role) return cat.role;
|
|
643
|
+
}
|
|
644
|
+
return requestedRole;
|
|
645
|
+
}
|