@howarewoo/pi-subagents 0.0.0-stage → 0.1.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.
@@ -0,0 +1,77 @@
1
+ import { ModelRuntime, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ InMemoryCredentialStore,
4
+ type Provider,
5
+ type Model,
6
+ type Api,
7
+ } from "@earendil-works/pi-ai";
8
+ import { join } from "node:path";
9
+
10
+ /** Resolve auth only through the parent's public registry; never copy credentials to child storage. */
11
+ export async function parentModelRuntime(
12
+ ctx: Pick<ExtensionContext, "model" | "modelRegistry" | "scopedModels">,
13
+ agentDir: string,
14
+ onRequest: () => void = () => {},
15
+ additionalModels: Model<Api>[] = [],
16
+ ): Promise<ModelRuntime> {
17
+ const { model, modelRegistry: registry } = ctx;
18
+ if (!model) throw new Error("Delegation requires a selected parent model");
19
+ if (model.api === "pi-virtual")
20
+ throw new Error(
21
+ "Selected virtual model cannot be faithfully routed through the public child adapter",
22
+ );
23
+ if (
24
+ ctx.scopedModels.length &&
25
+ !ctx.scopedModels.some(
26
+ (entry) => entry.model.provider === model.provider && entry.model.id === model.id,
27
+ )
28
+ )
29
+ throw new Error("Selected model is outside the parent's scoped models");
30
+ if (!registry.getProvider(model.provider) || !registry.find(model.provider, model.id))
31
+ throw new Error(
32
+ "Selected model has no public provider adapter (virtual-only models are not supported)",
33
+ );
34
+ if (!registry.hasConfiguredAuth(model))
35
+ throw new Error(`No configured parent authentication for ${model.provider}`);
36
+ const auth = await registry.getApiKeyAndHeaders(model);
37
+ if (!auth.ok) throw new Error(auth.error);
38
+ const runtime = await ModelRuntime.create({
39
+ credentials: new InMemoryCredentialStore(),
40
+ modelsPath: null,
41
+ modelsStorePath: join(agentDir, "models-cache.json"),
42
+ refreshOnCreate: false,
43
+ allowModelNetwork: false,
44
+ });
45
+ // Child runtime performs no independent auth discovery. Each request is forwarded to the
46
+ // parent registry, which owns configured providers, OAuth refresh and request-time auth.
47
+ const models = [model, ...additionalModels];
48
+ for (const providerId of new Set(models.map((item) => item.provider))) {
49
+ const provider: Provider = {
50
+ id: providerId,
51
+ name: `Parent selected ${providerId}`,
52
+ auth: {
53
+ apiKey: {
54
+ name: "Parent registry",
55
+ async resolve() {
56
+ return { auth: {}, source: "parent registry delegation" };
57
+ },
58
+ },
59
+ },
60
+ getModels: () => models.filter((item) => item.provider === providerId),
61
+ stream: (selected, context, options) => {
62
+ onRequest();
63
+ return registry.stream(registry.find(selected.provider, selected.id)!, context, options);
64
+ },
65
+ streamSimple: (selected, context, options) => {
66
+ onRequest();
67
+ return registry.streamSimple(
68
+ registry.find(selected.provider, selected.id)!,
69
+ context,
70
+ options,
71
+ );
72
+ },
73
+ };
74
+ runtime.registerNativeProvider(provider);
75
+ }
76
+ return runtime;
77
+ }
@@ -0,0 +1,100 @@
1
+ import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
2
+ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai/compat";
3
+
4
+ export const EFFORTS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
5
+ export type ModelSelection = Readonly<{
6
+ provider: string;
7
+ id: string;
8
+ effort: (typeof EFFORTS)[number];
9
+ }>;
10
+ export type ModelPolicy = Readonly<{
11
+ models?: readonly ModelSelection[];
12
+ model?: ModelSelection;
13
+ prewalk?: ModelSelection;
14
+ }>;
15
+ export function modelSelection(value: unknown): ModelSelection {
16
+ if (!value || typeof value !== "object" || Array.isArray(value))
17
+ throw new Error("Unsupported model selection: expected exact provider, id and effort");
18
+ const item = value as Record<string, unknown>;
19
+ if (
20
+ Object.keys(item).some((key) => !["provider", "id", "effort"].includes(key)) ||
21
+ typeof item.provider !== "string" ||
22
+ !item.provider.trim() ||
23
+ typeof item.id !== "string" ||
24
+ !item.id.trim() ||
25
+ !EFFORTS.includes(item.effort as ModelSelection["effort"])
26
+ )
27
+ throw new Error("Unsupported model selection: expected exact provider, id and effort");
28
+ return { provider: item.provider, id: item.id, effort: item.effort as ModelSelection["effort"] };
29
+ }
30
+ export function sameSelection(a: ModelSelection, b: ModelSelection): boolean {
31
+ return a.provider === b.provider && a.id === b.id && a.effort === b.effort;
32
+ }
33
+ export function allowsModel(policy: ModelPolicy, selection: ModelSelection): boolean {
34
+ return (
35
+ policy.models === undefined || policy.models.some((item) => sameSelection(item, selection))
36
+ );
37
+ }
38
+ export function narrowModelPolicy(
39
+ base: ModelPolicy,
40
+ value: Record<string, unknown>,
41
+ operator: boolean,
42
+ ): ModelPolicy {
43
+ let models = base.models;
44
+ if (value.models !== undefined) {
45
+ if (!Array.isArray(value.models) || value.models.length > 64)
46
+ throw new Error("models must be a bounded exact selection array");
47
+ models = value.models.map(modelSelection);
48
+ if (
49
+ !operator &&
50
+ models.some((item) => !base.models?.some((allowed) => sameSelection(item, allowed)))
51
+ )
52
+ throw new Error("Cannot mint model grants");
53
+ }
54
+ const policy: ModelPolicy = {
55
+ models,
56
+ model: value.model === undefined ? base.model : modelSelection(value.model),
57
+ prewalk: value.prewalk === undefined ? base.prewalk : modelSelection(value.prewalk),
58
+ };
59
+ for (const selection of [policy.model, policy.prewalk])
60
+ if (selection && !models?.some((item) => sameSelection(item, selection)))
61
+ throw new Error("Model and prewalk selections require an exact operator model grant");
62
+ if (
63
+ !operator &&
64
+ policy.prewalk &&
65
+ (!base.prewalk || !sameSelection(policy.prewalk, base.prewalk))
66
+ )
67
+ throw new Error("Cannot mint prewalk grants");
68
+ return policy;
69
+ }
70
+ export function validateModelSelection(
71
+ ctx: Pick<ExtensionToolContext, "modelRegistry" | "scopedModels">,
72
+ selection: ModelSelection,
73
+ ) {
74
+ const registry = ctx.modelRegistry;
75
+ const model = registry.find(selection.provider, selection.id);
76
+ if (!model || model.api === "pi-virtual" || !registry.getProvider(model.provider))
77
+ throw new Error("Unsupported or unknown physical model selection");
78
+ if (
79
+ !registry
80
+ .getAvailable()
81
+ .some((item) => item.provider === model.provider && item.id === model.id) ||
82
+ !registry.hasConfiguredAuth(model)
83
+ )
84
+ throw new Error("Model selection unavailable or missing configured parent authentication");
85
+ if (
86
+ ctx.scopedModels.length &&
87
+ !ctx.scopedModels.some(
88
+ (item) =>
89
+ item.model.provider === model.provider &&
90
+ item.model.id === model.id &&
91
+ (item.thinkingLevel === undefined || item.thinkingLevel === selection.effort),
92
+ )
93
+ )
94
+ throw new Error("Model selection outside parent scoped models/effort");
95
+ if (![model.contextWindow, model.maxTokens].every((limit) => Number.isFinite(limit) && limit > 0))
96
+ throw new Error("Model selection requires known finite context and output limits");
97
+ if (!getSupportedThinkingLevels(model).includes(selection.effort))
98
+ throw new Error("Unsupported model effort; silent coercion is denied");
99
+ return model;
100
+ }
@@ -0,0 +1,110 @@
1
+ import { compile, type Validator } from "@earendil-works/pi-schema";
2
+ import { Type } from "typebox";
3
+ import { link, unlink, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+
7
+ /** Deliberately finite JSON Schema subset: no references, regexes or executable extensions. */
8
+ export function resultValidator(schema: unknown): Validator {
9
+ if (JSON.stringify(schema)?.length > 16384)
10
+ throw new Error("Result schema exceeds 16384 characters");
11
+ let nodes = 0;
12
+ function visit(value: unknown, depth: number): void {
13
+ if (++nodes > 256 || depth > 16) throw new Error("Result schema complexity limit");
14
+ if (typeof value === "boolean") return;
15
+ if (!value || typeof value !== "object" || Array.isArray(value))
16
+ throw new Error("Result schema must be an object or boolean");
17
+ const node = value as Record<string, unknown>;
18
+ for (const [key, item] of Object.entries(node)) {
19
+ if (key === "properties") {
20
+ if (!item || typeof item !== "object" || Array.isArray(item))
21
+ throw new Error("Schema properties must be an object");
22
+ for (const child of Object.values(item)) visit(child, depth + 1);
23
+ } else if (key === "items" || key === "additionalProperties") visit(item, depth + 1);
24
+ else if (key === "type") {
25
+ if (
26
+ !["object", "array", "string", "number", "integer", "boolean", "null"].includes(
27
+ item as string,
28
+ )
29
+ )
30
+ throw new Error("Unsupported result schema type");
31
+ } else if (["title", "description"].includes(key)) {
32
+ if (typeof item !== "string") throw new Error(`Schema ${key} must be text`);
33
+ } else if (key === "required") {
34
+ if (
35
+ !Array.isArray(item) ||
36
+ !item.every((v) => typeof v === "string") ||
37
+ new Set(item).size !== item.length
38
+ )
39
+ throw new Error("Schema required must contain distinct strings");
40
+ } else if (key === "enum") {
41
+ if (!Array.isArray(item) || !item.length) throw new Error("Schema enum must be nonempty");
42
+ } else if (key === "const") {
43
+ /* JSON literal; never interpreted as a schema */
44
+ } else if (
45
+ [
46
+ "minimum",
47
+ "maximum",
48
+ "exclusiveMinimum",
49
+ "exclusiveMaximum",
50
+ "multipleOf",
51
+ "minLength",
52
+ "maxLength",
53
+ "minItems",
54
+ "maxItems",
55
+ "minProperties",
56
+ "maxProperties",
57
+ ].includes(key)
58
+ ) {
59
+ if (
60
+ typeof item !== "number" ||
61
+ !Number.isFinite(item) ||
62
+ (key === "multipleOf" && item <= 0) ||
63
+ (((key.startsWith("min") && key !== "minimum") ||
64
+ (key.startsWith("max") && key !== "maximum")) &&
65
+ (!Number.isSafeInteger(item) || item < 0))
66
+ )
67
+ throw new Error(`Invalid schema bound: ${key}`);
68
+ } else throw new Error(`Unsupported result schema keyword: ${key}`);
69
+ }
70
+ }
71
+ visit(schema, 0);
72
+ return compile(schema as object | boolean);
73
+ }
74
+
75
+ export const submissionParameters = Type.Object(
76
+ { value: Type.Unknown() },
77
+ { additionalProperties: false },
78
+ );
79
+ export type StructuredOutcome = {
80
+ validation: "awaiting" | "invalid" | "submitted";
81
+ attempts: number;
82
+ errors: string[];
83
+ value?: unknown;
84
+ finalized: boolean;
85
+ };
86
+
87
+ /** Atomic, exclusive publication; callers supply only fixed names inside a newly owned task directory. */
88
+ export async function saveOutcomeArtifact(
89
+ directory: string,
90
+ name: "result.txt" | "structured.json",
91
+ content: string,
92
+ ): Promise<string> {
93
+ if (name !== "result.txt" && name !== "structured.json")
94
+ throw new Error("Unsupported outcome artifact filename");
95
+ const target = join(directory, name);
96
+ const temporary = join(directory, `.outcome-${randomUUID()}`);
97
+ await writeFile(temporary, content, { flag: "wx", mode: 0o600 });
98
+ try {
99
+ await link(temporary, target);
100
+ } finally {
101
+ await unlink(temporary);
102
+ }
103
+ return target;
104
+ }
105
+
106
+ export function clippedText(text: string, maximum = 2048): string {
107
+ return text.length > maximum
108
+ ? text.slice(0, maximum) + "\n[Preview truncated; full output in artifact/transcript]"
109
+ : text;
110
+ }