@howarewoo/pi-subagents 0.0.0-stage → 0.2.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/CHANGELOG.md +63 -0
- package/LICENSE +21 -0
- package/README.md +499 -2
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/docs/advanced-policies.md +78 -0
- package/docs/delegation-contract.md +297 -0
- package/docs/images/subagent-hub.svg +1 -0
- package/docs/images/subagent-profiles.svg +1 -0
- package/docs/images/subagent-widget.svg +1 -0
- package/docs/parity-inventory.md +330 -0
- package/docs/provenance.md +13 -0
- package/docs/releases.md +236 -0
- package/docs/verification.md +430 -0
- package/package.json +64 -4
- package/src/capabilities.ts +39 -0
- package/src/child-execution.ts +459 -0
- package/src/coordinator.ts +1376 -0
- package/src/delegation-types.ts +111 -0
- package/src/hub.ts +612 -0
- package/src/model-adapter.ts +77 -0
- package/src/model-policy.ts +100 -0
- package/src/outcomes.ts +110 -0
- package/src/pi-extended-subagents.ts +673 -0
- package/src/profile-viewer.ts +246 -0
- package/src/profiles.ts +225 -0
- package/src/scheduling.ts +73 -0
- package/src/subagent-widget.ts +115 -0
- package/src/supported-host.ts +26 -0
- package/src/task-input.ts +74 -0
- package/src/task-presentation.ts +165 -0
- package/src/thread.ts +268 -0
- package/src/tool-schemas.ts +165 -0
- package/src/workspace.ts +46 -0
|
@@ -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
|
+
}
|
package/src/outcomes.ts
ADDED
|
@@ -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
|
+
}
|