@abelxiaoxing/cadence 1.0.3 → 1.0.4
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/config/.env.example +16 -1
- package/package.json +1 -1
- package/skills/_shared/load-config.d.mts +7 -0
- package/src/contracts.ts +9 -2
- package/src/parent-provider.ts +117 -0
- package/src/runtime.ts +62 -7
- package/src/subagent-endpoint.ts +158 -0
- package/src/worker.ts +7 -0
package/config/.env.example
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
# Copy to <project>/.pi/
|
|
1
|
+
# Copy to <project>/.pi/cadence/.env or ~/.pi/agent/cadence/.env.
|
|
2
2
|
# The project file wins as a whole file; values are not merged or interpolated.
|
|
3
3
|
|
|
4
|
+
# Optional Subagent endpoint. Any non-empty key commits the whole layer, which
|
|
5
|
+
# then requires MODEL and API_URL. Empty API/bounds use the documented defaults.
|
|
6
|
+
SUBAGENT_API_URL=
|
|
7
|
+
SUBAGENT_API_KEY=
|
|
8
|
+
SUBAGENT_MODEL=
|
|
9
|
+
# Dialect: openai-completions (default), openai-responses, anthropic-messages.
|
|
10
|
+
SUBAGENT_API=
|
|
11
|
+
# Internal defaults: context window 256000, max output tokens 128000.
|
|
12
|
+
SUBAGENT_CONTEXT_WINDOW=
|
|
13
|
+
SUBAGENT_MAX_TOKENS=
|
|
14
|
+
|
|
15
|
+
# Per-role layers use the same six suffixes and override the global layer whole:
|
|
16
|
+
# SUBAGENT_<ROLE>_<SUFFIX>, where ROLE is DESIGN_EXPLORER, CONTRACT_REVIEWER,
|
|
17
|
+
# IMPLEMENTATION_WORKER, or DIAGNOSIS_WORKER. Partial/invalid layers fail closed.
|
|
18
|
+
|
|
4
19
|
CONTEXT7_API_URL=https://context7.com/api/v2
|
|
5
20
|
CONTEXT7_API_KEY=
|
|
6
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abelxiaoxing/cadence",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Prompts, skills, and a private orchestration extension exposing the Abel four-stage workflow (/abel-init, /abel-design, /abel-implement, /abel-diagnose) with package-owned professional Agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/contracts.ts
CHANGED
|
@@ -114,6 +114,7 @@ export const ENVIRONMENT_FAILURE_CODES = [
|
|
|
114
114
|
"git-apply-unavailable",
|
|
115
115
|
"git-ignore-check-failed",
|
|
116
116
|
"git-index-unavailable",
|
|
117
|
+
"invalid-subagent-endpoint",
|
|
117
118
|
"root-unavailable",
|
|
118
119
|
"sandbox-runtime-unavailable",
|
|
119
120
|
] as const;
|
|
@@ -133,10 +134,16 @@ export type EnvironmentFailureCode = (typeof ENVIRONMENT_FAILURE_CODES)[number];
|
|
|
133
134
|
export type ApprovalBoundaryCode = (typeof APPROVAL_BOUNDARY_CODES)[number];
|
|
134
135
|
export type ChildTransportCode = (typeof CHILD_TRANSPORT_CODES)[number];
|
|
135
136
|
|
|
137
|
+
export type EnvironmentFailure = {
|
|
138
|
+
kind: "environment";
|
|
139
|
+
code: EnvironmentFailureCode;
|
|
140
|
+
message?: string;
|
|
141
|
+
};
|
|
142
|
+
|
|
136
143
|
export type CandidateFailure =
|
|
137
144
|
| { kind: "artifact"; code: ArtifactFailureCode; evidence?: string[] }
|
|
138
145
|
| { kind: "stale"; code: StaleFailureCode }
|
|
139
|
-
|
|
|
146
|
+
| EnvironmentFailure
|
|
140
147
|
| { kind: "approval-boundary"; code: ApprovalBoundaryCode }
|
|
141
148
|
| { kind: "cancelled"; code: "cancelled" }
|
|
142
149
|
| { kind: "result-limit"; limitBytes: number };
|
|
@@ -163,7 +170,7 @@ export type TaskFailure =
|
|
|
163
170
|
kind: "checkpoint-attempts-exhausted";
|
|
164
171
|
cause: "artifact" | "stale";
|
|
165
172
|
}
|
|
166
|
-
|
|
|
173
|
+
| EnvironmentFailure
|
|
167
174
|
| { kind: "result-limit"; limitBytes: number };
|
|
168
175
|
|
|
169
176
|
export interface CandidateApplyEvidence {
|
package/src/parent-provider.ts
CHANGED
|
@@ -2,7 +2,13 @@ import {
|
|
|
2
2
|
InMemoryCredentialStore,
|
|
3
3
|
type Model,
|
|
4
4
|
type Provider,
|
|
5
|
+
type StreamOptions,
|
|
5
6
|
} from "@earendil-works/pi-ai";
|
|
7
|
+
import {
|
|
8
|
+
createProvider,
|
|
9
|
+
stream as subagentStream,
|
|
10
|
+
streamSimple as subagentStreamSimple,
|
|
11
|
+
} from "@earendil-works/pi-ai/compat";
|
|
6
12
|
import {
|
|
7
13
|
type ExtensionContext,
|
|
8
14
|
ModelRuntime,
|
|
@@ -13,6 +19,117 @@ import type {
|
|
|
13
19
|
ParentPayloadCallback,
|
|
14
20
|
ParentPayloadCapture,
|
|
15
21
|
} from "./parent-payload-bridge.ts";
|
|
22
|
+
import type { SubagentEndpoint } from "./subagent-endpoint.ts";
|
|
23
|
+
|
|
24
|
+
const SUBAGENT_PROVIDER_ID = "abel-subagent";
|
|
25
|
+
|
|
26
|
+
function customStreamOptions<T extends StreamOptions>(
|
|
27
|
+
model: Model<string>,
|
|
28
|
+
options?: T,
|
|
29
|
+
): T | undefined {
|
|
30
|
+
if (model.api !== "openai-responses") return options;
|
|
31
|
+
const childOnPayload = options?.onPayload;
|
|
32
|
+
return {
|
|
33
|
+
...options,
|
|
34
|
+
onPayload: async (payload, requestModel) => {
|
|
35
|
+
const transformed = await childOnPayload?.(payload, requestModel);
|
|
36
|
+
const current = transformed === undefined ? payload : transformed;
|
|
37
|
+
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
38
|
+
return current;
|
|
39
|
+
const normalized = { ...current } as Record<string, unknown>;
|
|
40
|
+
delete normalized.max_output_tokens;
|
|
41
|
+
return normalized;
|
|
42
|
+
},
|
|
43
|
+
} as T;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The compat dispatchers resolve the concrete API implementation from
|
|
47
|
+
// model.api at call time, so one pair serves all supported dialects.
|
|
48
|
+
const SUBAGENT_STREAMS = {
|
|
49
|
+
stream(
|
|
50
|
+
model: Parameters<typeof subagentStream>[0],
|
|
51
|
+
context: Parameters<typeof subagentStream>[1],
|
|
52
|
+
options?: Parameters<typeof subagentStream>[2],
|
|
53
|
+
) {
|
|
54
|
+
return subagentStream(
|
|
55
|
+
model,
|
|
56
|
+
context,
|
|
57
|
+
customStreamOptions(model as Model<string>, options),
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
streamSimple(
|
|
61
|
+
model: Parameters<typeof subagentStreamSimple>[0],
|
|
62
|
+
context: Parameters<typeof subagentStreamSimple>[1],
|
|
63
|
+
options?: Parameters<typeof subagentStreamSimple>[2],
|
|
64
|
+
) {
|
|
65
|
+
return subagentStreamSimple(
|
|
66
|
+
model,
|
|
67
|
+
context,
|
|
68
|
+
customStreamOptions(model as Model<string>, options),
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function customSubagentModel(endpoint: SubagentEndpoint): Model<string> {
|
|
74
|
+
return {
|
|
75
|
+
id: endpoint.model,
|
|
76
|
+
name: endpoint.model,
|
|
77
|
+
api: endpoint.dialect,
|
|
78
|
+
provider: SUBAGENT_PROVIDER_ID,
|
|
79
|
+
baseUrl: endpoint.url,
|
|
80
|
+
reasoning: true,
|
|
81
|
+
input: ["text"],
|
|
82
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
83
|
+
contextWindow: endpoint.contextWindow,
|
|
84
|
+
maxTokens: endpoint.maxTokens,
|
|
85
|
+
} as Model<string>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function customSubagentProvider(
|
|
89
|
+
endpoint: SubagentEndpoint,
|
|
90
|
+
model: Model<string>,
|
|
91
|
+
): Provider {
|
|
92
|
+
return createProvider({
|
|
93
|
+
id: SUBAGENT_PROVIDER_ID,
|
|
94
|
+
name: "Abel Subagent Endpoint",
|
|
95
|
+
baseUrl: endpoint.url,
|
|
96
|
+
auth: {
|
|
97
|
+
apiKey: {
|
|
98
|
+
name: "Abel subagent endpoint auth",
|
|
99
|
+
resolve: async () =>
|
|
100
|
+
endpoint.apiKey
|
|
101
|
+
? { auth: { apiKey: endpoint.apiKey }, source: "subagent-endpoint" }
|
|
102
|
+
: {
|
|
103
|
+
auth: {
|
|
104
|
+
apiKey: "unused",
|
|
105
|
+
headers: { authorization: null, "x-api-key": null },
|
|
106
|
+
},
|
|
107
|
+
source: "subagent-endpoint-keyless",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
models: [model],
|
|
112
|
+
api: SUBAGENT_STREAMS,
|
|
113
|
+
}) as Provider;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function customPhaseRuntime(
|
|
117
|
+
endpoint: SubagentEndpoint,
|
|
118
|
+
signal?: AbortSignal,
|
|
119
|
+
): Promise<PhaseRuntimeResult> {
|
|
120
|
+
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
121
|
+
const model = customSubagentModel(endpoint);
|
|
122
|
+
const provider = customSubagentProvider(endpoint, model);
|
|
123
|
+
let modelRuntime: ModelRuntime;
|
|
124
|
+
try {
|
|
125
|
+
modelRuntime = await runtimeForProvider(provider, signal);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (isCancellation(error, signal)) return cancelledPhaseRuntime();
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
131
|
+
return { ok: true, modelRuntime, model };
|
|
132
|
+
}
|
|
16
133
|
|
|
17
134
|
export interface PhasePayloadBridge {
|
|
18
135
|
readonly bridge: ParentPayloadBridge;
|
package/src/runtime.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
snapshotFiles,
|
|
43
43
|
} from "./file-snapshot.ts";
|
|
44
44
|
import type { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
45
|
-
import { runtimeFromContext } from "./parent-provider.ts";
|
|
45
|
+
import { customPhaseRuntime, runtimeFromContext } from "./parent-provider.ts";
|
|
46
46
|
import { applyAgentsCheckpoint, applyRetainedPatch } from "./patch.ts";
|
|
47
47
|
import {
|
|
48
48
|
type BoundRetainedResult,
|
|
@@ -51,6 +51,11 @@ import {
|
|
|
51
51
|
type RetainedCandidateIdentity,
|
|
52
52
|
} from "./result-store.ts";
|
|
53
53
|
import { declarationsConflict, Scheduler } from "./scheduler.ts";
|
|
54
|
+
import {
|
|
55
|
+
describeInvalidSubagentEndpoint,
|
|
56
|
+
resolveSubagentEndpoint,
|
|
57
|
+
type SubagentEndpoint,
|
|
58
|
+
} from "./subagent-endpoint.ts";
|
|
54
59
|
import {
|
|
55
60
|
type TaskRecord,
|
|
56
61
|
taskConflictOf,
|
|
@@ -1309,11 +1314,30 @@ export class Runtime {
|
|
|
1309
1314
|
...cancellationError(signal),
|
|
1310
1315
|
failureKind: "cancelled",
|
|
1311
1316
|
};
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
+
const task = this.taskRecords.get(envelope);
|
|
1318
|
+
const endpoint = task
|
|
1319
|
+
? task.subagentEndpoint === null
|
|
1320
|
+
? ({ kind: "inherited" } as const)
|
|
1321
|
+
: ({ kind: "custom", endpoint: task.subagentEndpoint } as const)
|
|
1322
|
+
: resolveSubagentEndpoint(envelope.role, { cwd: ctx.cwd });
|
|
1323
|
+
if (endpoint.kind === "invalid") {
|
|
1324
|
+
const message = describeInvalidSubagentEndpoint(endpoint);
|
|
1325
|
+
return {
|
|
1326
|
+
ok: false,
|
|
1327
|
+
error: message,
|
|
1328
|
+
failure: {
|
|
1329
|
+
kind: "environment",
|
|
1330
|
+
code: "invalid-subagent-endpoint",
|
|
1331
|
+
message,
|
|
1332
|
+
},
|
|
1333
|
+
failureKind: "failed",
|
|
1334
|
+
failureClass: "environment",
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
const phase =
|
|
1338
|
+
endpoint.kind === "custom"
|
|
1339
|
+
? await customPhaseRuntime(endpoint.endpoint, signal)
|
|
1340
|
+
: await runtimeFromContext(ctx, this.parentPayloadBridge, signal);
|
|
1317
1341
|
if (!phase.ok) {
|
|
1318
1342
|
return {
|
|
1319
1343
|
ok: false,
|
|
@@ -1425,7 +1449,6 @@ export class Runtime {
|
|
|
1425
1449
|
usage: child.usage,
|
|
1426
1450
|
};
|
|
1427
1451
|
}
|
|
1428
|
-
const task = this.taskRecords.get(envelope);
|
|
1429
1452
|
const retainedRoot = task?.workspaceRoot ?? ctx.cwd;
|
|
1430
1453
|
const retainedSnapshot =
|
|
1431
1454
|
prepared?.snapshot ??
|
|
@@ -1547,11 +1570,43 @@ export class Runtime {
|
|
|
1547
1570
|
const identity = workerIdentity(ctx.model);
|
|
1548
1571
|
let record: TaskRecord;
|
|
1549
1572
|
if (runRequest.kind === "open-task") {
|
|
1573
|
+
const endpoint = resolveSubagentEndpoint("implementation-worker", {
|
|
1574
|
+
cwd: ctx.cwd,
|
|
1575
|
+
});
|
|
1576
|
+
if (endpoint.kind === "invalid") {
|
|
1577
|
+
const failure = {
|
|
1578
|
+
kind: "environment",
|
|
1579
|
+
code: "invalid-subagent-endpoint",
|
|
1580
|
+
message: describeInvalidSubagentEndpoint(endpoint),
|
|
1581
|
+
} as const;
|
|
1582
|
+
record = this.registry.open(
|
|
1583
|
+
runRequest.boundary,
|
|
1584
|
+
identity,
|
|
1585
|
+
workspaceRoot,
|
|
1586
|
+
attempt,
|
|
1587
|
+
);
|
|
1588
|
+
record.state = {
|
|
1589
|
+
kind: "blocked",
|
|
1590
|
+
phase: attempt.phase,
|
|
1591
|
+
failure,
|
|
1592
|
+
};
|
|
1593
|
+
return blockedOutcome(
|
|
1594
|
+
{
|
|
1595
|
+
taskId: attempt.taskId,
|
|
1596
|
+
requestId: attempt.requestId,
|
|
1597
|
+
phase: attempt.phase,
|
|
1598
|
+
},
|
|
1599
|
+
failure,
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
const subagentEndpoint: Readonly<SubagentEndpoint> | null =
|
|
1603
|
+
endpoint.kind === "custom" ? endpoint.endpoint : null;
|
|
1550
1604
|
record = this.registry.open(
|
|
1551
1605
|
runRequest.boundary,
|
|
1552
1606
|
identity,
|
|
1553
1607
|
workspaceRoot,
|
|
1554
1608
|
attempt,
|
|
1609
|
+
subagentEndpoint,
|
|
1555
1610
|
);
|
|
1556
1611
|
} else {
|
|
1557
1612
|
const existing = this.registry.get(key);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Per-role subagent endpoint resolution from the cadence env file.
|
|
2
|
+
// Three-tier whole-layer semantics: role layer, global layer, inherited.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { parseEnvFile } from "../skills/_shared/load-config.mjs";
|
|
8
|
+
import { ROLES } from "./contracts.ts";
|
|
9
|
+
|
|
10
|
+
export type SubagentRole = (typeof ROLES)[number];
|
|
11
|
+
|
|
12
|
+
export type SubagentDialect =
|
|
13
|
+
| "openai-completions"
|
|
14
|
+
| "openai-responses"
|
|
15
|
+
| "anthropic-messages";
|
|
16
|
+
|
|
17
|
+
const LAYER_FIELDS = [
|
|
18
|
+
"API_URL",
|
|
19
|
+
"API_KEY",
|
|
20
|
+
"MODEL",
|
|
21
|
+
"API",
|
|
22
|
+
"CONTEXT_WINDOW",
|
|
23
|
+
"MAX_TOKENS",
|
|
24
|
+
] as const;
|
|
25
|
+
type LayerField = (typeof LAYER_FIELDS)[number];
|
|
26
|
+
type Layer = Partial<Record<LayerField, string>>;
|
|
27
|
+
|
|
28
|
+
const GLOBAL_PREFIX = "SUBAGENT_";
|
|
29
|
+
const DEFAULT_DIALECT: SubagentDialect = "openai-completions";
|
|
30
|
+
const DIALECTS: readonly SubagentDialect[] = [
|
|
31
|
+
"openai-completions",
|
|
32
|
+
"openai-responses",
|
|
33
|
+
"anthropic-messages",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_CONTEXT_WINDOW = 256000;
|
|
37
|
+
export const DEFAULT_MAX_TOKENS = 128000;
|
|
38
|
+
|
|
39
|
+
export interface SubagentEndpoint {
|
|
40
|
+
readonly url: string;
|
|
41
|
+
readonly apiKey?: string;
|
|
42
|
+
readonly model: string;
|
|
43
|
+
readonly dialect: SubagentDialect;
|
|
44
|
+
readonly contextWindow: number;
|
|
45
|
+
readonly maxTokens: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type SubagentEndpointResolution =
|
|
49
|
+
| { kind: "inherited" }
|
|
50
|
+
| { kind: "custom"; endpoint: Readonly<SubagentEndpoint> }
|
|
51
|
+
| { kind: "invalid"; keys: string[]; file?: string };
|
|
52
|
+
|
|
53
|
+
export function describeInvalidSubagentEndpoint(
|
|
54
|
+
resolution: Extract<SubagentEndpointResolution, { kind: "invalid" }>,
|
|
55
|
+
): string {
|
|
56
|
+
return resolution.keys.length > 0
|
|
57
|
+
? `invalid subagent endpoint configuration keys: ${resolution.keys.join(", ")}`
|
|
58
|
+
: `invalid subagent endpoint configuration file: ${resolution.file ?? "unknown"}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function layerOf(values: Record<string, string>, prefix: string): Layer {
|
|
62
|
+
const layer: Layer = {};
|
|
63
|
+
for (const field of LAYER_FIELDS) {
|
|
64
|
+
const value = values[prefix + field];
|
|
65
|
+
if (value !== undefined && value !== "") layer[field] = value;
|
|
66
|
+
}
|
|
67
|
+
return layer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isCommitted(layer: Layer): boolean {
|
|
71
|
+
return LAYER_FIELDS.some((field) => layer[field] !== undefined);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isHttpUrl(value: string): boolean {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = new URL(value);
|
|
77
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isPositiveInteger(value: string): boolean {
|
|
84
|
+
const number = Number(value);
|
|
85
|
+
return /^[0-9]+$/.test(value) && Number.isSafeInteger(number) && number > 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function offendingKeys(prefix: string, layer: Layer): string[] {
|
|
89
|
+
const keys: string[] = [];
|
|
90
|
+
if (layer.API_URL === undefined || !isHttpUrl(layer.API_URL))
|
|
91
|
+
keys.push(`${prefix}API_URL`);
|
|
92
|
+
if (layer.MODEL === undefined) keys.push(`${prefix}MODEL`);
|
|
93
|
+
if (
|
|
94
|
+
layer.API !== undefined &&
|
|
95
|
+
!DIALECTS.includes(layer.API as SubagentDialect)
|
|
96
|
+
)
|
|
97
|
+
keys.push(`${prefix}API`);
|
|
98
|
+
if (
|
|
99
|
+
layer.CONTEXT_WINDOW !== undefined &&
|
|
100
|
+
!isPositiveInteger(layer.CONTEXT_WINDOW)
|
|
101
|
+
)
|
|
102
|
+
keys.push(`${prefix}CONTEXT_WINDOW`);
|
|
103
|
+
if (layer.MAX_TOKENS !== undefined && !isPositiveInteger(layer.MAX_TOKENS))
|
|
104
|
+
keys.push(`${prefix}MAX_TOKENS`);
|
|
105
|
+
return keys;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function selectEnvFile(cwd: string, home: string): string | null {
|
|
109
|
+
const projectPath = path.join(cwd, ".pi", "cadence", ".env");
|
|
110
|
+
if (existsSync(projectPath)) return projectPath;
|
|
111
|
+
const userPath = path.join(home, ".pi", "agent", "cadence", ".env");
|
|
112
|
+
if (existsSync(userPath)) return userPath;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveSubagentEndpoint(
|
|
117
|
+
role: string,
|
|
118
|
+
options?: { cwd?: string; home?: string },
|
|
119
|
+
): SubagentEndpointResolution {
|
|
120
|
+
if (!(ROLES as readonly string[]).includes(role))
|
|
121
|
+
return { kind: "invalid", keys: ["SUBAGENT_ROLE"] };
|
|
122
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
123
|
+
const home = options?.home ?? homedir();
|
|
124
|
+
const file = selectEnvFile(cwd, home);
|
|
125
|
+
if (!file) return { kind: "inherited" };
|
|
126
|
+
let values: Record<string, string>;
|
|
127
|
+
try {
|
|
128
|
+
values = parseEnvFile(readFileSync(file, "utf8"));
|
|
129
|
+
} catch {
|
|
130
|
+
return { kind: "invalid", keys: [], file };
|
|
131
|
+
}
|
|
132
|
+
const rolePrefix = `SUBAGENT_${role.toUpperCase().replaceAll("-", "_")}_`;
|
|
133
|
+
const roleLayer = layerOf(values, rolePrefix);
|
|
134
|
+
let layer: Layer | undefined = isCommitted(roleLayer) ? roleLayer : undefined;
|
|
135
|
+
if (!layer) {
|
|
136
|
+
const globalLayer = layerOf(values, GLOBAL_PREFIX);
|
|
137
|
+
if (isCommitted(globalLayer)) layer = globalLayer;
|
|
138
|
+
}
|
|
139
|
+
if (!layer) return { kind: "inherited" };
|
|
140
|
+
const prefix = layer === roleLayer ? rolePrefix : GLOBAL_PREFIX;
|
|
141
|
+
const keys = offendingKeys(prefix, layer);
|
|
142
|
+
if (keys.length > 0) return { kind: "invalid", keys, file };
|
|
143
|
+
const endpoint: Readonly<SubagentEndpoint> = Object.freeze({
|
|
144
|
+
url: layer.API_URL ?? "",
|
|
145
|
+
...(layer.API_KEY === undefined ? {} : { apiKey: layer.API_KEY }),
|
|
146
|
+
model: layer.MODEL ?? "",
|
|
147
|
+
dialect: (layer.API as SubagentDialect | undefined) ?? DEFAULT_DIALECT,
|
|
148
|
+
contextWindow:
|
|
149
|
+
layer.CONTEXT_WINDOW === undefined
|
|
150
|
+
? DEFAULT_CONTEXT_WINDOW
|
|
151
|
+
: Number(layer.CONTEXT_WINDOW),
|
|
152
|
+
maxTokens:
|
|
153
|
+
layer.MAX_TOKENS === undefined
|
|
154
|
+
? DEFAULT_MAX_TOKENS
|
|
155
|
+
: Number(layer.MAX_TOKENS),
|
|
156
|
+
});
|
|
157
|
+
return { kind: "custom", endpoint };
|
|
158
|
+
}
|
package/src/worker.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
TaskFailure,
|
|
9
9
|
} from "./contracts.ts";
|
|
10
10
|
import type { ConflictDeclaration } from "./scheduler.ts";
|
|
11
|
+
import type { SubagentEndpoint } from "./subagent-endpoint.ts";
|
|
11
12
|
|
|
12
13
|
export type TaskState =
|
|
13
14
|
| {
|
|
@@ -35,6 +36,7 @@ export interface TaskRecord {
|
|
|
35
36
|
key: string;
|
|
36
37
|
workspaceRoot: string;
|
|
37
38
|
workerIdentity: string;
|
|
39
|
+
subagentEndpoint: Readonly<SubagentEndpoint> | null;
|
|
38
40
|
boundary: TaskBoundary;
|
|
39
41
|
conflict: ConflictDeclaration;
|
|
40
42
|
state: TaskState;
|
|
@@ -118,6 +120,7 @@ export class WorkerRegistry {
|
|
|
118
120
|
identity: string,
|
|
119
121
|
workspaceRoot: string,
|
|
120
122
|
attempt: PhaseAttempt,
|
|
123
|
+
subagentEndpoint: Readonly<SubagentEndpoint> | null = null,
|
|
121
124
|
): TaskRecord {
|
|
122
125
|
const key = taskRecordKey(
|
|
123
126
|
workspaceRoot,
|
|
@@ -130,6 +133,10 @@ export class WorkerRegistry {
|
|
|
130
133
|
key,
|
|
131
134
|
workspaceRoot,
|
|
132
135
|
workerIdentity: identity,
|
|
136
|
+
subagentEndpoint:
|
|
137
|
+
subagentEndpoint === null
|
|
138
|
+
? null
|
|
139
|
+
: Object.freeze({ ...subagentEndpoint }),
|
|
133
140
|
boundary: storedBoundary,
|
|
134
141
|
conflict: taskConflictOf(storedBoundary),
|
|
135
142
|
state: { kind: "ready", phase: attempt.phase, launchIndex: 0 },
|