@akagilnc/pi-workflow-roles 0.1.2043 → 0.1.2052
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 +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/public-cli/main.js +489 -183
- package/extensions/role-runtime.ts +5 -0
- package/package.json +1 -1
- package/resources/engines/codex.md +32 -0
- package/resources/engines/cursor.md +32 -0
- package/resources/engines/kimi.md +31 -0
- package/src/oauth-keepalive.ts +216 -0
- package/src/package-resources/engine-material.ts +111 -0
- package/src/public-cli/cli.ts +168 -22
- package/src/public-cli/config.ts +167 -22
- package/src/public-cli/invocation.ts +7 -1
- package/src/public-cli/judge-run.ts +11 -1
- package/src/public-cli/option-definitions.ts +14 -0
- package/src/role-runtime.ts +14 -0
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { loadCanonicalSkillBinding as loadHomeCanonicalSkillBinding } from "../src/canonical-skill-binding.ts";
|
|
34
34
|
import { loadPackagedCanonicalSkillBinding } from "../src/package-resources/method-skill-binding.ts";
|
|
35
35
|
import { JUDGE_OUTPUT_TOOL_NAME } from "../src/package-contracts/judge-output.ts";
|
|
36
|
+
import { readOAuthKeepaliveProviders } from "../src/oauth-keepalive.ts";
|
|
36
37
|
import {
|
|
37
38
|
createProductionMergerGitState,
|
|
38
39
|
createRoleRuntimeExtension,
|
|
@@ -244,7 +245,11 @@ export default function roleRuntime(pi: ExtensionAPI): void {
|
|
|
244
245
|
const reviewerAgent = createReviewerAgentRunner();
|
|
245
246
|
registerNavigatorModelCommand(pi);
|
|
246
247
|
const navigatorSessionFactory = createNativeNavigatorSessionFactory();
|
|
248
|
+
// #351: static provider list from extension setting (default ["kimi-coding"]).
|
|
249
|
+
// Production root is the sole reader; keepalive never auto-detects providers.
|
|
250
|
+
const oauthKeepaliveProviders = readOAuthKeepaliveProviders();
|
|
247
251
|
createRoleRuntimeExtension({
|
|
252
|
+
oauthKeepalive: { providers: oauthKeepaliveProviders },
|
|
248
253
|
loadJudgeSoul: () => readFile(judgeSoulPath, "utf8"),
|
|
249
254
|
loadFixerSoul: () => readFile(fixerSoulPath, "utf8"),
|
|
250
255
|
loadFixPacket: (path) => readFile(path, "utf8"),
|
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# codex engine method material
|
|
2
|
+
|
|
3
|
+
This file is packaged method material for the optional `codex` labor engine.
|
|
4
|
+
When a role run selects this engine, read these bytes and follow the local CLI's
|
|
5
|
+
actual interface for the labor detour. Return the labor result to the same role
|
|
6
|
+
session so typed submission stays on the existing in-session path.
|
|
7
|
+
|
|
8
|
+
Material is data for the model, not a code contract. Do not invent package flags.
|
|
9
|
+
|
|
10
|
+
## Invocation examples (local Codex CLI)
|
|
11
|
+
|
|
12
|
+
Non-interactive labor prompt (working root = role project):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
codex exec -C "$PROJECT_ROOT" "YOUR_LABOR_PROMPT"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Write the last agent message to a file, then bring that text back into the role
|
|
19
|
+
session:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
codex exec -C "$PROJECT_ROOT" -o /tmp/codex-labor-last.txt "YOUR_LABOR_PROMPT"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
JSON event stream when the host needs machine-readable progress:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
codex exec --json -C "$PROJECT_ROOT" "YOUR_LABOR_PROMPT"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Prefer the installed `codex` binary's own `--help` / `codex exec --help` over
|
|
32
|
+
any remembered flag set. Do not wrap this engine behind `ak-role` flags.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# cursor engine method material
|
|
2
|
+
|
|
3
|
+
This file is packaged method material for the optional `cursor` labor engine.
|
|
4
|
+
When a role run selects this engine, read these bytes and follow the local CLI's
|
|
5
|
+
actual interface for the labor detour. Return the labor result to the same role
|
|
6
|
+
session so typed submission stays on the existing in-session path.
|
|
7
|
+
|
|
8
|
+
Material is data for the model, not a code contract. Do not invent package flags.
|
|
9
|
+
|
|
10
|
+
## Invocation examples (local Cursor Agent CLI)
|
|
11
|
+
|
|
12
|
+
The machine entrypoint is `agent` (also exposed as `cursor-agent`). Run from the
|
|
13
|
+
role project root. Non-interactive print mode:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
agent -p --force --output-format text "YOUR_LABOR_PROMPT"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Choose a model explicitly when the seat needs a known Cursor model id:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
agent -p --force --model grok-4.5 --output-format text "YOUR_LABOR_PROMPT"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Stream JSON events for long labor:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
agent -p --force --output-format stream-json "YOUR_LABOR_PROMPT"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Prefer `agent --help` on the host over any remembered flag set. Do not wrap this
|
|
32
|
+
engine behind `ak-role` flags.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# kimi engine method material
|
|
2
|
+
|
|
3
|
+
This file is packaged method material for the optional `kimi` labor engine.
|
|
4
|
+
When a role run selects this engine, read these bytes and follow the local CLI's
|
|
5
|
+
actual interface for the labor detour. Return the labor result to the same role
|
|
6
|
+
session so typed submission stays on the existing in-session path.
|
|
7
|
+
|
|
8
|
+
Material is data for the model, not a code contract. Do not invent package flags.
|
|
9
|
+
|
|
10
|
+
## Invocation examples (local Kimi Code CLI)
|
|
11
|
+
|
|
12
|
+
One-shot non-interactive prompt from the role project root:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
kimi --yolo -p "YOUR_LABOR_PROMPT"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Pin a model alias when needed:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
kimi --yolo -m <model-alias> -p "YOUR_LABOR_PROMPT"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Machine-readable prompt mode:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
kimi --yolo --output-format text -p "YOUR_LABOR_PROMPT"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Prefer `kimi --help` on the host over any remembered flag set. Do not wrap this
|
|
31
|
+
engine behind `ak-role` flags.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension-side OAuth credential keepalive (#351).
|
|
3
|
+
*
|
|
4
|
+
* Periodically calls the public `ctx.modelRegistry.refresh` facade so expired
|
|
5
|
+
* OAuth tokens (e.g. kimi-coding 900s TTL) are renewed during long sessions.
|
|
6
|
+
* Does not read auth.json, touch pi-ai private APIs, or reuse the 183s idle gate.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_OAUTH_KEEPALIVE_PROVIDERS = ["kimi-coding"] as const;
|
|
14
|
+
|
|
15
|
+
export const OAUTH_KEEPALIVE_INTERVAL_MS = 60_000;
|
|
16
|
+
|
|
17
|
+
/** Persistent extension setting filename under PI_CODING_AGENT_DIR / ~/.pi/agent. */
|
|
18
|
+
export const OAUTH_KEEPALIVE_SETTING_FILENAME = "oauth-keepalive.json";
|
|
19
|
+
|
|
20
|
+
/** Path to the static provider-list extension setting (default ["kimi-coding"]). */
|
|
21
|
+
export function oauthKeepaliveSettingPath(): string {
|
|
22
|
+
return join(
|
|
23
|
+
process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"),
|
|
24
|
+
OAUTH_KEEPALIVE_SETTING_FILENAME,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isExactRecord(value: unknown): value is Record<string, unknown> {
|
|
29
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read the static keepalive provider list from the extension setting file.
|
|
34
|
+
* Missing file → default ["kimi-coding"]. Malformed content fails closed.
|
|
35
|
+
* Production extension root is the sole caller that feeds this into keepalive.
|
|
36
|
+
*/
|
|
37
|
+
export function readOAuthKeepaliveProviders(
|
|
38
|
+
path = oauthKeepaliveSettingPath(),
|
|
39
|
+
): readonly string[] {
|
|
40
|
+
try {
|
|
41
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
42
|
+
if (!isExactRecord(raw) || !Array.isArray(raw.providers)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"OAuth keepalive setting is malformed: expected { providers: string[] }",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const providers = raw.providers.map((id, index) => {
|
|
48
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`OAuth keepalive setting providers[${index}] must be a non-empty string`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
// Accept via trim-non-empty; return the normalized id so refresh can hit real providers.
|
|
54
|
+
return id.trim();
|
|
55
|
+
});
|
|
56
|
+
return Object.freeze(providers);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Absent optional setting uses the documented default; all other causes propagate.
|
|
59
|
+
if (
|
|
60
|
+
error instanceof Error &&
|
|
61
|
+
"code" in error &&
|
|
62
|
+
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
63
|
+
) {
|
|
64
|
+
return DEFAULT_OAUTH_KEEPALIVE_PROVIDERS;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Injected interval scheduler (same shape as TrajectoryScheduler). */
|
|
71
|
+
export type OAuthKeepaliveScheduler = {
|
|
72
|
+
/** Schedule `tick` every `ms` milliseconds; return a cancel function. */
|
|
73
|
+
every: (ms: number, tick: () => void) => () => void;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type OAuthKeepaliveOptions = {
|
|
77
|
+
/** Static provider id list; frozen at construction. Default: ["kimi-coding"]. */
|
|
78
|
+
providers?: readonly string[];
|
|
79
|
+
/** Interval between ticks in ms. Default: 60_000. First tick is after one interval. */
|
|
80
|
+
intervalMs?: number;
|
|
81
|
+
/** Injectable scheduler for tests. Default: setInterval + unref. */
|
|
82
|
+
scheduler?: OAuthKeepaliveScheduler;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Minimal context surface used by keepalive (public extension facade only). */
|
|
86
|
+
export type OAuthKeepaliveContext = {
|
|
87
|
+
modelRegistry: {
|
|
88
|
+
refresh: (options?: {
|
|
89
|
+
allowNetwork?: boolean;
|
|
90
|
+
providers?: readonly string[];
|
|
91
|
+
force?: boolean;
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
}) => Promise<{
|
|
94
|
+
aborted: boolean;
|
|
95
|
+
errors: ReadonlyMap<string, Error>;
|
|
96
|
+
}>;
|
|
97
|
+
};
|
|
98
|
+
ui?: {
|
|
99
|
+
notify?: (message: string, type?: "info" | "warning" | "error") => void;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type OAuthKeepalive = {
|
|
104
|
+
start(ctx: OAuthKeepaliveContext): void;
|
|
105
|
+
stop(): void;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const defaultScheduler: OAuthKeepaliveScheduler = {
|
|
109
|
+
every(ms, tick) {
|
|
110
|
+
const timer = setInterval(tick, ms);
|
|
111
|
+
// Allow the process to exit if a caller forgets stop (same discipline as trajectory).
|
|
112
|
+
timer.unref?.();
|
|
113
|
+
return () => clearInterval(timer);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function errorClassName(error: unknown): string {
|
|
118
|
+
if (error instanceof Error && error.name.trim() !== "") return error.name;
|
|
119
|
+
if (error !== null && typeof error === "object" && "name" in error) {
|
|
120
|
+
const name = (error as { name?: unknown }).name;
|
|
121
|
+
if (typeof name === "string" && name.trim() !== "") return name;
|
|
122
|
+
}
|
|
123
|
+
return "Error";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatWarning(providerId: string, error: unknown): string {
|
|
127
|
+
const cls = errorClassName(error);
|
|
128
|
+
const detail =
|
|
129
|
+
error instanceof Error
|
|
130
|
+
? error.message
|
|
131
|
+
: typeof error === "string"
|
|
132
|
+
? error
|
|
133
|
+
: String(error);
|
|
134
|
+
return `oauth-keepalive: refresh failed for provider ${providerId} (${cls}): ${detail}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function emitWarning(ctx: OAuthKeepaliveContext, providerId: string, error: unknown): void {
|
|
138
|
+
const text = formatWarning(providerId, error);
|
|
139
|
+
// Exactly one visible surface per failure: prefer ui.notify when present;
|
|
140
|
+
// console.warn only when notify is unavailable (print/json / headless)
|
|
141
|
+
// or when notify itself throws (still one surface; carry the notify cause).
|
|
142
|
+
const notify = ctx.ui?.notify;
|
|
143
|
+
if (typeof notify === "function") {
|
|
144
|
+
try {
|
|
145
|
+
notify(text, "warning");
|
|
146
|
+
} catch (notifyError) {
|
|
147
|
+
// Notify threw: fall back once so refresh failure stays visible.
|
|
148
|
+
console.warn(text, notifyError);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
console.warn(text);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Session-lifecycle owner for periodic OAuth refresh via the public modelRegistry facade.
|
|
157
|
+
* Single-flight: overlapping ticks are skipped. stop() cancels the interval and aborts in-flight work.
|
|
158
|
+
*/
|
|
159
|
+
export function createOAuthKeepalive(options: OAuthKeepaliveOptions = {}): OAuthKeepalive {
|
|
160
|
+
const providers = Object.freeze(
|
|
161
|
+
(options.providers ?? DEFAULT_OAUTH_KEEPALIVE_PROVIDERS).map((id) => id),
|
|
162
|
+
) as readonly string[];
|
|
163
|
+
const intervalMs = options.intervalMs ?? OAUTH_KEEPALIVE_INTERVAL_MS;
|
|
164
|
+
const scheduler = options.scheduler ?? defaultScheduler;
|
|
165
|
+
|
|
166
|
+
let cancel: (() => void) | undefined;
|
|
167
|
+
let abortController: AbortController | undefined;
|
|
168
|
+
/** Bare single-flight gate; survives stop/start until the in-flight call settles. */
|
|
169
|
+
let inFlight = false;
|
|
170
|
+
|
|
171
|
+
const stop = (): void => {
|
|
172
|
+
cancel?.();
|
|
173
|
+
cancel = undefined;
|
|
174
|
+
abortController?.abort();
|
|
175
|
+
abortController = undefined;
|
|
176
|
+
// Do not clear unsettled inFlight — single-flight is continuous across stop/start.
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const runTick = async (ctx: OAuthKeepaliveContext): Promise<void> => {
|
|
180
|
+
if (inFlight) return;
|
|
181
|
+
const ac = abortController;
|
|
182
|
+
if (ac === undefined || ac.signal.aborted) return;
|
|
183
|
+
inFlight = true;
|
|
184
|
+
try {
|
|
185
|
+
const result = await ctx.modelRegistry.refresh({
|
|
186
|
+
providers: [...providers],
|
|
187
|
+
allowNetwork: true,
|
|
188
|
+
signal: ac.signal,
|
|
189
|
+
});
|
|
190
|
+
if (ac.signal.aborted || result.aborted) return;
|
|
191
|
+
for (const [providerId, error] of result.errors) {
|
|
192
|
+
emitWarning(ctx, providerId, error);
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (ac.signal.aborted) return;
|
|
196
|
+
// Whole-call failure: one warning per configured provider (ticket error surface).
|
|
197
|
+
for (const providerId of providers) {
|
|
198
|
+
emitWarning(ctx, providerId, error);
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
inFlight = false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
start(ctx) {
|
|
207
|
+
// Reload / re-start: tear down any prior interval before hanging a new one.
|
|
208
|
+
stop();
|
|
209
|
+
abortController = new AbortController();
|
|
210
|
+
cancel = scheduler.every(intervalMs, () => {
|
|
211
|
+
void runTick(ctx);
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
stop,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packaged engine method-material seam (#356 T1 / ADR 0069).
|
|
3
|
+
* Legal engine names = directory stems under resources/engines/ — no code whitelist.
|
|
4
|
+
* Material body is data for the LLM, not a code contract.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const ENGINE_MATERIAL_RELATIVE_ROOT = "resources/engines" as const;
|
|
10
|
+
|
|
11
|
+
export type EngineSessionMaterial = Readonly<{
|
|
12
|
+
name: string;
|
|
13
|
+
materialPath: string;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
/** Non-empty, trimmed, no path separators or traversal. */
|
|
17
|
+
export function isEngineNameSyntax(name: string): boolean {
|
|
18
|
+
if (typeof name !== "string") return false;
|
|
19
|
+
if (name.length === 0 || name.trim() !== name) return false;
|
|
20
|
+
if (name === "." || name === "..") return false;
|
|
21
|
+
if (name.includes("/") || name.includes("\\") || name.includes("\0")) return false;
|
|
22
|
+
if (name.includes("..")) return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function engineMaterialRelativeDirectory(): string {
|
|
27
|
+
return ENGINE_MATERIAL_RELATIVE_ROOT;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveEngineMaterialDirectory(packageRoot: string): string {
|
|
31
|
+
return join(packageRoot, ENGINE_MATERIAL_RELATIVE_ROOT);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Enumerate legal engine names from packaged material files.
|
|
36
|
+
* Only `*.md` stems that pass name syntax are legal.
|
|
37
|
+
*/
|
|
38
|
+
export function listEngineMaterialNames(packageRoot: string): readonly string[] {
|
|
39
|
+
const dir = resolveEngineMaterialDirectory(packageRoot);
|
|
40
|
+
if (!existsSync(dir)) return Object.freeze([]);
|
|
41
|
+
const names = readdirSync(dir)
|
|
42
|
+
.filter((entry) => entry.endsWith(".md"))
|
|
43
|
+
.map((entry) => entry.slice(0, -".md".length))
|
|
44
|
+
.filter((stem) => isEngineNameSyntax(stem))
|
|
45
|
+
.sort();
|
|
46
|
+
return Object.freeze([...names]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveEngineMaterialPath(
|
|
50
|
+
packageRoot: string,
|
|
51
|
+
name: string,
|
|
52
|
+
): string {
|
|
53
|
+
const legal = assertLegalEngineName(packageRoot, name);
|
|
54
|
+
return join(resolveEngineMaterialDirectory(packageRoot), `${legal}.md`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Assert `name` is a legal engine material stem under packageRoot.
|
|
59
|
+
* Returns the canonical name on success; throws Error on illegal.
|
|
60
|
+
*/
|
|
61
|
+
export function assertLegalEngineName(packageRoot: string, name: string): string {
|
|
62
|
+
if (!isEngineNameSyntax(name)) {
|
|
63
|
+
throw new Error(`illegal engine name: ${name}`);
|
|
64
|
+
}
|
|
65
|
+
const legal = listEngineMaterialNames(packageRoot);
|
|
66
|
+
if (!legal.includes(name)) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`unknown engine: ${name} (known: ${legal.length === 0 ? "(none)" : legal.join(", ")})`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return name;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve optional engine options into session material coordinates.
|
|
76
|
+
* No engine → undefined (caller keeps default prompt bytes).
|
|
77
|
+
*/
|
|
78
|
+
export function engineSessionMaterialFromOptions(options: {
|
|
79
|
+
engine?: string;
|
|
80
|
+
packageRoot?: string;
|
|
81
|
+
}): EngineSessionMaterial | undefined {
|
|
82
|
+
if (options.engine === undefined) return undefined;
|
|
83
|
+
if (options.packageRoot === undefined || options.packageRoot.trim() === "") {
|
|
84
|
+
throw new Error("packageRoot is required when engine is configured");
|
|
85
|
+
}
|
|
86
|
+
const name = assertLegalEngineName(options.packageRoot, options.engine);
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
name,
|
|
89
|
+
materialPath: resolveEngineMaterialPath(options.packageRoot, name),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Append engine method-material path delivery to session initial material lines.
|
|
95
|
+
* No material → identity copy (byte-stable when joined the same way).
|
|
96
|
+
* Delivers engine name + absolute material path only — never material body.
|
|
97
|
+
*/
|
|
98
|
+
export function appendEngineSessionMaterial(
|
|
99
|
+
lines: readonly string[],
|
|
100
|
+
engineMaterial?: EngineSessionMaterial,
|
|
101
|
+
): string[] {
|
|
102
|
+
if (engineMaterial === undefined) {
|
|
103
|
+
return [...lines];
|
|
104
|
+
}
|
|
105
|
+
const out = [...lines];
|
|
106
|
+
out.push("");
|
|
107
|
+
out.push("Engine method material (read these bytes and follow them):");
|
|
108
|
+
out.push(`- engine: ${engineMaterial.name}`);
|
|
109
|
+
out.push(`- ${engineMaterial.materialPath}`);
|
|
110
|
+
return out;
|
|
111
|
+
}
|