@try-works/dsh-recursive-mode 0.3.1 → 0.4.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 +959 -0
- package/lib/client.js +9 -2
- package/lib/closeout-report.d.ts +113 -0
- package/lib/closeout-standards.d.ts +35 -0
- package/lib/closeout.d.ts +12 -0
- package/lib/commands.d.ts +1 -1
- package/lib/config.d.ts +202 -0
- package/lib/delegation.d.ts +123 -3
- package/lib/enforcement.d.ts +90 -1
- package/lib/errors.d.ts +168 -0
- package/lib/git-context.d.ts +17 -0
- package/lib/guard-log.d.ts +39 -0
- package/lib/handoff.d.ts +29 -0
- package/lib/hooks.d.ts +103 -0
- package/lib/identity.d.ts +61 -0
- package/lib/index.d.ts +32 -12
- package/lib/index.js +9819 -3858
- package/lib/job-log.d.ts +34 -0
- package/lib/jobs-runner.d.ts +105 -0
- package/lib/json-safe.d.ts +33 -0
- package/lib/lock.d.ts +42 -0
- package/lib/memory-feedback.d.ts +52 -0
- package/lib/memory-select.d.ts +78 -0
- package/lib/memory.d.ts +137 -0
- package/lib/model-inventory.d.ts +106 -0
- package/lib/phase-graph.d.ts +111 -0
- package/lib/phase-rules.d.ts +67 -8
- package/lib/plan-gate.d.ts +68 -0
- package/lib/policy-globs.d.ts +222 -0
- package/lib/policy-write.d.ts +42 -0
- package/lib/policy.d.ts +39 -0
- package/lib/recursive_ask.tool.d.ts +88 -0
- package/lib/recursive_closeout.tool.d.ts +1 -1
- package/lib/recursive_delegate.tool.d.ts +22 -0
- package/lib/recursive_preview.tool.d.ts +48 -0
- package/lib/recursive_review.tool.d.ts +28 -0
- package/lib/result-cap.d.ts +70 -0
- package/lib/review-round.d.ts +82 -0
- package/lib/review.d.ts +9 -0
- package/lib/role-route.d.ts +122 -0
- package/lib/router.d.ts +90 -5
- package/lib/runtime.d.ts +252 -12
- package/lib/settlement.d.ts +132 -0
- package/lib/skills-phase.d.ts +71 -0
- package/lib/status.d.ts +53 -1
- package/lib/teams-loop.d.ts +91 -2
- package/lib/training.d.ts +211 -0
- package/lib/ts-lint.d.ts +15 -0
- package/lib/types.d.ts +48 -0
- package/lib/workflow-audit.d.ts +207 -0
- package/package.json +29 -30
- package/preset/recursive.patch.yml +312 -0
- package/scripts/e2e-run.mjs +51 -0
- package/scripts/link-dsh.mjs +233 -0
- package/scripts/live/fake-llm.mjs +150 -0
- package/scripts/live-session-plugin.mjs +179 -0
- package/scripts/live-session-stock.mjs +106 -0
- package/scripts/live-session.mjs +139 -0
- package/src/client/derive.ts +18 -2
- package/src/closeout-report.ts +274 -0
- package/src/closeout-standards.ts +102 -0
- package/src/closeout.ts +39 -2
- package/src/commands.ts +116 -4
- package/src/config.ts +113 -0
- package/src/delegation.ts +336 -18
- package/src/enforcement.ts +262 -72
- package/src/errors.ts +197 -0
- package/src/git-context.ts +33 -2
- package/src/guard-log.ts +134 -0
- package/src/handoff.ts +62 -0
- package/src/hooks.ts +316 -0
- package/src/identity.ts +230 -0
- package/src/index.ts +385 -20
- package/src/job-log.ts +112 -0
- package/src/jobs-runner.ts +222 -0
- package/src/json-safe.ts +75 -0
- package/src/lock.ts +153 -16
- package/src/memory-feedback.ts +185 -0
- package/src/memory-select.ts +187 -0
- package/src/memory.ts +309 -0
- package/src/model-inventory.ts +196 -0
- package/src/phase-graph.ts +191 -0
- package/src/phase-rules.ts +236 -0
- package/src/plan-gate.ts +111 -0
- package/src/policy-globs.ts +636 -0
- package/src/policy-write.ts +210 -0
- package/src/policy.ts +70 -5
- package/src/recursive_ask.tool.ts +276 -0
- package/src/recursive_audit_team.tool.ts +7 -3
- package/src/recursive_closeout.tool.ts +36 -35
- package/src/recursive_delegate.tool.ts +194 -0
- package/src/recursive_init.tool.ts +4 -3
- package/src/recursive_lint.tool.ts +81 -6
- package/src/recursive_lock.tool.ts +21 -4
- package/src/recursive_phase.tool.ts +3 -2
- package/src/recursive_preview.tool.ts +142 -0
- package/src/recursive_review.tool.ts +190 -0
- package/src/recursive_scratch.tool.ts +5 -4
- package/src/recursive_status.tool.ts +3 -2
- package/src/recursive_worktree.tool.ts +6 -5
- package/src/result-cap.ts +130 -0
- package/src/review-round.ts +335 -0
- package/src/review.ts +17 -3
- package/src/role-route.ts +230 -0
- package/src/router.ts +128 -2
- package/src/runtime.ts +968 -39
- package/src/settlement.ts +355 -0
- package/src/skills-phase.ts +143 -0
- package/src/snapshot.ts +39 -8
- package/src/status.ts +209 -4
- package/src/teams-loop.ts +223 -9
- package/src/training.ts +565 -0
- package/src/ts-lint.ts +38 -4
- package/src/types.ts +51 -0
- package/src/workflow-audit.ts +288 -0
- package/scripts/install-preset.cmd +0 -7
- package/scripts/install-preset.js +0 -101
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RecursiveRuntime } from './runtime.ts';
|
|
2
|
+
import type { SubagentsRuntimeLike } from './delegation.ts';
|
|
3
|
+
/**
|
|
4
|
+
* ⚠ FU-17 — THE WORK TOOL FILTER, AND WHY IT IS A DENY LIST RATHER THAN AN ALLOW LIST.
|
|
5
|
+
*
|
|
6
|
+
* `delegateReview` applies `input.toolFilter ?? defaultReviewToolFilter()` and that default is
|
|
7
|
+
* `{ allow: ['fs_read', 'grep', 'glob'] }`. So a work delegation that passes nothing receives a child that
|
|
8
|
+
* cannot write — and nothing about the outcome would say so.
|
|
9
|
+
*
|
|
10
|
+
* The obvious fix is an allow list of the writing tools, and it is the wrong one: it has to name EVERY tool a
|
|
11
|
+
* worker might need (`bash`, `pwsh`, `str_replace_editor`, `session_search`, `session_event_read`, `skill`,
|
|
12
|
+
* `load_workspace_dependencies`, `present`, …) and it SILENTLY REMOVES capability for each name forgotten.
|
|
13
|
+
* That is the same defect class this project has already fixed ten times: a surface that cannot express what
|
|
14
|
+
* the system can do, failing quietly.
|
|
15
|
+
*
|
|
16
|
+
* `deny` states the actual posture instead: a delegated worker may use everything EXCEPT spawning its own
|
|
17
|
+
* children and driving the team board. That is what the workflow wants — no recursive fan-out from a worker and
|
|
18
|
+
* no contention over the audit board — and it cannot lose a tool by omission. The host accepts `allow` and/or
|
|
19
|
+
* `deny` (`subagent/src/descriptor.ts`), throwing only when neither is declared.
|
|
20
|
+
*/
|
|
21
|
+
export declare const WORK_TOOL_FILTER: unknown;
|
|
22
|
+
export declare function createRecursiveDelegateTool(recursive: RecursiveRuntime, subagents?: SubagentsRuntimeLike): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EnforcementConfig } from './enforcement.ts';
|
|
2
|
+
import type { RecursiveRuntime } from './runtime.ts';
|
|
3
|
+
/** A tool call the caller wants to preview the guard's decision for. */
|
|
4
|
+
export interface PreviewProbe {
|
|
5
|
+
name: string;
|
|
6
|
+
arguments?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface PreviewResult {
|
|
9
|
+
/** T22: the byte-identical prefix, the per-phase tail, and the local identifier. */
|
|
10
|
+
policy: {
|
|
11
|
+
stable: string;
|
|
12
|
+
tail: string;
|
|
13
|
+
digest: string;
|
|
14
|
+
};
|
|
15
|
+
/** What the phase being worked on requires, from the same rules the linter enforces. */
|
|
16
|
+
phase: {
|
|
17
|
+
file: string;
|
|
18
|
+
requiredSections: string[];
|
|
19
|
+
audited: boolean;
|
|
20
|
+
tdd: boolean;
|
|
21
|
+
qa: boolean;
|
|
22
|
+
} | null;
|
|
23
|
+
/** The next legal transition, or null when nothing is pending. */
|
|
24
|
+
next: {
|
|
25
|
+
phase: string;
|
|
26
|
+
requiredSections: string[];
|
|
27
|
+
} | null;
|
|
28
|
+
/** What the guard WOULD decide for a probe call — the rule that would fire, by name. */
|
|
29
|
+
probe: {
|
|
30
|
+
kind: string;
|
|
31
|
+
rule: string;
|
|
32
|
+
reason?: string;
|
|
33
|
+
} | null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the preview. Pure apart from the run-directory reads it is given.
|
|
37
|
+
*
|
|
38
|
+
* `next` is `null` for a completed run rather than a fabricated phase: "nothing is pending" is a fact
|
|
39
|
+
* a reader needs, and inventing the last phase as "next" would be the opposite of a preview.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildPreview(input: {
|
|
42
|
+
root: string;
|
|
43
|
+
runId: string;
|
|
44
|
+
config: EnforcementConfig;
|
|
45
|
+
probe?: PreviewProbe;
|
|
46
|
+
}): PreviewResult;
|
|
47
|
+
/** `recursive_preview` — the read-only view, registered as a tool so it is one call away. */
|
|
48
|
+
export declare function createRecursivePreviewTool(recursive: RecursiveRuntime): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RecursiveRuntime } from './runtime.ts';
|
|
2
|
+
import type { ContinuableDelegationLike, SubagentsRuntimeLike } from './delegation.ts';
|
|
3
|
+
export declare function createRecursiveReviewTool(recursive: RecursiveRuntime, subagents?: SubagentsRuntimeLike): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
4
|
+
/** The child's reply file, or '' when it has not written one (an empty reply is not an approval). */
|
|
5
|
+
export declare function readReplyText(root: string, runId: string, delegationId: string, childId: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Project `delegateReview`'s result onto the loop shape the driver reads.
|
|
8
|
+
*
|
|
9
|
+
* The mapping is where the honest failure modes live: no continuable result at all
|
|
10
|
+
* (self-audit, or the delegation never ran) becomes `fellBackToOneShot`, which the
|
|
11
|
+
* driver reports as `unavailable` — the review happened without the repair path, and
|
|
12
|
+
* saying so beats reporting a success that cannot be acted on.
|
|
13
|
+
*/
|
|
14
|
+
export declare function toContinuable(review: {
|
|
15
|
+
continuable: {
|
|
16
|
+
rounds: unknown[];
|
|
17
|
+
childId?: string;
|
|
18
|
+
fellBackToOneShot?: boolean;
|
|
19
|
+
parked?: boolean;
|
|
20
|
+
ok?: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
} | null;
|
|
23
|
+
evaluation?: {
|
|
24
|
+
accepted?: boolean;
|
|
25
|
+
};
|
|
26
|
+
error?: string | null;
|
|
27
|
+
parked?: boolean;
|
|
28
|
+
}): ContinuableDelegationLike;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T24 — bounded tool results.
|
|
3
|
+
*
|
|
4
|
+
* A linter over a 126 KB port can emit hundreds of findings, and `recursive_lint`
|
|
5
|
+
* returned `errors[]`/`warnings[]` unbounded, so a single tool result could grow
|
|
6
|
+
* without limit. Every result this plugin returns must be BOUNDED, and when a
|
|
7
|
+
* bound bites it must say so in a way the reader can act on:
|
|
8
|
+
*
|
|
9
|
+
* - WHAT was removed (which list, how many),
|
|
10
|
+
* - what is SHOWN, and
|
|
11
|
+
* - HOW to see the rest.
|
|
12
|
+
*
|
|
13
|
+
* A silent truncation is worse than no cap at all: the reader cannot tell a
|
|
14
|
+
* complete result from a clipped one. Hence {@link ElisionMeta} accompanies every
|
|
15
|
+
* truncation, and the caller surfaces it next to the findings.
|
|
16
|
+
*
|
|
17
|
+
* The "how to see the rest" route is deliberately ITERATIVE rather than a
|
|
18
|
+
* bypass argument: the cap is a hard bound, so telling the caller to pass
|
|
19
|
+
* `mode: 'full'` when `mode: 'full'` is already the capped mode would be a lie.
|
|
20
|
+
* Fixing the shown findings and re-running genuinely reveals the next batch,
|
|
21
|
+
* which is also the order the workflow wants them fixed in.
|
|
22
|
+
*/
|
|
23
|
+
/** Findings kept by `mode: 'full'`. */
|
|
24
|
+
export declare const MAX_FINDINGS_FULL = 200;
|
|
25
|
+
/** Findings kept by `mode: 'summary'` — enough to orient, not enough to drown a turn. */
|
|
26
|
+
export declare const MAX_FINDINGS_SUMMARY = 10;
|
|
27
|
+
export interface ElisionMeta {
|
|
28
|
+
/** Which list was clipped. */
|
|
29
|
+
kind: 'errors' | 'warnings';
|
|
30
|
+
/** Findings the source produced. */
|
|
31
|
+
total: number;
|
|
32
|
+
/** Findings actually returned. */
|
|
33
|
+
shown: number;
|
|
34
|
+
/** `total - shown`; stated so a consumer need not compute it. */
|
|
35
|
+
omitted: number;
|
|
36
|
+
/** One self-sufficient sentence: what was removed and how to see it. */
|
|
37
|
+
hint: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ElideResult {
|
|
40
|
+
kept: string[];
|
|
41
|
+
/** Present only when the list was clipped. */
|
|
42
|
+
meta: ElisionMeta | null;
|
|
43
|
+
}
|
|
44
|
+
/** The payload shape the byte budget knows how to trim. */
|
|
45
|
+
export interface CappedPayload {
|
|
46
|
+
errors: string[];
|
|
47
|
+
warnings: string[];
|
|
48
|
+
elided: ElisionMeta[];
|
|
49
|
+
}
|
|
50
|
+
/** Serialized size of a payload, measured the way the byte budget is enforced. */
|
|
51
|
+
export declare function payloadBytes(value: unknown): number;
|
|
52
|
+
/**
|
|
53
|
+
* T28 — cap a payload by BYTES, which T24's count cap cannot do.
|
|
54
|
+
*
|
|
55
|
+
* `MAX_FINDINGS_FULL` bounds HOW MANY findings come back; it cannot see SIZE, so a
|
|
56
|
+
* handful of enormous findings still passes. This trims the longer list repeatedly
|
|
57
|
+
* until the payload fits, and reports every trim through the same `ElisionMeta` T24
|
|
58
|
+
* established — a silent truncation is worse than no cap, because the reader cannot
|
|
59
|
+
* tell a complete result from a clipped one.
|
|
60
|
+
*
|
|
61
|
+
* Terminates: each pass at least halves the longer list, and an empty pair of lists
|
|
62
|
+
* ends the loop. A payload that still exceeds the budget once both lists are empty is
|
|
63
|
+
* returned as-is — the alternative would be deleting fields the caller needs.
|
|
64
|
+
*/
|
|
65
|
+
export declare function capPayloadBytes<T extends CappedPayload>(payload: T, maxBytes: number): T;
|
|
66
|
+
/**
|
|
67
|
+
* Keep at most `max` findings. Clipping is reported, never silent.
|
|
68
|
+
* A `max` of 0 or less is treated as "keep nothing" but still reports honestly.
|
|
69
|
+
*/
|
|
70
|
+
export declare function elideFindings(kind: 'errors' | 'warnings', findings: readonly string[], max: number): ElideResult;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type ContinuableDelegationLike, type DelegationVerdict } from './delegation.ts';
|
|
2
|
+
/** The durable record of an in-flight review, so the next turn can resume it. */
|
|
3
|
+
export interface ReviewState {
|
|
4
|
+
delegationId: string;
|
|
5
|
+
/** The durable child carrying this review, stable across turns. */
|
|
6
|
+
childId: string;
|
|
7
|
+
phase: string;
|
|
8
|
+
role: string;
|
|
9
|
+
/** How many rounds the loop has observed so far. */
|
|
10
|
+
rounds: number;
|
|
11
|
+
startedAt: string;
|
|
12
|
+
lastVerdict?: DelegationVerdict;
|
|
13
|
+
/**
|
|
14
|
+
* ⚠ FU-17 — whether this round carries a VERDICT (`review`) or a DELIVERABLE (`work`). Optional and defaulted
|
|
15
|
+
* on read, so a state file written before this field existed still means what it always meant.
|
|
16
|
+
*/
|
|
17
|
+
kind?: 'review' | 'work';
|
|
18
|
+
}
|
|
19
|
+
export interface AdvanceReviewOutcome {
|
|
20
|
+
status: 'reviewing' | 'revised' | 'approved' | 'rejected' | 'unavailable' | 'submitted';
|
|
21
|
+
childId?: string;
|
|
22
|
+
verdict?: DelegationVerdict;
|
|
23
|
+
rounds: number;
|
|
24
|
+
/** One sentence for the agent, saying what to do next. */
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
/** Where one delegation's review state lives (beside its handoff and replies). */
|
|
28
|
+
export declare function reviewStatePath(runDir: string, delegationId: string): string;
|
|
29
|
+
/** Read an in-flight review, or null when there is none (or it is unreadable). */
|
|
30
|
+
export declare function readReviewState(runDir: string, delegationId: string): ReviewState | null;
|
|
31
|
+
export declare function writeReviewState(runDir: string, state: ReviewState): string;
|
|
32
|
+
/** Forget a finished review. A finished review must not be resumable by accident. */
|
|
33
|
+
export declare function clearReviewState(runDir: string, delegationId: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Advance one review by exactly one turn.
|
|
36
|
+
*
|
|
37
|
+
* `delegate` is injected rather than called directly so this driver can be tested
|
|
38
|
+
* against a scripted loop, and so the caller owns how the review is dispatched
|
|
39
|
+
* (which provider, which bundle, which parent). It receives `resumeChild` when a
|
|
40
|
+
* child is already carrying the review — a resumed round must NEVER re-establish
|
|
41
|
+
* the child, which would orphan the one already doing the work.
|
|
42
|
+
*/
|
|
43
|
+
export declare function advanceReview(input: {
|
|
44
|
+
runDir: string;
|
|
45
|
+
delegationId: string;
|
|
46
|
+
phase: string;
|
|
47
|
+
role: string;
|
|
48
|
+
/** Dispatch one turn of the continuable loop, resuming `resumeChild` when given. */
|
|
49
|
+
delegate: (args: {
|
|
50
|
+
resumeChild?: string;
|
|
51
|
+
}) => Promise<ContinuableDelegationLike>;
|
|
52
|
+
/** The child's reply text for the settled round, or '' when it has not written one. */
|
|
53
|
+
readReply: (childId: string) => string;
|
|
54
|
+
/**
|
|
55
|
+
* Deliver a repair instruction to the child.
|
|
56
|
+
*
|
|
57
|
+
* NEEDED BECAUSE THE LOOP CAN STOP EARLY ON A MISREAD. The loop's own verdict
|
|
58
|
+
* reader falls back to APPROVE when a result carries no structured verdict, so a
|
|
59
|
+
* child that answered in prose can make the loop believe it was approved and
|
|
60
|
+
* END — leaving no repair sent and the child idle. This driver re-reads the reply
|
|
61
|
+
* fail-closed and disagrees, so it must be able to send the repair itself;
|
|
62
|
+
* otherwise the round would sit in `revised` forever with nothing ever asking the
|
|
63
|
+
* child to fix anything.
|
|
64
|
+
*/
|
|
65
|
+
sendRepair?: (childId: string, instruction: string) => Promise<void>;
|
|
66
|
+
now?: () => string;
|
|
67
|
+
/**
|
|
68
|
+
* ⚠ FU-17 — WHICH KIND OF ROUND THIS IS, and it defaults to `'review'`.
|
|
69
|
+
*
|
|
70
|
+
* The kind is a PARAMETER rather than a forked code path, so every existing caller and spec keeps the exact
|
|
71
|
+
* behaviour it had — that is what makes "review is unchanged" a proof instead of a hope. A `'work'` round is
|
|
72
|
+
* the same round: same child, same state file, same park-and-resume mechanics, same repair delivery. The one
|
|
73
|
+
* difference is what a settlement MEANS: a work round's settlement is a DELIVERABLE, so the driver must not
|
|
74
|
+
* read an APPROVE out of prose that was never a verdict.
|
|
75
|
+
*/
|
|
76
|
+
kind?: 'review' | 'work';
|
|
77
|
+
/**
|
|
78
|
+
* The main agent's feedback for a `'work'` round — delivered to the SAME child, which then repairs with its
|
|
79
|
+
* working set intact. Ignored for reviews, whose repair text is read from the reviewer's own reply.
|
|
80
|
+
*/
|
|
81
|
+
instruction?: string;
|
|
82
|
+
}): Promise<AdvanceReviewOutcome>;
|
package/lib/review.d.ts
CHANGED
|
@@ -19,6 +19,15 @@ export interface ReviewBundleInput {
|
|
|
19
19
|
addenda?: string[];
|
|
20
20
|
priorEvidence?: string[];
|
|
21
21
|
memoryRefs?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* T14 — the RETRIEVED prior-run memory, rendered, written into the bundle body.
|
|
24
|
+
*
|
|
25
|
+
* ⚠ `memoryRefs` above is the slot the bundle already had, and nothing ever filled it — so the
|
|
26
|
+
* memory section a reviewer was supposed to see did not exist. The two are complementary, not
|
|
27
|
+
* duplicates: `memoryRefs` is WHERE the memory lives (traceability), and this is the CONTENT,
|
|
28
|
+
* because a reviewer cannot cite a path whose contents it was never given.
|
|
29
|
+
*/
|
|
30
|
+
memory?: string;
|
|
22
31
|
changedFiles?: string[];
|
|
23
32
|
}
|
|
24
33
|
export interface ReviewBundleResult {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T9 → FU-19 — per-role model routing, and now a full provider/model ladder.
|
|
3
|
+
*
|
|
4
|
+
* WHY. A delegation would otherwise run on whatever model the child inherits: the reviewer that must be rigorous
|
|
5
|
+
* and the repairer that will iterate many times become the same choice by accident. The item asks for them to
|
|
6
|
+
* differ — and FU-19 extends that to a general default, per-phase overrides, and a per-call choice.
|
|
7
|
+
*
|
|
8
|
+
* ⚠ THE PARAGRAPH THAT USED TO BE HERE WAS TRUE WHEN WRITTEN AND IS NOW FALSE, so it is replaced rather than
|
|
9
|
+
* left to mislead. It said the plugin "cannot by itself force a child onto a model", because
|
|
10
|
+
* `SubagentStartRequestLike` had no model field and model selection was the host's concern. Both halves changed:
|
|
11
|
+
* the harness accepts `agentOptions` on a start, and `delegateReview` now sets `request.agentOptions = { model }`
|
|
12
|
+
* — but ONLY for a provider that declares the `agentOptions` capability, because the harness REJECTS such a start
|
|
13
|
+
* otherwise.
|
|
14
|
+
*
|
|
15
|
+
* SO WHAT THIS MODULE PROMISES TODAY, precisely:
|
|
16
|
+
* - it resolves WHICH provider and model a child should get, from the ladder in `resolveSubagentTarget`, and it
|
|
17
|
+
* reports which level chose each value;
|
|
18
|
+
* - the caller (not this module) applies them, because applying is a decision with consequences — a model on a
|
|
19
|
+
* provider that cannot take overrides is NOT applied and is REPORTED rather than silently dropped;
|
|
20
|
+
* - and the model is checked against what DSH actually has (`model-inventory.ts`), with an `unverified` verdict
|
|
21
|
+
* when there is no inventory to ask, so a choice is never silently approved OR silently replaced.
|
|
22
|
+
*
|
|
23
|
+
* A module that claimed more than that would be lying about where the choice is made. The comment above is the
|
|
24
|
+
* second half of that lesson: a stale rationale is how a working feature gets deleted by accident.
|
|
25
|
+
*/
|
|
26
|
+
import type { RouterPolicy } from './router.ts';
|
|
27
|
+
/** What kind of work a role does — the distinction the item is about. */
|
|
28
|
+
export type RoleKind = 'review' | 'repair' | 'unknown';
|
|
29
|
+
export declare function roleKindOf(role: string): RoleKind;
|
|
30
|
+
/** One role's resolved route: what kind of work it is, and which model the policy names. */
|
|
31
|
+
export interface RoleRoute {
|
|
32
|
+
role: string;
|
|
33
|
+
kind: RoleKind;
|
|
34
|
+
/** The policy's model for this role, or null when it names none. */
|
|
35
|
+
model: string | null;
|
|
36
|
+
/** Where the model came from — `unset` is a fact worth carrying, not an absence. */
|
|
37
|
+
source: 'policy' | 'unset';
|
|
38
|
+
/** One self-sufficient sentence, including the unknown-role case. */
|
|
39
|
+
reason: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a role's route from the policy.
|
|
43
|
+
*
|
|
44
|
+
* Never throws and never invents a model: an unknown role, an unconfigured role and a role whose
|
|
45
|
+
* policy entry names no model each produce a route with a reason that says which it is. A caller
|
|
46
|
+
* that wants to refuse an unknown role can; a caller that wants to proceed knows exactly what it
|
|
47
|
+
* is proceeding with.
|
|
48
|
+
*/
|
|
49
|
+
export declare function routeForRole(role: string, policy: RouterPolicy): RoleRoute;
|
|
50
|
+
/**
|
|
51
|
+
* The model the caller should use for a role, or null to inherit.
|
|
52
|
+
*
|
|
53
|
+
* A one-line convenience for a caller that only needs the value; {@link routeForRole} is what a
|
|
54
|
+
* caller should use when it wants to SAY why.
|
|
55
|
+
*/
|
|
56
|
+
export declare function modelForRole(role: string, policy: RouterPolicy): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* ⚠ FU-19 — WHICH PROVIDER AND MODEL A DELEGATED CHILD WOULD ACTUALLY GET, AND WHO CHOSE EACH.
|
|
59
|
+
*
|
|
60
|
+
* ## The words, because they were doing too much work
|
|
61
|
+
*
|
|
62
|
+
* "Provider" is the thing that CREATES a child. In this harness it is resolved by a tier ladder, and the tiers
|
|
63
|
+
* have names that are not self-explanatory:
|
|
64
|
+
*
|
|
65
|
+
* - **native** — a provider the harness runs ITSELF, in-process. It is what `ctx.subagents` serves; in the
|
|
66
|
+
* session I measured it advertised exactly two: `spawn` and `fork`. "Native" means "the harness's own",
|
|
67
|
+
* as opposed to something it shells out to.
|
|
68
|
+
* - **external-cli** — a registered provider that drives a SEPARATE installed program (Codex, Claude Code and
|
|
69
|
+
* friends). A child still exists, but the work happens in another process the user installed.
|
|
70
|
+
* - **self-audit** — no child is created at all: the main agent reviews its own work. This is the honest
|
|
71
|
+
* fallback, and it is NAMED rather than hidden.
|
|
72
|
+
* - **local-controller** — the host's own controller. I have not measured this tier's behaviour in a live
|
|
73
|
+
* session, so I will not describe it further here.
|
|
74
|
+
*
|
|
75
|
+
* ## Why "a provider but no model" — the part my earlier wording got wrong
|
|
76
|
+
*
|
|
77
|
+
* A MODEL CAN BE LEFT UNSET ON PURPOSE, AND THAT IS NOT THE SAME AS "NO MODEL". Absent means **inherited**: the
|
|
78
|
+
* plugin sends no `agentOptions.model` at all, and the child runs on whatever the session/provider default is.
|
|
79
|
+
* That is the measured behaviour — `delegation.ts` forwards `agentOptions` only when the caller supplies them,
|
|
80
|
+
* and `modelForRole` returns null precisely to mean "inherit".
|
|
81
|
+
*
|
|
82
|
+
* So the floor of the ladder is not a gap. A child cannot exist without a provider, so a provider is always
|
|
83
|
+
* resolved (by the ladder, or by the user); a model is only sent when someone actually chose one, because
|
|
84
|
+
* inventing one would silently override the session's own setting — and overriding a user's session default
|
|
85
|
+
* without being asked is worse than inheriting it.
|
|
86
|
+
*
|
|
87
|
+
* ## The precedence, and the labels it reports
|
|
88
|
+
*
|
|
89
|
+
* per-call override → phase route → role route → general default → inherit
|
|
90
|
+
*
|
|
91
|
+
* Every value carries the label of the level that produced it, so "why did this child run on that model?" is
|
|
92
|
+
* answerable from the decision alone rather than by reading this function.
|
|
93
|
+
*/
|
|
94
|
+
export interface SubagentTarget {
|
|
95
|
+
role: string;
|
|
96
|
+
/** The provider that would create the child, or null when nothing resolved one. */
|
|
97
|
+
provider: string | null;
|
|
98
|
+
/** The model to ask for, or null meaning INHERIT — do not send `agentOptions.model` at all. */
|
|
99
|
+
model: string | null;
|
|
100
|
+
/** Plain-language provenance for each value: which level chose it. */
|
|
101
|
+
chosen: {
|
|
102
|
+
provider: string;
|
|
103
|
+
model: string;
|
|
104
|
+
};
|
|
105
|
+
/** One sentence a reader can act on. */
|
|
106
|
+
reason: string;
|
|
107
|
+
}
|
|
108
|
+
/** Which level produced a value. `inherit` is a decision, not an absence. */
|
|
109
|
+
export type ChoiceSource = 'per-call' | 'phase' | 'role' | 'general' | 'ladder' | 'inherit';
|
|
110
|
+
export declare function resolveSubagentTarget(input: {
|
|
111
|
+
role: string;
|
|
112
|
+
/** The phase the delegation is for, when it is known — the narrowest configured level. */
|
|
113
|
+
phase?: string;
|
|
114
|
+
policy: RouterPolicy;
|
|
115
|
+
/** A per-call override from the tool: wins over every configured level. */
|
|
116
|
+
override?: {
|
|
117
|
+
provider?: string | null;
|
|
118
|
+
model?: string | null;
|
|
119
|
+
};
|
|
120
|
+
/** What the tier ladder resolved. Used when no level named a provider, and labelled as the ladder's. */
|
|
121
|
+
ladderProvider?: string | null;
|
|
122
|
+
}): SubagentTarget;
|
package/lib/router.d.ts
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠ FU-19 — WHAT A CHILD RUNS ON, WHEN THE USER SAYS SO AND NOTHING MORE SPECIFIC DOES.
|
|
3
|
+
*
|
|
4
|
+
* Optional on purpose, and the reason is the same one the override layer gives below: an absent field must mean
|
|
5
|
+
* "defer to the next level in the precedence", never "reset to nothing". A default here would make every field
|
|
6
|
+
* present and silently shadow the role routes and phase routes forever.
|
|
7
|
+
*/
|
|
8
|
+
export interface SubagentDefault {
|
|
9
|
+
/** The SUBAGENT provider — who creates the child (`spawn`, `fork`). NOT the LLM provider; see below. */
|
|
10
|
+
provider?: string | null;
|
|
11
|
+
/** The model id to ask for. */
|
|
12
|
+
model?: string | null;
|
|
13
|
+
/**
|
|
14
|
+
* ⚠ THE LLM PROVIDER — WHO SERVES THE MODEL (`deepseek-official`), which is a DIFFERENT thing from the
|
|
15
|
+
* `provider` field above. Conflating the two was a real defect in this feature's first schema: a plain
|
|
16
|
+
* `provider` could mean either, and a user setting it had no way to know which.
|
|
17
|
+
*
|
|
18
|
+
* Optional: when only a model is named, the plugin looks its provider up from the inventory DSH exposes.
|
|
19
|
+
*/
|
|
20
|
+
modelProvider?: string | null;
|
|
21
|
+
}
|
|
22
|
+
/** Per-phase overrides — the narrowest level, and the one that answers "this phase needs a stronger model". */
|
|
23
|
+
export interface PhaseRoute {
|
|
24
|
+
role?: string;
|
|
25
|
+
/** The subagent provider, as above. */
|
|
26
|
+
provider?: string | null;
|
|
27
|
+
model?: string | null;
|
|
28
|
+
/** The LLM provider serving `model`, as above. */
|
|
29
|
+
modelProvider?: string | null;
|
|
30
|
+
}
|
|
1
31
|
export interface RouterDefaults {
|
|
2
32
|
when_role_unconfigured: string;
|
|
3
33
|
when_cli_unavailable: string;
|
|
@@ -5,18 +35,32 @@ export interface RouterDefaults {
|
|
|
5
35
|
allow_auto_assign_if_single_cli: boolean;
|
|
6
36
|
probe_timeout_ms: number;
|
|
7
37
|
invoke_timeout_ms: number;
|
|
38
|
+
/** FU-19: the general provider/model for delegated subagents, absent when the user has not chosen one. */
|
|
39
|
+
subagent?: SubagentDefault;
|
|
8
40
|
}
|
|
9
41
|
export interface RoleRoute {
|
|
10
42
|
enabled: boolean;
|
|
11
43
|
mode: string;
|
|
12
44
|
cli: string | null;
|
|
13
45
|
model: string | null;
|
|
46
|
+
/**
|
|
47
|
+
* FU-19: the provider this role should be served by, when the user has chosen one. Absent means "let the tier
|
|
48
|
+
* ladder decide", which is what every policy scaffolded before this field did.
|
|
49
|
+
*
|
|
50
|
+
* This is the SUBAGENT provider — who creates the child. The LLM provider that serves the model is its own
|
|
51
|
+
* field below, because one name for two ideas is how a configuration becomes a guess.
|
|
52
|
+
*/
|
|
53
|
+
provider?: string | null;
|
|
54
|
+
/** FU-19: the LLM provider serving this role's `model`. Optional; looked up from the inventory when omitted. */
|
|
55
|
+
modelProvider?: string | null;
|
|
14
56
|
fallback: string;
|
|
15
57
|
}
|
|
16
58
|
export interface RouterPolicy {
|
|
17
59
|
version: number;
|
|
18
60
|
defaults: RouterDefaults;
|
|
19
61
|
role_routes: Record<string, RoleRoute>;
|
|
62
|
+
/** FU-19: phase-keyed overrides, e.g. `{ '08': { role: 'memory-auditor' } }`. Absent means none. */
|
|
63
|
+
phase_routes?: Record<string, PhaseRoute>;
|
|
20
64
|
cli_overrides: Record<string, unknown>;
|
|
21
65
|
custom_clis: unknown[];
|
|
22
66
|
}
|
|
@@ -25,6 +69,25 @@ export interface RouteDecision {
|
|
|
25
69
|
tier: RouteTier;
|
|
26
70
|
provider?: string;
|
|
27
71
|
reason: string;
|
|
72
|
+
/**
|
|
73
|
+
* T9: the model the policy names for this role, or null when it names none.
|
|
74
|
+
*
|
|
75
|
+
* Present on EVERY decision because it is attached by the wrapper, never per-return-site.
|
|
76
|
+
*
|
|
77
|
+
* ⚠ THIS COMMENT USED TO SAY THE PLUGIN DOES NOT APPLY IT. That was true when it was written and is false now:
|
|
78
|
+
* `delegateReview` sets `request.agentOptions = { model }` when the provider declares the `agentOptions`
|
|
79
|
+
* capability, and reports a routing note when it does not. So this is still a value a caller can HONOUR, but
|
|
80
|
+
* the plugin now does the honouring — and says so when it cannot.
|
|
81
|
+
*/
|
|
82
|
+
model?: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* FU-19: where the provider and model came from, one short label per value, so "why did this child run on
|
|
85
|
+
* that model" is answerable without reading the resolution code.
|
|
86
|
+
*/
|
|
87
|
+
chosen?: {
|
|
88
|
+
provider?: string;
|
|
89
|
+
model?: string;
|
|
90
|
+
};
|
|
28
91
|
}
|
|
29
92
|
export interface SubagentProviderLike {
|
|
30
93
|
name: string;
|
|
@@ -33,6 +96,12 @@ export interface SubagentProviderLike {
|
|
|
33
96
|
depthLimit?: boolean;
|
|
34
97
|
toolFilter?: boolean;
|
|
35
98
|
persona?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* T9: whether this provider accepts `agentOptions` (provider/model/reasoning-effort
|
|
101
|
+
* overrides). The harness REJECTS a start that sends them to a provider without this
|
|
102
|
+
* capability, so the caller must ASK rather than assume — see `delegateReview`.
|
|
103
|
+
*/
|
|
104
|
+
agentOptions?: boolean;
|
|
36
105
|
};
|
|
37
106
|
}
|
|
38
107
|
export interface CapabilityProbe {
|
|
@@ -46,15 +115,31 @@ export interface CapabilityProbe {
|
|
|
46
115
|
};
|
|
47
116
|
reason: string;
|
|
48
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* T7 — the settings OVERRIDE layer over the declarative file.
|
|
120
|
+
*
|
|
121
|
+
* ONE PATH, NOT TWO (the item's own interaction note): `recursive-router.json` remains the
|
|
122
|
+
* declarative source, and the settings namespace can OVERRIDE individual fields without
|
|
123
|
+
* restating the file. That is why every field here is optional and why the schema declares
|
|
124
|
+
* NO defaults for them: an absent field means "defer to the file", and a default would make
|
|
125
|
+
* every field present and silently shadow the file forever.
|
|
126
|
+
*/
|
|
127
|
+
export interface RouterPolicyOverrides {
|
|
128
|
+
defaults?: Partial<RouterPolicy['defaults']>;
|
|
129
|
+
}
|
|
49
130
|
/** Parse recursive-router.json. A missing/invalid file yields a default self-audit policy (never throws). */
|
|
50
|
-
export declare function loadRouterPolicy(path?: string): RouterPolicy;
|
|
131
|
+
export declare function loadRouterPolicy(path?: string, overrides?: RouterPolicyOverrides): RouterPolicy;
|
|
51
132
|
/** Default router policy path inside a workspace root. */
|
|
52
133
|
export declare function routerPolicyPath(root: string): string;
|
|
53
134
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
135
|
+
* T9 — attach the role's model to the decision.
|
|
136
|
+
*
|
|
137
|
+
* ⚠ WHY A WRAPPER AND NOT SIX EDITS. `resolveRoleInner` has six return sites, and adding the
|
|
138
|
+
* model to each would leave a decision shape that carries it on some paths and not others — a
|
|
139
|
+
* trap for the next reader, and exactly the kind of quiet inconsistency this plan keeps
|
|
140
|
+
* refusing to ship. Attaching it ONCE, at the seam where the decision leaves, makes the field
|
|
141
|
+
* present on EVERY path by construction. The policy lookup itself lives in `role-route.ts`
|
|
142
|
+
* (`routeForRole`), so there is one definition of "which model does this role use", not two.
|
|
58
143
|
*/
|
|
59
144
|
export declare function resolveRole(role: string, policy: RouterPolicy, providers: Record<string, SubagentProviderLike>): RouteDecision;
|
|
60
145
|
/** Probe a single provider and return its advertised capabilities. */
|