@fro.bot/systematic 3.15.1 → 3.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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[];