@fro.bot/systematic 3.15.1 → 3.16.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/dist/cli.js +205 -36
- package/dist/{index-7cyxcwf5.js → index-ae6mhwth.js} +688 -160
- package/dist/index.js +42 -29
- package/dist/lib/agent-resolver.d.ts +7 -1
- package/dist/lib/capability-snapshot.d.ts +1 -1
- package/dist/lib/config-schema.d.ts +63 -1
- package/dist/lib/config.d.ts +59 -1
- package/dist/lib/pi-delegate-session.d.ts +2 -0
- package/dist/lib/pi-delegate-tool.d.ts +33 -1
- package/dist/lib/routing-resolver.d.ts +151 -0
- package/dist/pi.js +7496 -252
- package/dist/schemas/systematic-config.schema.json +1027 -185
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
_enum,
|
|
9
9
|
AgentOverlaySchema,
|
|
10
10
|
CategoryOverlaySchema,
|
|
11
|
+
isRecord,
|
|
12
|
+
resolveRouting,
|
|
11
13
|
loadConfig,
|
|
12
14
|
loadConfigWithSources,
|
|
13
|
-
isRecord,
|
|
14
15
|
isDiscoverableMarkdown,
|
|
15
16
|
findAgentsInDir,
|
|
16
17
|
extractAgentFrontmatter,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
extractCommandFrontmatter,
|
|
19
20
|
findSkillsInDir,
|
|
20
21
|
discoverSkills
|
|
21
|
-
} from "./index-
|
|
22
|
+
} from "./index-ae6mhwth.js";
|
|
22
23
|
|
|
23
24
|
// src/index.ts
|
|
24
25
|
import { createHash as createHash4 } from "crypto";
|
|
@@ -482,7 +483,7 @@ function loadSkillAsCommand(loaded) {
|
|
|
482
483
|
config.subtask = loaded.subtask;
|
|
483
484
|
return config;
|
|
484
485
|
}
|
|
485
|
-
function collectAgents(dir, disabledAgents, nativeAgents, overlays) {
|
|
486
|
+
function collectAgents(dir, disabledAgents, nativeAgents, overlays, rawOverlays) {
|
|
486
487
|
const agents = {};
|
|
487
488
|
const agentList = findAgentsInDir(dir);
|
|
488
489
|
const disabledSet = new Set(disabledAgents);
|
|
@@ -497,12 +498,16 @@ function collectAgents(dir, disabledAgents, nativeAgents, overlays) {
|
|
|
497
498
|
continue;
|
|
498
499
|
const config = loadAgentAsConfig(agentInfo);
|
|
499
500
|
if (config) {
|
|
500
|
-
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays);
|
|
501
|
+
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays, rawOverlays);
|
|
501
502
|
}
|
|
502
503
|
}
|
|
503
504
|
return agents;
|
|
504
505
|
}
|
|
505
|
-
|
|
506
|
+
var EMPTY_PI_SUBAGENTS_OVERLAYS = {
|
|
507
|
+
agents: {},
|
|
508
|
+
categories: {}
|
|
509
|
+
};
|
|
510
|
+
function applyAgentOverlays(config, agentInfo, overlays, rawOverlays) {
|
|
506
511
|
const id = agentInfo.category ? `${agentInfo.category}/${agentInfo.name}` : agentInfo.name;
|
|
507
512
|
const categoryOverlay = agentInfo.category ? overlays.categoriesByKey.get(agentInfo.category) : undefined;
|
|
508
513
|
const exactOverlay = overlays.agentsByTargetId.get(id);
|
|
@@ -512,15 +517,40 @@ function applyAgentOverlays(config, agentInfo, overlays) {
|
|
|
512
517
|
if (hasPermissionOverlay && isRecord(config.permission)) {
|
|
513
518
|
addPermissionRules(permissionRules, config.permission);
|
|
514
519
|
}
|
|
515
|
-
|
|
516
|
-
|
|
520
|
+
const routing = resolveRouting({
|
|
521
|
+
overlays: rawOverlays,
|
|
522
|
+
piSubagentsOverlays: EMPTY_PI_SUBAGENTS_OVERLAYS,
|
|
523
|
+
target: { agentKey: agentInfo.name, category: agentInfo.category ?? "" },
|
|
524
|
+
harness: "opencode"
|
|
525
|
+
});
|
|
526
|
+
applyAgentOverlayPass(result, categoryOverlay?.value, permissionRules, routing, "category");
|
|
527
|
+
applyAgentOverlayPass(result, exactOverlay?.value, permissionRules, routing, "agent");
|
|
517
528
|
applyPermissionOverlay(result, permissionRules, hasPermissionOverlay);
|
|
518
529
|
return result;
|
|
519
530
|
}
|
|
520
|
-
function
|
|
531
|
+
function applyAgentOverlayPass(target, overlay, permissionRules, routing, level) {
|
|
532
|
+
if (routing.source.model?.level === level) {
|
|
533
|
+
applyRoutingModel(target, routing.model);
|
|
534
|
+
}
|
|
535
|
+
if (routing.source.qualifier?.level === level) {
|
|
536
|
+
target.variant = routing.qualifier;
|
|
537
|
+
}
|
|
521
538
|
if (overlay === undefined)
|
|
522
539
|
return;
|
|
523
|
-
|
|
540
|
+
applyOverlayObjectFields(target, overlay);
|
|
541
|
+
if (isRecord(overlay.permission)) {
|
|
542
|
+
addPermissionRules(permissionRules, overlay.permission);
|
|
543
|
+
}
|
|
544
|
+
if (Array.isArray(overlay.skills)) {
|
|
545
|
+
addManagedSkillRules(permissionRules, overlay.skills);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function applyRoutingModel(target, model) {
|
|
549
|
+
if (model === null) {
|
|
550
|
+
delete target.model;
|
|
551
|
+
} else if (model !== undefined) {
|
|
552
|
+
target.model = model;
|
|
553
|
+
}
|
|
524
554
|
}
|
|
525
555
|
function applyPermissionOverlay(target, permissionRules, hasPermissionOverlay) {
|
|
526
556
|
if (!hasPermissionOverlay)
|
|
@@ -536,8 +566,6 @@ function overlayControlsPermission(overlay) {
|
|
|
536
566
|
return overlay !== undefined && (Object.hasOwn(overlay, "permission") || Object.hasOwn(overlay, "skills"));
|
|
537
567
|
}
|
|
538
568
|
var OVERLAY_ASSIGN_FIELDS = [
|
|
539
|
-
"model",
|
|
540
|
-
"variant",
|
|
541
569
|
"temperature",
|
|
542
570
|
"top_p",
|
|
543
571
|
"mode",
|
|
@@ -545,27 +573,12 @@ var OVERLAY_ASSIGN_FIELDS = [
|
|
|
545
573
|
"steps",
|
|
546
574
|
"hidden"
|
|
547
575
|
];
|
|
548
|
-
function
|
|
549
|
-
const overlayHasModel = Object.hasOwn(overlay, "model");
|
|
550
|
-
const overlayHasVariant = Object.hasOwn(overlay, "variant");
|
|
551
|
-
if (overlayHasModel && !overlayHasVariant) {
|
|
552
|
-
delete target.variant;
|
|
553
|
-
}
|
|
576
|
+
function applyOverlayObjectFields(target, overlay) {
|
|
554
577
|
for (const field of OVERLAY_ASSIGN_FIELDS) {
|
|
555
578
|
if (Object.hasOwn(overlay, field)) {
|
|
556
|
-
|
|
557
|
-
delete target[field];
|
|
558
|
-
} else {
|
|
559
|
-
target[field] = overlay[field];
|
|
560
|
-
}
|
|
579
|
+
target[field] = overlay[field];
|
|
561
580
|
}
|
|
562
581
|
}
|
|
563
|
-
if (isRecord(overlay.permission)) {
|
|
564
|
-
addPermissionRules(permissionRules, overlay.permission);
|
|
565
|
-
}
|
|
566
|
-
if (Array.isArray(overlay.skills)) {
|
|
567
|
-
addManagedSkillRules(permissionRules, overlay.skills);
|
|
568
|
-
}
|
|
569
582
|
}
|
|
570
583
|
function createPermissionRuleAccumulator() {
|
|
571
584
|
return new Map;
|
|
@@ -708,7 +721,7 @@ function createConfigHandler(deps) {
|
|
|
708
721
|
enabledSkills: enabledSkillNames
|
|
709
722
|
});
|
|
710
723
|
const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
|
|
711
|
-
const bundledAgents = collectAgents(bundledAgentsDir, systematicConfig.disabled_agents, nativeAgents, resolvedOverlays);
|
|
724
|
+
const bundledAgents = collectAgents(bundledAgentsDir, systematicConfig.disabled_agents, nativeAgents, resolvedOverlays, overlays);
|
|
712
725
|
const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
|
|
713
726
|
const discoveredSkillCommands = systematicConfig.skills_as_commands !== false ? collectDiscoveredSkillsAsCommands(directory, homeDir, opencodeConfigDir, opencodeConfigDirOverride, systematicConfig.disabled_commands) : {};
|
|
714
727
|
const bundledAgentKeys = new Set(Object.keys(bundledAgents));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Runtime persona catalog for `systematic_delegate`. Pi has no agent-discovery of its own, so category is dropped when flattening `agents/<category>/<name>.md` into this catalog. */
|
|
2
2
|
/** A single resolved persona entry in the flattened catalog. */
|
|
3
3
|
export interface AgentCatalogEntry {
|
|
4
|
-
/** Flat persona name (category dropped). */
|
|
4
|
+
/** Flat persona name (category dropped). Used for dispatch matching (`resolveAgent`); may differ from `key` if frontmatter `name` and the file stem diverge. */
|
|
5
5
|
name: string;
|
|
6
6
|
/** Human-readable description, used in tool description/parameter hints. */
|
|
7
7
|
description: string;
|
|
@@ -9,6 +9,12 @@ export interface AgentCatalogEntry {
|
|
|
9
9
|
body: string;
|
|
10
10
|
/** Raw comma-separated `tools:` frontmatter value, if declared. Undefined = not declared. */
|
|
11
11
|
toolsSource: string | undefined;
|
|
12
|
+
/** The agent's source file stem (filename without `.md`), used to key into `agents.<key>` overlays for routing (distinct from the display `name`). */
|
|
13
|
+
key: string;
|
|
14
|
+
/** The agent's category (source subdirectory name), used to key into `categories.<category>` overlays. `''` when the file has no category subdirectory -- the same no-category sentinel `config-handler.ts` and `pi-subagents-export.ts` use. */
|
|
15
|
+
category: string;
|
|
16
|
+
/** Qualified `category/key` id, mirroring `agent-overlays.ts`'s target-id convention, for callers that want a single stable identity. */
|
|
17
|
+
id: string;
|
|
12
18
|
}
|
|
13
19
|
/** Fails closed if the same persona name appears under more than one category. */
|
|
14
20
|
export declare function buildAgentCatalog(agentsDir: string): AgentCatalogEntry[];
|
|
@@ -3,7 +3,7 @@ declare const CAPABILITY_SNAPSHOT_COMMAND: 'systematic capabilities';
|
|
|
3
3
|
declare const CAPABILITY_SOURCE_IDS: readonly ['config:custom', 'config:global', 'config:project', 'config:user', 'discovery:agents', 'discovery:skills', 'host:runtime', 'package'];
|
|
4
4
|
declare const CONFIG_SOURCE_KINDS: readonly ['custom', 'project', 'user'];
|
|
5
5
|
declare const CONFIG_AUTHORITY_FIELD_PATHS: readonly ['bootstrap.enabled', 'bootstrap.file', 'skills_as_commands', 'workflow_guard.debug', 'workflow_guard.mode'];
|
|
6
|
-
declare const CONFIG_PROTECTED_FIELD_PATHS: readonly ['workflow_guard', 'agents.*.model', 'agents.*.permission', 'agents.*.skills', 'agents.*.variant', 'categories.*.model', 'categories.*.permission', 'categories.*.skills', 'categories.*.variant'];
|
|
6
|
+
declare const CONFIG_PROTECTED_FIELD_PATHS: readonly ['workflow_guard', 'profiles', 'agents.*.model', 'agents.*.permission', 'agents.*.skills', 'agents.*.variant', 'agents.*.opencode', 'agents.*.pi', 'categories.*.model', 'categories.*.permission', 'categories.*.skills', 'categories.*.variant', 'categories.*.opencode', 'categories.*.pi'];
|
|
7
7
|
declare const CONFIG_SOURCE_ERROR_CODES: readonly ['parse-failed', 'read-failed', 'schema-invalid', 'source-invalid'];
|
|
8
8
|
declare const CAPABILITY_SOURCE_PRESENCE: readonly ['absent', 'invalid', 'present'];
|
|
9
9
|
declare const CAPABILITY_STATUSES: readonly ['available', 'unknown', 'unavailable'];
|
|
@@ -54,6 +54,22 @@ export declare const AgentOverlaySchema: z.ZodObject<{
|
|
|
54
54
|
ask: "ask";
|
|
55
55
|
deny: "deny";
|
|
56
56
|
}>>]>>>;
|
|
57
|
+
opencode: z.ZodOptional<z.ZodObject<{
|
|
58
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
59
|
+
variant: z.ZodOptional<z.ZodString>;
|
|
60
|
+
}, z.core.$strict>>;
|
|
61
|
+
pi: z.ZodOptional<z.ZodObject<{
|
|
62
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
63
|
+
thinking: z.ZodOptional<z.ZodEnum<{
|
|
64
|
+
high: "high";
|
|
65
|
+
low: "low";
|
|
66
|
+
max: "max";
|
|
67
|
+
medium: "medium";
|
|
68
|
+
minimal: "minimal";
|
|
69
|
+
off: "off";
|
|
70
|
+
xhigh: "xhigh";
|
|
71
|
+
}>>;
|
|
72
|
+
}, z.core.$strict>>;
|
|
57
73
|
}, z.core.$strict>;
|
|
58
74
|
export declare const CategoryOverlaySchema: z.ZodObject<{
|
|
59
75
|
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
@@ -86,6 +102,52 @@ export declare const CategoryOverlaySchema: z.ZodObject<{
|
|
|
86
102
|
ask: "ask";
|
|
87
103
|
deny: "deny";
|
|
88
104
|
}>>]>>>;
|
|
105
|
+
opencode: z.ZodOptional<z.ZodObject<{
|
|
106
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
107
|
+
variant: z.ZodOptional<z.ZodString>;
|
|
108
|
+
}, z.core.$strict>>;
|
|
109
|
+
pi: z.ZodOptional<z.ZodObject<{
|
|
110
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
111
|
+
thinking: z.ZodOptional<z.ZodEnum<{
|
|
112
|
+
high: "high";
|
|
113
|
+
low: "low";
|
|
114
|
+
max: "max";
|
|
115
|
+
medium: "medium";
|
|
116
|
+
minimal: "minimal";
|
|
117
|
+
off: "off";
|
|
118
|
+
xhigh: "xhigh";
|
|
119
|
+
}>>;
|
|
120
|
+
}, z.core.$strict>>;
|
|
121
|
+
}, z.core.$strict>;
|
|
122
|
+
/**
|
|
123
|
+
* Routing-only projection of the agent/category overlay fields, permitted
|
|
124
|
+
* inside a named `profiles` bundle. Deliberately excludes every non-routing
|
|
125
|
+
* field (`mode`, `color`, `steps`, `hidden`, `disable`, `skills`,
|
|
126
|
+
* `permission`) so a profile cannot smuggle in UI/execution/permission
|
|
127
|
+
* changes through the profile-selection mechanism — only the fields a
|
|
128
|
+
* profile exists to carry: model, variant/thinking, and sampling knobs.
|
|
129
|
+
*/
|
|
130
|
+
export declare const ProfileOverlaySchema: z.ZodObject<{
|
|
131
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
132
|
+
variant: z.ZodOptional<z.ZodString>;
|
|
133
|
+
temperature: z.ZodOptional<z.ZodNumber>;
|
|
134
|
+
top_p: z.ZodOptional<z.ZodNumber>;
|
|
135
|
+
opencode: z.ZodOptional<z.ZodObject<{
|
|
136
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
137
|
+
variant: z.ZodOptional<z.ZodString>;
|
|
138
|
+
}, z.core.$strict>>;
|
|
139
|
+
pi: z.ZodOptional<z.ZodObject<{
|
|
140
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
141
|
+
thinking: z.ZodOptional<z.ZodEnum<{
|
|
142
|
+
high: "high";
|
|
143
|
+
low: "low";
|
|
144
|
+
max: "max";
|
|
145
|
+
medium: "medium";
|
|
146
|
+
minimal: "minimal";
|
|
147
|
+
off: "off";
|
|
148
|
+
xhigh: "xhigh";
|
|
149
|
+
}>>;
|
|
150
|
+
}, z.core.$strict>>;
|
|
89
151
|
}, z.core.$strict>;
|
|
90
152
|
export declare const PiSubagentsAgentOverlaySchema: z.ZodObject<{
|
|
91
153
|
thinking: z.ZodOptional<z.ZodEnum<{
|
|
@@ -201,4 +263,4 @@ export declare function validateConfig(input: unknown): ValidationResult;
|
|
|
201
263
|
*
|
|
202
264
|
* Matches the hand-coded `SECURITY_OVERLAY_FIELDS` set in `src/lib/config.ts`.
|
|
203
265
|
*/
|
|
204
|
-
export declare const SECURITY_OVERLAY_FIELDS: readonly ['model', 'variant', 'skills', 'permission'];
|
|
266
|
+
export declare const SECURITY_OVERLAY_FIELDS: readonly ['model', 'variant', 'skills', 'permission', 'opencode', 'pi'];
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RoutingTarget } from './routing-resolver.js';
|
|
1
2
|
export interface BootstrapConfig {
|
|
2
3
|
enabled: boolean;
|
|
3
4
|
file?: string;
|
|
@@ -19,7 +20,7 @@ export interface SourcedOverlayConfigMap {
|
|
|
19
20
|
categories: Record<string, SourcedOverlayConfig>;
|
|
20
21
|
}
|
|
21
22
|
export declare const CONFIG_AUTHORITY_FIELD_PATHS: readonly ['bootstrap.enabled', 'bootstrap.file', 'skills_as_commands', 'workflow_guard.debug', 'workflow_guard.mode'];
|
|
22
|
-
export declare const CONFIG_PROTECTED_FIELD_PATHS: readonly ['workflow_guard', 'agents.*.model', 'agents.*.permission', 'agents.*.skills', 'agents.*.variant', 'categories.*.model', 'categories.*.permission', 'categories.*.skills', 'categories.*.variant'];
|
|
23
|
+
export declare const CONFIG_PROTECTED_FIELD_PATHS: readonly ['workflow_guard', 'profiles', 'agents.*.model', 'agents.*.permission', 'agents.*.skills', 'agents.*.variant', 'agents.*.opencode', 'agents.*.pi', 'categories.*.model', 'categories.*.permission', 'categories.*.skills', 'categories.*.variant', 'categories.*.opencode', 'categories.*.pi'];
|
|
23
24
|
export type ConfigSourceKind = 'custom' | 'project' | 'user';
|
|
24
25
|
export type ConfigSourcePresence = 'absent' | 'invalid' | 'present';
|
|
25
26
|
export type ConfigSourceErrorCode = 'parse-failed' | 'read-failed' | 'schema-invalid' | 'source-invalid';
|
|
@@ -39,15 +40,48 @@ export interface ConfigProtectedFieldMetadata {
|
|
|
39
40
|
readonly outcome: 'blocked';
|
|
40
41
|
readonly sourceKind: ConfigSourceKind;
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The source kind that supplied the winning `profile` selector value, or
|
|
45
|
+
* `null` when no source set `profile` at all (case 1 of the selection table
|
|
46
|
+
* in plan 2026-09-04-002-feat-model-config-profiles, Unit 2). This names
|
|
47
|
+
* whichever source's value won the `custom ?? project ?? user` selector
|
|
48
|
+
* lookup -- including when that value turned out to name a missing bundle
|
|
49
|
+
* and the loader fell back to the user's own default (see
|
|
50
|
+
* {@link ConfigObservationMetadata.profileFallback}).
|
|
51
|
+
*/
|
|
52
|
+
export type ProfileSelectorSource = ConfigSourceKind | null;
|
|
53
|
+
/**
|
|
54
|
+
* Present when the winning `profile` selector named a bundle absent from
|
|
55
|
+
* the user source's `profiles` map. `usedDefault` names the user's own
|
|
56
|
+
* default profile if it resolved instead, or `null` if the loader fell back
|
|
57
|
+
* to base configuration (no profile).
|
|
58
|
+
*/
|
|
59
|
+
export interface ProfileFallbackMetadata {
|
|
60
|
+
readonly requested: string;
|
|
61
|
+
readonly usedDefault: string | null;
|
|
62
|
+
}
|
|
42
63
|
export interface ConfigObservationMetadata {
|
|
43
64
|
readonly authorities: readonly ConfigAuthorityMetadata[];
|
|
44
65
|
readonly protectedFields: readonly ConfigProtectedFieldMetadata[];
|
|
45
66
|
readonly sources: readonly ConfigSourceMetadata[];
|
|
67
|
+
/** The active profile's name, or `null` when base configuration is active. */
|
|
68
|
+
readonly activeProfile: string | null;
|
|
69
|
+
readonly profileSelectorSource: ProfileSelectorSource;
|
|
70
|
+
readonly profileFallback: ProfileFallbackMetadata | null;
|
|
46
71
|
}
|
|
47
72
|
export interface SourceAwareConfigResult {
|
|
48
73
|
config: SystematicConfig;
|
|
49
74
|
metadata: ConfigObservationMetadata;
|
|
50
75
|
overlays: SourcedOverlayConfigMap;
|
|
76
|
+
/**
|
|
77
|
+
* Merged `pi_subagents.{agents,categories}` overlays (three-entry chain --
|
|
78
|
+
* user, project, custom; no profile pseudo-entry, see the merge-order
|
|
79
|
+
* comment in `loadConfigWithSources`). Exposed so a caller building a
|
|
80
|
+
* routing table (e.g. `systematic config show`) can pass the exact same
|
|
81
|
+
* legacy-`thinking`-fallback input `resolveRouting` uses internally for
|
|
82
|
+
* the post-merge qualifier check, without recomputing it.
|
|
83
|
+
*/
|
|
84
|
+
piSubagentsOverlays: SourcedOverlayConfigMap;
|
|
51
85
|
}
|
|
52
86
|
export interface PiSubagentsOverlayMap {
|
|
53
87
|
categories?: OverlayConfigMap;
|
|
@@ -95,6 +129,30 @@ export interface LoadConfigOptions {
|
|
|
95
129
|
}
|
|
96
130
|
export declare function loadConfig(projectDir: string, options?: LoadConfigOptions): SystematicConfig;
|
|
97
131
|
export declare function loadConfigWithSources(projectDir: string, options?: LoadConfigOptions): SourceAwareConfigResult;
|
|
132
|
+
/**
|
|
133
|
+
* Enumerate every routing-resolver target implied by the merged overlays:
|
|
134
|
+
* every raw agent-overlay key that resolves to a real bundled agent (bare or
|
|
135
|
+
* qualified `category/key`), plus every bundled agent whose category has a
|
|
136
|
+
* category overlay (R3b's "categories are not checked in isolation" --
|
|
137
|
+
* `categories.review.variant` with no category model is fine as long as
|
|
138
|
+
* every agent in `review` resolves its own model, so every agent in an
|
|
139
|
+
* overlaid category must be walked, not just the category itself).
|
|
140
|
+
*
|
|
141
|
+
* A raw overlay key that does not resolve to any known bundled agent (e.g.
|
|
142
|
+
* an unrestricted `profiles.<name>.agents.<key>` entry naming something that
|
|
143
|
+
* isn't a real bundled agent) is silently skipped here -- it is not a valid
|
|
144
|
+
* routing target and this check has no stronger claim to make about it than
|
|
145
|
+
* schema validation already does elsewhere.
|
|
146
|
+
*
|
|
147
|
+
* A disabled agent is excluded from the result: matched by bare key or
|
|
148
|
+
* qualified `category/key` id in `disabledAgents` (the merged
|
|
149
|
+
* `disabled_agents` list), or by its own effective overlay's
|
|
150
|
+
* `disable: true` (bare key checked before the qualified id, mirroring
|
|
151
|
+
* `config-handler.ts`'s `collectAgents`/`applyAgentOverlays` precedence).
|
|
152
|
+
* A disabled agent is never emitted to OpenCode at all, so it must never
|
|
153
|
+
* block config load over a routing invariant it can't violate in practice.
|
|
154
|
+
*/
|
|
155
|
+
export declare function collectRoutingTargets(overlays: SourcedOverlayConfigMap, disabledAgents?: ReadonlySet<string>): RoutingTarget[];
|
|
98
156
|
interface ConfigPathOptions {
|
|
99
157
|
readonly homeDir?: string;
|
|
100
158
|
readonly userConfigDir?: string;
|
|
@@ -22,6 +22,8 @@ export declare function buildDelegateAgentSessionOptions(options: {
|
|
|
22
22
|
cwd: string;
|
|
23
23
|
agentDir: string;
|
|
24
24
|
model: CreateAgentSessionOptions['model'];
|
|
25
|
+
/** Omitted (not `undefined`-valued) when unset, so the child inherits Pi's default thinking level instead of an explicit `undefined` overriding it. */
|
|
26
|
+
thinkingLevel?: CreateAgentSessionOptions['thinkingLevel'];
|
|
25
27
|
allowedToolNames: string[];
|
|
26
28
|
resourceLoader: ResourceLoader;
|
|
27
29
|
sessionManager: ReturnType<typeof SessionManager.inMemory>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Pi-specific adapter factory for the `systematic_delegate` tool. `noExtensions: true` is the structural depth-1 boundary; max turns is fixed at 20. */
|
|
2
|
-
import type { ExtensionContext, ToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { CreateAgentSessionOptions, ExtensionContext, ToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
3
3
|
import { Type } from 'typebox';
|
|
4
4
|
import { type AgentCatalogEntry } from './agent-resolver.js';
|
|
5
|
+
import type { PiSubagentsOverlayMap, SourcedOverlayConfigMap } from './config.js';
|
|
5
6
|
/** Fixed, non-configurable delegation bounds (LOCKED). */
|
|
6
7
|
export declare const MAX_DELEGATE_TURNS = 20;
|
|
7
8
|
export declare const DELEGATE_TOOL_NAME = "systematic_delegate";
|
|
@@ -9,6 +10,16 @@ export declare const DELEGATE_EXECUTION_MODE: 'sequential';
|
|
|
9
10
|
export type DelegateOutcome = 'completed' | 'turn_limit' | 'aborted' | 'failed';
|
|
10
11
|
/** The parent session's model, narrowed to always-defined (validated before use). */
|
|
11
12
|
export type DelegateParentModel = NonNullable<ExtensionContext['model']>;
|
|
13
|
+
/**
|
|
14
|
+
* Derived from the pinned Pi SDK's own `CreateAgentSessionOptions` type
|
|
15
|
+
* (`'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'` as of
|
|
16
|
+
* @earendil-works/pi-coding-agent@0.83.0) rather than hand-declared, so a
|
|
17
|
+
* future SDK bump that changes the union surfaces as a type error here
|
|
18
|
+
* instead of silently drifting. Value-for-value identical to Systematic's
|
|
19
|
+
* own `piSubagentsThinkingSchema` enum in config-schema.ts today — see
|
|
20
|
+
* `isKnownThinkingLevel`'s runtime guard for the defence-in-depth check.
|
|
21
|
+
*/
|
|
22
|
+
export type DelegateThinkingLevel = NonNullable<CreateAgentSessionOptions['thinkingLevel']>;
|
|
12
23
|
export interface DelegateToolDetails {
|
|
13
24
|
persona: string;
|
|
14
25
|
turnCount: number;
|
|
@@ -26,6 +37,8 @@ export interface DelegateSessionLike {
|
|
|
26
37
|
export type CreateDelegateSession = (options: {
|
|
27
38
|
agentName: string;
|
|
28
39
|
model: DelegateParentModel;
|
|
40
|
+
/** Omitted for a config-neutral or thinking-neutral dispatch — the child then inherits Pi's default thinking level. Resolved from `pi.thinking` (or the legacy `pi_subagents.thinking`) the same way `model` is; the pinned Pi SDK's `CreateAgentSessionOptions.thinkingLevel` field is what makes this a real session option, not just an export-time one. */
|
|
41
|
+
thinkingLevel?: DelegateThinkingLevel;
|
|
29
42
|
cwd: string;
|
|
30
43
|
systemPromptOverride: string;
|
|
31
44
|
allowedToolNames: string[];
|
|
@@ -33,6 +46,25 @@ export type CreateDelegateSession = (options: {
|
|
|
33
46
|
export interface PiDelegateToolDeps {
|
|
34
47
|
catalog: AgentCatalogEntry[];
|
|
35
48
|
createDelegateSession: CreateDelegateSession;
|
|
49
|
+
/**
|
|
50
|
+
* Merged agent/category routing overlays from `loadConfigWithSources`'s
|
|
51
|
+
* `overlays` field. Omit for a config-neutral tool — routing then always
|
|
52
|
+
* inherits `ctx.model` (matches pre-Unit-5 behaviour).
|
|
53
|
+
*/
|
|
54
|
+
overlays?: SourcedOverlayConfigMap;
|
|
55
|
+
/**
|
|
56
|
+
* The final config's merged `pi_subagents` map (`SystematicConfig.pi_subagents`,
|
|
57
|
+
* already project-stripped by the loader), for `resolveRouting`'s legacy
|
|
58
|
+
* `thinking` fallback (R5). Model resolution never reads this map.
|
|
59
|
+
*/
|
|
60
|
+
piSubagentsOverlays?: PiSubagentsOverlayMap;
|
|
61
|
+
/**
|
|
62
|
+
* The active profile's name, from `ConfigObservationMetadata.activeProfile`.
|
|
63
|
+
* Echoed in the R4a routing notice so a user with multiple profiles can
|
|
64
|
+
* tell which one produced the model. `null`/omitted means no profile is
|
|
65
|
+
* active.
|
|
66
|
+
*/
|
|
67
|
+
activeProfile?: string | null;
|
|
36
68
|
}
|
|
37
69
|
export declare function createPiDelegateTool(deps: PiDelegateToolDeps): ToolDefinition<ReturnType<typeof buildDelegateParametersSchema>, DelegateToolDetails>;
|
|
38
70
|
declare function buildDelegateParametersSchema(catalog: AgentCatalogEntry[]): Type.TObject<{
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing resolver: answers "what model and qualifier does target T get on
|
|
3
|
+
* harness H, and from where" from a set of already-merged config overlays.
|
|
4
|
+
*
|
|
5
|
+
* Part of the model-config-profiles feature (plan
|
|
6
|
+
* 2026-09-04-002-feat-model-config-profiles, Unit 3). One routing precedence,
|
|
7
|
+
* shared by every consumer that needs to know an agent's effective model:
|
|
8
|
+
* the OpenCode config hook, the Pi delegate tool, the Pi persona export, and
|
|
9
|
+
* `config show` (Units 4-6). This module is pure — it takes already-merged
|
|
10
|
+
* overlay data and returns a resolution; it never reads files, never throws,
|
|
11
|
+
* and never calls `console.warn`. The post-merge qualifier-requires-model
|
|
12
|
+
* invariant check and any warning emission live in the caller
|
|
13
|
+
* (`src/lib/config.ts`), which has the `warningSink` this module deliberately
|
|
14
|
+
* does not.
|
|
15
|
+
*/
|
|
16
|
+
import type { OverlayConfigMap, PiSubagentsOverlayMap, SourcedOverlayConfig, SourcedOverlayConfigMap } from './config.js';
|
|
17
|
+
export type Harness = 'opencode' | 'pi';
|
|
18
|
+
/**
|
|
19
|
+
* One resolution target: a bundled agent's bare file-stem key plus its
|
|
20
|
+
* category, keyed the same way `agent-overlays.ts` keys bundled agents
|
|
21
|
+
* (`resolveAgentOverlaySet`'s `agentsByTargetId` uses the qualified
|
|
22
|
+
* `category/key` id internally; this module accepts the split form since
|
|
23
|
+
* that's what a category-driven walk naturally produces).
|
|
24
|
+
*/
|
|
25
|
+
export interface RoutingTarget {
|
|
26
|
+
readonly agentKey: string;
|
|
27
|
+
readonly category: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Where a resolved `model` or qualifier value came from.
|
|
31
|
+
*
|
|
32
|
+
* `form` is `'legacy-pi-subagents'` only for a `pi` harness qualifier
|
|
33
|
+
* resolved from the deprecated `pi_subagents.<agents|categories>.<key>.thinking`
|
|
34
|
+
* location (R5) — `model` and `variant` never resolve from there.
|
|
35
|
+
*/
|
|
36
|
+
export interface RoutingFieldSource {
|
|
37
|
+
readonly level: 'agent' | 'category';
|
|
38
|
+
readonly form: 'block' | 'flat' | 'legacy-pi-subagents';
|
|
39
|
+
}
|
|
40
|
+
export interface RoutingResolution {
|
|
41
|
+
/**
|
|
42
|
+
* `undefined` when no layer set a model at all (inherit from the parent
|
|
43
|
+
* agent/session with no explicit source). `null` is itself a resolved
|
|
44
|
+
* value meaning "inherit", explicitly set by some layer — it is NOT the
|
|
45
|
+
* same as `undefined` and beats a lower layer's explicit model string,
|
|
46
|
+
* per R3a/the plan's KTD on `model: null` precedence.
|
|
47
|
+
*/
|
|
48
|
+
readonly model: string | null | undefined;
|
|
49
|
+
/** `undefined` when no layer set a qualifier for this harness. */
|
|
50
|
+
readonly qualifier: string | undefined;
|
|
51
|
+
readonly source: {
|
|
52
|
+
readonly model: RoutingFieldSource | undefined;
|
|
53
|
+
readonly qualifier: RoutingFieldSource | undefined;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* True when the deprecated `pi_subagents.<key>.thinking` value is present
|
|
57
|
+
* for this target (agent overlay checked before category, mirroring
|
|
58
|
+
* `pi-subagents-export.ts`'s existing precedence) — regardless of whether
|
|
59
|
+
* it actually won as `qualifier`'s source. Always `false` for the
|
|
60
|
+
* `opencode` harness.
|
|
61
|
+
*
|
|
62
|
+
* R5 requires one deprecation warning whenever the legacy field is
|
|
63
|
+
* present, even when a `pi.thinking` block is also set and wins (the user
|
|
64
|
+
* still has stale config to migrate away from). Callers should branch on
|
|
65
|
+
* this flag, not on `source.qualifier.form === 'legacy-pi-subagents'`, to
|
|
66
|
+
* decide whether to warn — the latter is `true` only when legacy actually
|
|
67
|
+
* supplied the resolved value.
|
|
68
|
+
*/
|
|
69
|
+
readonly legacyPiSubagentsThinkingPresent: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Which harness this resolution was computed for. Carried on the result
|
|
72
|
+
* (not just the input) so `qualifierResolvesWithoutModel` can self-guard
|
|
73
|
+
* against ever flagging a Pi resolution as a violation, regardless of
|
|
74
|
+
* caller discipline — see that function's doc comment.
|
|
75
|
+
*/
|
|
76
|
+
readonly harness: Harness;
|
|
77
|
+
}
|
|
78
|
+
export interface ResolveRoutingInput {
|
|
79
|
+
/** The `overlays` value `loadConfigWithSources` returns (agents/categories, already merged). */
|
|
80
|
+
readonly overlays: SourcedOverlayConfigMap;
|
|
81
|
+
/** The merged `pi_subagents` overlays, for the legacy `thinking` fallback. */
|
|
82
|
+
readonly piSubagentsOverlays: SourcedOverlayConfigMap;
|
|
83
|
+
readonly target: RoutingTarget;
|
|
84
|
+
readonly harness: Harness;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `loadConfigWithSources` exposes the merged `agents`/`categories` routing
|
|
88
|
+
* overlays in `SourcedOverlayConfigMap` form (value + source metadata), but
|
|
89
|
+
* some callers (the Pi delegate tool, Pi persona export) only have a plain,
|
|
90
|
+
* already-flattened overlay map on hand -- e.g. `SystematicConfig.pi_subagents`,
|
|
91
|
+
* which retains no per-value source metadata past its own merge.
|
|
92
|
+
* `resolveRouting`'s `piSubagentsOverlays` parameter only ever reads
|
|
93
|
+
* `.value` off each entry (see `getOverlayValue` above), so wrapping each
|
|
94
|
+
* plain value with placeholder source fields is a safe, purely-shape
|
|
95
|
+
* adapter for feeding the resolver -- it never changes what resolves.
|
|
96
|
+
* Exported so every consumer shares one implementation instead of each
|
|
97
|
+
* defining its own copy.
|
|
98
|
+
*/
|
|
99
|
+
export declare function toSourcedOverlayMap(map: OverlayConfigMap | undefined): Record<string, SourcedOverlayConfig>;
|
|
100
|
+
/**
|
|
101
|
+
* Apply {@link toSourcedOverlayMap} to both halves of a plain
|
|
102
|
+
* `pi_subagents`-shaped map (`{agents, categories}`), producing a
|
|
103
|
+
* `SourcedOverlayConfigMap` ready to pass as `resolveRouting`'s
|
|
104
|
+
* `piSubagentsOverlays` argument.
|
|
105
|
+
*/
|
|
106
|
+
export declare function toSourcedPiSubagentsOverlays(map: PiSubagentsOverlayMap | undefined): SourcedOverlayConfigMap;
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the effective `{ model, qualifier, source }` for one target on one
|
|
109
|
+
* harness. Pure — same inputs always produce the same output; no I/O, no
|
|
110
|
+
* console output, no throwing.
|
|
111
|
+
*/
|
|
112
|
+
export declare function resolveRouting(input: ResolveRoutingInput): RoutingResolution;
|
|
113
|
+
/**
|
|
114
|
+
* True when the OpenCode `variant` resolved but no model resolved at any
|
|
115
|
+
* layer (`model` is `undefined` — `null` counts as a resolved model meaning
|
|
116
|
+
* "inherit", so it does NOT trigger this). Used by the loader's post-merge
|
|
117
|
+
* check to raise a config error naming the target and harness (R3b).
|
|
118
|
+
*
|
|
119
|
+
* ALWAYS false for the `pi` harness: Pi's `thinking` qualifier is
|
|
120
|
+
* independent of `model` by design (see `resolveRouting`'s Pi branch) — it
|
|
121
|
+
* applies to whatever model the delegate ends up running, including one
|
|
122
|
+
* inherited from the parent session, so "thinking with no model anywhere"
|
|
123
|
+
* is a normal, valid configuration, not an error. This function self-guards
|
|
124
|
+
* on `resolution.harness` rather than relying on every caller to only ever
|
|
125
|
+
* invoke it for opencode resolutions.
|
|
126
|
+
*/
|
|
127
|
+
export declare function qualifierResolvesWithoutModel(resolution: RoutingResolution): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Format the one-line deprecation message for a WRITTEN
|
|
130
|
+
* `pi_subagents.<scope>.<key>.thinking` field, naming the exact path the
|
|
131
|
+
* user wrote and the replacement path it should move to. `scope` is
|
|
132
|
+
* `'agents'` or `'categories'`, matching whichever map the field was
|
|
133
|
+
* actually written under -- a category-level write is never renamed to
|
|
134
|
+
* look like an agent-level path (or vice versa).
|
|
135
|
+
*/
|
|
136
|
+
export declare function formatWrittenLegacyPiSubagentsThinkingWarning(scope: 'agents' | 'categories', key: string): string;
|
|
137
|
+
/**
|
|
138
|
+
* Collect one deprecation-warning message per WRITTEN
|
|
139
|
+
* `pi_subagents.<scope>.<key>.thinking` field found directly in the merged
|
|
140
|
+
* `pi_subagents` overlays -- NOT one per agent the field happens to
|
|
141
|
+
* resolve for. A category-level write (`pi_subagents.categories.<c>.thinking`)
|
|
142
|
+
* fans out to every bundled agent in that category when resolved through
|
|
143
|
+
* `resolveRouting` (by design -- that is how the legacy fallback applies),
|
|
144
|
+
* but the user only wrote ONE field, so they should see exactly ONE
|
|
145
|
+
* warning naming exactly the field they wrote, not N warnings each naming
|
|
146
|
+
* an agent-level path they never touched. The warning fires whenever the
|
|
147
|
+
* field is written, regardless of whether a higher-priority `pi.thinking`
|
|
148
|
+
* block ends up winning for any given agent -- a written legacy field is
|
|
149
|
+
* always deprecated, whether or not it is currently shadowed.
|
|
150
|
+
*/
|
|
151
|
+
export declare function collectWrittenLegacyPiSubagentsThinkingWarnings(piSubagentsOverlays: SourcedOverlayConfigMap): string[];
|