pi-subagents-lite 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -6
- package/package.json +1 -1
- package/src/agent-manager.ts +81 -62
- package/src/agent-runner.ts +194 -167
- package/src/agent-types.ts +1 -1
- package/src/config-io.ts +9 -1
- package/src/config-mutator.ts +183 -0
- package/src/context.ts +1 -1
- package/src/default-agents.ts +1 -1
- package/src/format.ts +173 -0
- package/src/index.ts +124 -173
- package/src/menus.ts +188 -136
- package/src/model-precedence.ts +5 -0
- package/src/output-file.ts +1 -68
- package/src/renderer.ts +157 -0
- package/src/result-viewer.ts +2 -1
- package/src/state.ts +80 -0
- package/src/tool-execution.ts +152 -65
- package/src/types.ts +100 -31
- package/src/ui/agent-widget.ts +148 -145
- package/src/usage.ts +5 -0
- package/src/stop-agent-tool.ts +0 -77
package/src/agent-runner.ts
CHANGED
|
@@ -369,219 +369,207 @@ async function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo> {
|
|
|
369
369
|
};
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
|
|
373
|
-
ctx: ExtensionContext,
|
|
374
|
-
type: SubagentType,
|
|
375
|
-
prompt: string,
|
|
376
|
-
options: RunOptions,
|
|
377
|
-
): Promise<RunResult> {
|
|
378
|
-
const config = getConfig(type);
|
|
379
|
-
const agentConfig = getAgentConfig(type);
|
|
380
|
-
|
|
381
|
-
// Warn on mutual exclusion violations
|
|
382
|
-
const notify = (msg: string) => {
|
|
383
|
-
if (ctx.ui?.notify) {
|
|
384
|
-
ctx.ui.notify(`[pi-subagents] ${msg}`, "warning");
|
|
385
|
-
} else {
|
|
386
|
-
console.warn(`[pi-subagents] ${msg}`);
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
if (agentConfig?.excludeTools && Array.isArray(agentConfig.tools)) {
|
|
390
|
-
notify(`agent "${type}": both tools and exclude_tools set — tools (whitelist) wins`);
|
|
391
|
-
}
|
|
392
|
-
if (agentConfig?.excludeExtensions && Array.isArray(agentConfig.extensions)) {
|
|
393
|
-
notify(`agent "${type}": both extensions and exclude_extensions set — extensions (whitelist) wins`);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Resolve working directory
|
|
397
|
-
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
398
|
-
|
|
399
|
-
const env = await detectEnv(options.pi, effectiveCwd);
|
|
400
|
-
|
|
401
|
-
// Resolve extensions/skills from agent config (frontmatter)
|
|
402
|
-
const extensions = config.extensions;
|
|
403
|
-
const skills = config.skills;
|
|
404
|
-
const preloadSkillsList = agentConfig?.preloadSkills;
|
|
372
|
+
// ── runAgent phases ────────────────────────────────────────────────
|
|
405
373
|
|
|
406
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Phase 1: Resolve system prompt from agent config, skills, and env info.
|
|
376
|
+
*/
|
|
377
|
+
function buildPrompt(
|
|
378
|
+
type: SubagentType,
|
|
379
|
+
agentConfig: ReturnType<typeof getAgentConfig>,
|
|
380
|
+
config: ReturnType<typeof getConfig>,
|
|
381
|
+
cwd: string,
|
|
382
|
+
env: EnvInfo,
|
|
383
|
+
): string {
|
|
407
384
|
const extras: PromptExtras = {};
|
|
408
|
-
if (Array.isArray(
|
|
409
|
-
extras.skillBlocks = preloadSkills(
|
|
385
|
+
if (Array.isArray(agentConfig?.preloadSkills)) {
|
|
386
|
+
extras.skillBlocks = preloadSkills(agentConfig.preloadSkills, cwd);
|
|
410
387
|
}
|
|
411
|
-
if (Array.isArray(skills)) {
|
|
412
|
-
extras.skillMetas = loadSkillMeta(skills,
|
|
388
|
+
if (Array.isArray(config.skills)) {
|
|
389
|
+
extras.skillMetas = loadSkillMeta(config.skills, cwd);
|
|
413
390
|
}
|
|
414
|
-
|
|
415
|
-
const toolNames = getToolNamesForType(type);
|
|
416
|
-
|
|
417
|
-
// Build system prompt from agent config
|
|
418
|
-
let systemPrompt: string;
|
|
419
391
|
if (agentConfig) {
|
|
420
|
-
|
|
421
|
-
} else {
|
|
422
|
-
// Unknown type fallback: spread the canonical general-purpose config
|
|
423
|
-
const fallback = DEFAULT_AGENTS.get("general-purpose");
|
|
424
|
-
if (!fallback) throw new Error(`No fallback config available for unknown type "${type}"`);
|
|
425
|
-
systemPrompt = buildAgentPrompt({ ...fallback, name: type }, effectiveCwd, env, extras);
|
|
392
|
+
return buildAgentPrompt(agentConfig, cwd, env, extras);
|
|
426
393
|
}
|
|
394
|
+
const fallback = DEFAULT_AGENTS.get("general-purpose");
|
|
395
|
+
if (!fallback) throw new Error(`No fallback config available for unknown type "${type}"`);
|
|
396
|
+
return buildAgentPrompt({ ...fallback, name: type }, cwd, env, extras);
|
|
397
|
+
}
|
|
427
398
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
399
|
+
/** Build extension name → tool names map from loaded extensions. */
|
|
400
|
+
function buildExtToolMap(extensions: Array<{ path: string; tools: Map<string, unknown> }>) {
|
|
401
|
+
const map = new Map<string, string[]>();
|
|
402
|
+
for (const ext of extensions) {
|
|
403
|
+
const name = extractExtensionName(ext.path);
|
|
404
|
+
const tools = [...ext.tools.keys()];
|
|
405
|
+
if (tools.length > 0) map.set(name, tools);
|
|
406
|
+
}
|
|
407
|
+
return map;
|
|
408
|
+
}
|
|
435
409
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
cwd: effectiveCwd,
|
|
442
|
-
agentDir,
|
|
443
|
-
noExtensions: extensions === false,
|
|
444
|
-
noSkills: skipSkillLoader,
|
|
445
|
-
noPromptTemplates: true,
|
|
446
|
-
noThemes: true,
|
|
447
|
-
noContextFiles: true,
|
|
448
|
-
systemPromptOverride: () => systemPrompt,
|
|
449
|
-
appendSystemPromptOverride: () => [],
|
|
450
|
-
};
|
|
451
|
-
const excludeExtSet = agentConfig?.excludeExtensions
|
|
452
|
-
? new Set(agentConfig.excludeExtensions)
|
|
453
|
-
: undefined;
|
|
410
|
+
/** Build extension override for whitelist or blacklist filtering. */
|
|
411
|
+
function buildExtOverride(
|
|
412
|
+
extensions: true | string[] | false | undefined,
|
|
413
|
+
excludeExtensions?: string[],
|
|
414
|
+
) {
|
|
454
415
|
if (Array.isArray(extensions)) {
|
|
455
|
-
// Whitelist mode: only load listed extensions
|
|
456
416
|
const allowedNames = new Set(extensions.map(ext => {
|
|
457
417
|
const slashIdx = ext.indexOf("/");
|
|
458
418
|
return slashIdx !== -1 ? ext.slice(0, slashIdx) : ext;
|
|
459
419
|
}));
|
|
460
|
-
|
|
420
|
+
return (result: any) => ({
|
|
461
421
|
...result,
|
|
462
|
-
extensions: result.extensions.filter(ext =>
|
|
422
|
+
extensions: result.extensions.filter((ext: { path: string }) =>
|
|
463
423
|
allowedNames.has(extractExtensionName(ext.path)),
|
|
464
424
|
),
|
|
465
425
|
});
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
426
|
+
}
|
|
427
|
+
if (excludeExtensions) {
|
|
428
|
+
const excludeSet = new Set(excludeExtensions);
|
|
429
|
+
return (result: any) => ({
|
|
469
430
|
...result,
|
|
470
|
-
extensions: result.extensions.filter(ext =>
|
|
471
|
-
!
|
|
431
|
+
extensions: result.extensions.filter((ext: { path: string }) =>
|
|
432
|
+
!excludeSet.has(extractExtensionName(ext.path)),
|
|
472
433
|
),
|
|
473
434
|
});
|
|
474
435
|
}
|
|
475
|
-
|
|
476
|
-
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
477
438
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
439
|
+
/**
|
|
440
|
+
* Phase 2: Build DefaultResourceLoader with extension filtering.
|
|
441
|
+
* Returns the loader and a function that reloads it and builds the ext→tool map.
|
|
442
|
+
*/
|
|
443
|
+
function createResourceLoader(
|
|
444
|
+
config: ReturnType<typeof getConfig>,
|
|
445
|
+
agentConfig: ReturnType<typeof getAgentConfig>,
|
|
446
|
+
cwd: string,
|
|
447
|
+
systemPrompt: string,
|
|
448
|
+
) {
|
|
449
|
+
const extensions = config.extensions;
|
|
450
|
+
const noSkills = config.skills === false
|
|
451
|
+
|| Array.isArray(config.skills)
|
|
452
|
+
|| Array.isArray(agentConfig?.preloadSkills);
|
|
453
|
+
const agentDir = getAgentDir();
|
|
454
|
+
const loaderOpts: ConstructorParameters<typeof DefaultResourceLoader>[0] = {
|
|
455
|
+
cwd, agentDir,
|
|
456
|
+
noExtensions: extensions === false, noSkills,
|
|
457
|
+
noPromptTemplates: true, noThemes: true, noContextFiles: true,
|
|
458
|
+
systemPromptOverride: () => systemPrompt,
|
|
459
|
+
appendSystemPromptOverride: () => [],
|
|
460
|
+
extensionsOverride: buildExtOverride(extensions, agentConfig?.excludeExtensions),
|
|
461
|
+
};
|
|
462
|
+
const loader = new DefaultResourceLoader(loaderOpts);
|
|
463
|
+
return {
|
|
464
|
+
loader,
|
|
465
|
+
reloadAndMap: async () => {
|
|
466
|
+
await loader.reload();
|
|
467
|
+
const extResult = loader.getExtensions();
|
|
468
|
+
return { extResult, extToolMap: buildExtToolMap(extResult.extensions) };
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
487
472
|
|
|
488
|
-
|
|
473
|
+
/** Create an agent session with the resolved model and thinking level. */
|
|
474
|
+
async function initSession(
|
|
475
|
+
ctx: ExtensionContext,
|
|
476
|
+
options: RunOptions,
|
|
477
|
+
agentConfig: ReturnType<typeof getAgentConfig>,
|
|
478
|
+
type: SubagentType,
|
|
479
|
+
cwd: string,
|
|
480
|
+
loader: DefaultResourceLoader,
|
|
481
|
+
) {
|
|
489
482
|
const model = options.model ?? findModelInRegistry(
|
|
490
483
|
agentConfig?.model, ctx.modelRegistry, ctx.model,
|
|
491
484
|
);
|
|
492
|
-
|
|
493
|
-
// Resolve thinking level: explicit option > agent config > undefined (inherit)
|
|
494
485
|
const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinking;
|
|
495
|
-
|
|
486
|
+
const agentDir = getAgentDir();
|
|
496
487
|
const sessionOpts: Parameters<typeof createAgentSession>[0] = {
|
|
497
|
-
cwd
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
model,
|
|
503
|
-
tools: toolNames,
|
|
504
|
-
resourceLoader: loader,
|
|
488
|
+
cwd, agentDir,
|
|
489
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
490
|
+
settingsManager: SettingsManager.create(cwd, agentDir),
|
|
491
|
+
modelRegistry: ctx.modelRegistry, model,
|
|
492
|
+
tools: getToolNamesForType(type), resourceLoader: loader,
|
|
505
493
|
};
|
|
506
|
-
if (thinkingLevel)
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const { session } = await createAgentSession(sessionOpts);
|
|
494
|
+
if (thinkingLevel) sessionOpts.thinkingLevel = thinkingLevel;
|
|
495
|
+
return createAgentSession(sessionOpts);
|
|
496
|
+
}
|
|
511
497
|
|
|
512
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Phase 3: Create session, bind extensions, filter tools.
|
|
500
|
+
*/
|
|
501
|
+
async function createAndConfigureSession(
|
|
502
|
+
ctx: ExtensionContext,
|
|
503
|
+
options: RunOptions,
|
|
504
|
+
agentConfig: ReturnType<typeof getAgentConfig>,
|
|
505
|
+
type: SubagentType,
|
|
506
|
+
cwd: string,
|
|
507
|
+
loader: DefaultResourceLoader,
|
|
508
|
+
extResult: { extensions: Array<{ path: string; tools: Map<string, unknown> }> },
|
|
509
|
+
notify: (msg: string) => void,
|
|
510
|
+
): Promise<AgentSession> {
|
|
511
|
+
const { session } = await initSession(ctx, options, agentConfig, type, cwd, loader);
|
|
512
|
+
const baseName = agentConfig?.name ?? type;
|
|
513
513
|
session.setSessionName(
|
|
514
|
-
options.agentId ? `${
|
|
514
|
+
options.agentId ? `${baseName}#${options.agentId.slice(0, SHORT_ID_LENGTH)}` : baseName,
|
|
515
515
|
);
|
|
516
|
-
|
|
517
|
-
// Bind extensions so that session_start fires and extensions can initialize
|
|
518
|
-
// This must happen BEFORE tool filtering — extensions like pi-mcp-adapter
|
|
519
|
-
// register tools lazily during session_start, not at extension load time.
|
|
520
516
|
await session.bindExtensions({
|
|
521
|
-
onError: (err) => {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
toolName: `extension-error:${err.extensionPath}`,
|
|
525
|
-
});
|
|
526
|
-
},
|
|
517
|
+
onError: (err) => options.onToolActivity?.({
|
|
518
|
+
type: "end", toolName: `extension-error:${err.extensionPath}`,
|
|
519
|
+
}),
|
|
527
520
|
});
|
|
528
|
-
|
|
529
|
-
// Rebuild extToolMap after session_start — extensions may have registered
|
|
530
|
-
// new tools (e.g., pi-mcp-adapter registers 'mcp' tool at session_start).
|
|
531
|
-
const postBindExtToolMap = new Map<string, string[]>();
|
|
532
|
-
for (const ext of extResult.extensions) {
|
|
533
|
-
const name = extractExtensionName(ext.path);
|
|
534
|
-
const tools = [...ext.tools.keys()];
|
|
535
|
-
if (tools.length > 0) postBindExtToolMap.set(name, tools);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Filter active tools: apply tools allowlist/denylist and EXCLUDED_TOOL_NAMES
|
|
539
521
|
const filteredTools = filterActiveTools(
|
|
540
|
-
session.getActiveToolNames(),
|
|
541
|
-
|
|
542
|
-
agentConfig?.tools,
|
|
543
|
-
agentConfig?.excludeTools,
|
|
544
|
-
(msg) => {
|
|
545
|
-
if (ctx.ui?.notify) {
|
|
546
|
-
ctx.ui.notify(`[pi-subagents] ${msg}`, "warning");
|
|
547
|
-
} else {
|
|
548
|
-
console.warn(`[pi-subagents] ${msg}`);
|
|
549
|
-
}
|
|
550
|
-
},
|
|
522
|
+
session.getActiveToolNames(), buildExtToolMap(extResult.extensions),
|
|
523
|
+
agentConfig?.tools, agentConfig?.excludeTools, notify,
|
|
551
524
|
);
|
|
552
|
-
if (filteredTools)
|
|
553
|
-
session.setActiveToolsByName(filteredTools);
|
|
554
|
-
}
|
|
555
|
-
|
|
525
|
+
if (filteredTools) session.setActiveToolsByName(filteredTools);
|
|
556
526
|
options.onSessionCreated?.(session);
|
|
527
|
+
return session;
|
|
528
|
+
}
|
|
557
529
|
|
|
558
|
-
|
|
530
|
+
/**
|
|
531
|
+
* Phase 4: Subscribe to turn_end events for graceful max_turns enforcement.
|
|
532
|
+
* Returns an unsubscribe function and state getters.
|
|
533
|
+
*/
|
|
534
|
+
function wireTurnTracking(
|
|
535
|
+
session: AgentSession,
|
|
536
|
+
options: Pick<RunOptions, "maxTurns" | "graceTurns" | "onTurnEnd">,
|
|
537
|
+
) {
|
|
559
538
|
let turnCount = 0;
|
|
560
|
-
const maxTurns = normalizeMaxTurns(options.maxTurns
|
|
539
|
+
const maxTurns = normalizeMaxTurns(options.maxTurns);
|
|
561
540
|
let softLimitReached = false;
|
|
562
541
|
let aborted = false;
|
|
563
542
|
const graceTurns = options.graceTurns ?? DEFAULT_GRACE_TURNS;
|
|
564
543
|
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
aborted = true;
|
|
577
|
-
session.abort();
|
|
578
|
-
}
|
|
544
|
+
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
545
|
+
if (event.type !== "turn_end") return;
|
|
546
|
+
turnCount++;
|
|
547
|
+
options.onTurnEnd?.(turnCount);
|
|
548
|
+
if (maxTurns == null) return;
|
|
549
|
+
if (!softLimitReached && turnCount >= maxTurns) {
|
|
550
|
+
softLimitReached = true;
|
|
551
|
+
session.steer("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
|
|
552
|
+
} else if (softLimitReached && turnCount >= maxTurns + graceTurns) {
|
|
553
|
+
aborted = true;
|
|
554
|
+
session.abort();
|
|
579
555
|
}
|
|
580
556
|
});
|
|
581
557
|
|
|
558
|
+
return { unsubscribe, getAborted: () => aborted, getSteered: () => softLimitReached };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Phase 5: Execute the prompt turn loop with event wiring and cleanup.
|
|
563
|
+
*/
|
|
564
|
+
async function runTurnLoop(
|
|
565
|
+
session: AgentSession,
|
|
566
|
+
prompt: string,
|
|
567
|
+
options: RunOptions,
|
|
568
|
+
unsubTurns: () => void,
|
|
569
|
+
) {
|
|
570
|
+
const unsubEvents = subscribeToSessionEvents(session, options);
|
|
582
571
|
const collector = collectResponseText(session, options.onTextDelta);
|
|
583
572
|
const cleanupAbort = forwardAbortSignal(session, options.signal);
|
|
584
|
-
|
|
585
573
|
try {
|
|
586
574
|
await session.prompt(prompt);
|
|
587
575
|
} finally {
|
|
@@ -590,7 +578,46 @@ export async function runAgent(
|
|
|
590
578
|
collector.unsubscribe();
|
|
591
579
|
cleanupAbort();
|
|
592
580
|
}
|
|
581
|
+
return collector.getText().trim() || getLastAssistantText(session);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── main entry ─────────────────────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
export async function runAgent(
|
|
587
|
+
ctx: ExtensionContext,
|
|
588
|
+
type: SubagentType,
|
|
589
|
+
prompt: string,
|
|
590
|
+
options: RunOptions,
|
|
591
|
+
): Promise<RunResult> {
|
|
592
|
+
const config = getConfig(type);
|
|
593
|
+
const agentConfig = getAgentConfig(type);
|
|
594
|
+
|
|
595
|
+
// Warn on mutual exclusion violations
|
|
596
|
+
const notify = (msg: string) => {
|
|
597
|
+
if (ctx.ui?.notify) ctx.ui.notify(`[pi-subagents] ${msg}`, "warning");
|
|
598
|
+
else console.warn(`[pi-subagents] ${msg}`);
|
|
599
|
+
};
|
|
600
|
+
if (agentConfig?.excludeTools && Array.isArray(agentConfig.tools)) {
|
|
601
|
+
notify(`agent "${type}": both tools and exclude_tools set — tools (whitelist) wins`);
|
|
602
|
+
}
|
|
603
|
+
if (agentConfig?.excludeExtensions && Array.isArray(agentConfig.extensions)) {
|
|
604
|
+
notify(`agent "${type}": both extensions and exclude_extensions set — extensions (whitelist) wins`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
608
|
+
const env = await detectEnv(options.pi, effectiveCwd);
|
|
609
|
+
|
|
610
|
+
const systemPrompt = buildPrompt(type, agentConfig, config, effectiveCwd, env);
|
|
611
|
+
const { loader, reloadAndMap } = createResourceLoader(config, agentConfig, effectiveCwd, systemPrompt);
|
|
612
|
+
const { extResult } = await reloadAndMap();
|
|
613
|
+
const session = await createAndConfigureSession(
|
|
614
|
+
ctx, options, agentConfig, type, effectiveCwd, loader, extResult, notify,
|
|
615
|
+
);
|
|
616
|
+
const { unsubscribe: unsubTurns, getAborted, getSteered } = wireTurnTracking(session, {
|
|
617
|
+
...options,
|
|
618
|
+
maxTurns: options.maxTurns ?? agentConfig?.maxTurns,
|
|
619
|
+
});
|
|
593
620
|
|
|
594
|
-
const responseText =
|
|
595
|
-
return { responseText, session, aborted, steered:
|
|
621
|
+
const responseText = await runTurnLoop(session, prompt, options, unsubTurns);
|
|
622
|
+
return { responseText, session, aborted: getAborted(), steered: getSteered() };
|
|
596
623
|
}
|
package/src/agent-types.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { AgentConfig } from "./types.js";
|
|
|
17
17
|
* `find` and `ls` were removed — they're thin wrappers over bash commands
|
|
18
18
|
* that add ~180 tokens/turn with no real benefit.
|
|
19
19
|
*/
|
|
20
|
-
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep"];
|
|
20
|
+
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find"];
|
|
21
21
|
|
|
22
22
|
/** Unified runtime registry of all agents (defaults + user-defined). */
|
|
23
23
|
const agents = new Map<string, AgentConfig>();
|
package/src/config-io.ts
CHANGED
|
@@ -14,7 +14,15 @@ const CONFIG_PATH = path.join(CONFIG_DIR, "subagents-lite.json");
|
|
|
14
14
|
|
|
15
15
|
/** Default configuration — used when config file doesn't exist or is invalid. */
|
|
16
16
|
export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
17
|
-
agent: {
|
|
17
|
+
agent: {
|
|
18
|
+
default: null,
|
|
19
|
+
forceBackground: false,
|
|
20
|
+
graceTurns: 6,
|
|
21
|
+
widgetMaxLines: 12,
|
|
22
|
+
// widgetMaxLinesCompact intentionally omitted — derives from widgetMaxLines
|
|
23
|
+
widgetCompact: false,
|
|
24
|
+
widgetShortcut: false,
|
|
25
|
+
},
|
|
18
26
|
concurrency: { default: 4 },
|
|
19
27
|
};
|
|
20
28
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config-mutator.ts — Typed setters for all __config mutations.
|
|
3
|
+
*
|
|
4
|
+
* Every setter saves (saveConfigAtomic) and syncs internally.
|
|
5
|
+
* menus.ts calls setters instead of directly mutating __config.
|
|
6
|
+
*
|
|
7
|
+
* Sync responsibilities:
|
|
8
|
+
* - Widget settings (compact, maxLines, shortcut) → syncWidgetSettings
|
|
9
|
+
* - Cost display → setShowCostEnabled (syncs to widget)
|
|
10
|
+
* - Agent bulk replace → syncWidgetSettings
|
|
11
|
+
* - Concurrency → getManager().setConcurrency()
|
|
12
|
+
* - All others → saveConfigAtomic only
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
__config,
|
|
17
|
+
getManager,
|
|
18
|
+
setShowCostEnabled,
|
|
19
|
+
syncWidgetSettings,
|
|
20
|
+
} from "./state.js";
|
|
21
|
+
import { saveConfigAtomic, DEFAULT_CONFIG } from "./config-io.js";
|
|
22
|
+
import { CONFIG_AGENT_NON_MODEL_KEYS } from "./types.js";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Local helpers
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Persist concurrency config to disk and apply to the running manager.
|
|
30
|
+
* Defined locally so concurrency setters don't double-save.
|
|
31
|
+
*/
|
|
32
|
+
function applyConcurrencyConfig(): void {
|
|
33
|
+
saveConfigAtomic(__config);
|
|
34
|
+
getManager()?.setConcurrency(__config.concurrency);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Model override setters
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/** Set or update a model override for a type (or "default" for global). */
|
|
42
|
+
export function setModelOverride(type: string, value: string | null): void {
|
|
43
|
+
__config.agent[type] = value;
|
|
44
|
+
saveConfigAtomic(__config);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Set the global default model. */
|
|
48
|
+
export function setDefaultModel(value: string | null): void {
|
|
49
|
+
__config.agent.default = value;
|
|
50
|
+
saveConfigAtomic(__config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Clear a single per-type model override. */
|
|
54
|
+
export function clearModelOverride(type: string): void {
|
|
55
|
+
delete __config.agent[type];
|
|
56
|
+
saveConfigAtomic(__config);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Clear all model overrides, preserving non-model settings. */
|
|
60
|
+
export function clearAllModelOverrides(): void {
|
|
61
|
+
const preserved: Record<string, unknown> = {};
|
|
62
|
+
for (const key of CONFIG_AGENT_NON_MODEL_KEYS) {
|
|
63
|
+
const val = __config.agent[key];
|
|
64
|
+
if (val != null || key === "default" || key === "forceBackground") {
|
|
65
|
+
preserved[key] = val;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
__config.agent = preserved as typeof __config.agent;
|
|
69
|
+
saveConfigAtomic(__config);
|
|
70
|
+
syncWidgetSettings();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Simple agent settings
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
/** Toggle force-background mode. */
|
|
78
|
+
export function setForceBackground(enabled: boolean): void {
|
|
79
|
+
__config.agent.forceBackground = enabled;
|
|
80
|
+
saveConfigAtomic(__config);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Set the cost display toggle (syncs to widget via setShowCostEnabled). */
|
|
84
|
+
export function setShowCost(enabled: boolean): void {
|
|
85
|
+
setShowCostEnabled(enabled);
|
|
86
|
+
saveConfigAtomic(__config);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Set grace turns (number of turns after timeout before hard kill). */
|
|
90
|
+
export function setGraceTurns(n: number): void {
|
|
91
|
+
__config.agent.graceTurns = n;
|
|
92
|
+
saveConfigAtomic(__config);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Widget settings (sync via syncWidgetSettings)
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
/** Toggle force-compact widget mode. */
|
|
100
|
+
export function setWidgetCompact(enabled: boolean): void {
|
|
101
|
+
__config.agent.widgetCompact = enabled;
|
|
102
|
+
saveConfigAtomic(__config);
|
|
103
|
+
syncWidgetSettings();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Set max lines for full widget mode.
|
|
108
|
+
* Auto-derives widgetMaxLinesCompact if not explicitly set.
|
|
109
|
+
*/
|
|
110
|
+
export function setWidgetMaxLines(lines: number): void {
|
|
111
|
+
__config.agent.widgetMaxLines = lines;
|
|
112
|
+
if (__config.agent.widgetMaxLinesCompact === undefined) {
|
|
113
|
+
__config.agent.widgetMaxLinesCompact = Math.floor(lines / 2);
|
|
114
|
+
}
|
|
115
|
+
saveConfigAtomic(__config);
|
|
116
|
+
syncWidgetSettings();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Set max lines for compact widget mode. */
|
|
120
|
+
export function setWidgetMaxLinesCompact(lines: number): void {
|
|
121
|
+
__config.agent.widgetMaxLinesCompact = lines;
|
|
122
|
+
saveConfigAtomic(__config);
|
|
123
|
+
syncWidgetSettings();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Toggle ctrl+o widget shortcut. */
|
|
127
|
+
export function setWidgetShortcut(enabled: boolean): void {
|
|
128
|
+
__config.agent.widgetShortcut = enabled;
|
|
129
|
+
saveConfigAtomic(__config);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Replace the entire agent config object (used by "clear all overrides"). */
|
|
133
|
+
export function setAgent(agent: typeof __config.agent): void {
|
|
134
|
+
__config.agent = agent;
|
|
135
|
+
saveConfigAtomic(__config);
|
|
136
|
+
syncWidgetSettings();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Concurrency setters (save + sync via getManager().setConcurrency)
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/** Set the global concurrency default. */
|
|
144
|
+
export function setConcurrencyDefault(n: number): void {
|
|
145
|
+
__config.concurrency.default = n;
|
|
146
|
+
applyConcurrencyConfig();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Set or update a per-provider concurrency limit. */
|
|
150
|
+
export function setConcurrencyProvider(key: string, n: number): void {
|
|
151
|
+
const current = __config.concurrency.providers ?? {};
|
|
152
|
+
__config.concurrency.providers = { ...current, [key]: n };
|
|
153
|
+
applyConcurrencyConfig();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Set or update a per-model concurrency limit. */
|
|
157
|
+
export function setConcurrencyModel(key: string, n: number): void {
|
|
158
|
+
const current = __config.concurrency.models ?? {};
|
|
159
|
+
__config.concurrency.models = { ...current, [key]: n };
|
|
160
|
+
applyConcurrencyConfig();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Remove a per-provider concurrency limit. */
|
|
164
|
+
export function removeConcurrencyProvider(key: string): void {
|
|
165
|
+
if (__config.concurrency.providers) {
|
|
166
|
+
delete __config.concurrency.providers[key];
|
|
167
|
+
}
|
|
168
|
+
applyConcurrencyConfig();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Remove a per-model concurrency limit. */
|
|
172
|
+
export function removeConcurrencyModel(key: string): void {
|
|
173
|
+
if (__config.concurrency.models) {
|
|
174
|
+
delete __config.concurrency.models[key];
|
|
175
|
+
}
|
|
176
|
+
applyConcurrencyConfig();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Reset all concurrency settings to defaults. */
|
|
180
|
+
export function resetConcurrency(): void {
|
|
181
|
+
__config.concurrency = { ...DEFAULT_CONFIG.concurrency };
|
|
182
|
+
applyConcurrencyConfig();
|
|
183
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* buildSnapshotMarkdown: format agent conversation as markdown for snapshot viewer.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { summarizeToolArgs } from "./
|
|
8
|
+
import { summarizeToolArgs } from "./format.js";
|
|
9
9
|
|
|
10
10
|
function isTextBlock(c: unknown): c is { type: "text"; text: string } {
|
|
11
11
|
return typeof c === "object" && c !== null && (c as Record<string, unknown>).type === "text";
|
package/src/default-agents.ts
CHANGED