pi-ultracode 0.1.2 → 0.3.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 +47 -42
- package/examples/workflows/loop-until-dry-bugs.workflow.js +16 -6
- package/extensions/ultracode.ts +0 -1
- package/package.json +7 -7
- package/src/commands.ts +19 -23
- package/src/index.ts +1 -1
- package/src/mode.ts +6 -40
- package/src/prompts.ts +18 -28
- package/src/workflow/admission.ts +318 -0
- package/src/workflow/agent-runner.ts +12 -0
- package/src/workflow/display.ts +17 -13
- package/src/workflow/journal.ts +0 -0
- package/src/workflow/json-schema.ts +39 -17
- package/src/workflow/leases.ts +60 -0
- package/src/workflow/parser.ts +809 -5
- package/src/workflow/registry.ts +39 -22
- package/src/workflow/run-artifacts.ts +231 -0
- package/src/workflow/run-details.ts +106 -28
- package/src/workflow/runtime.ts +640 -303
- package/src/workflow/script-executor.ts +317 -0
- package/src/workflow/script-worker.mjs +678 -0
- package/src/workflow/structured-output.ts +2 -0
- package/src/workflow/tool.ts +207 -74
- package/src/workflow/value-limits.ts +363 -0
- package/src/workflow/worktree.ts +5 -3
- package/types/workflow.d.ts +60 -22
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
* `agent(prompt, { schema })`) into a TypeBox `TSchema` so Pi can both validate
|
|
4
4
|
* the subagent's structured output and serialize a faithful tool schema to the model.
|
|
5
5
|
*
|
|
6
|
-
* Covers the
|
|
7
|
-
*
|
|
6
|
+
* Covers the explicitly validated workflow JSON Schema subset. Unsupported
|
|
7
|
+
* keywords are rejected before conversion rather than silently weakened.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Type, type TSchema } from "typebox";
|
|
11
|
+
import { assertWorkflowSchemaLimit } from "./value-limits.ts";
|
|
11
12
|
|
|
12
13
|
type Json = Record<string, any>;
|
|
13
14
|
|
|
14
15
|
export function jsonSchemaToTypeBox(schema: unknown): TSchema {
|
|
16
|
+
assertWorkflowSchemaLimit(schema);
|
|
17
|
+
return convertJsonSchema(schema);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function convertJsonSchema(schema: unknown): TSchema {
|
|
15
21
|
if (!schema || typeof schema !== "object") {
|
|
16
22
|
// No constraints -> accept anything.
|
|
17
23
|
return Type.Unsafe<unknown>({});
|
|
@@ -21,27 +27,29 @@ export function jsonSchemaToTypeBox(schema: unknown): TSchema {
|
|
|
21
27
|
|
|
22
28
|
// Composite keywords first.
|
|
23
29
|
if (Array.isArray(node.enum)) {
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
return withAnnotations(Type.Unsafe<unknown>({
|
|
31
|
+
...(node.type !== undefined ? { type: node.type } : {}),
|
|
32
|
+
enum: node.enum,
|
|
33
|
+
}), annotations);
|
|
26
34
|
}
|
|
27
35
|
if ("const" in node) {
|
|
28
|
-
return withAnnotations(Type.
|
|
36
|
+
return withAnnotations(Type.Unsafe<unknown>({
|
|
37
|
+
...(node.type !== undefined ? { type: node.type } : {}),
|
|
38
|
+
const: node.const,
|
|
39
|
+
}), annotations);
|
|
29
40
|
}
|
|
30
41
|
if (Array.isArray(node.anyOf)) {
|
|
31
|
-
return withAnnotations(Type.Union(node.anyOf.map(
|
|
32
|
-
}
|
|
33
|
-
if (Array.isArray(node.oneOf)) {
|
|
34
|
-
return withAnnotations(Type.Union(node.oneOf.map(jsonSchemaToTypeBox)), annotations);
|
|
42
|
+
return withAnnotations(Type.Union(node.anyOf.map(convertJsonSchema)), annotations);
|
|
35
43
|
}
|
|
36
44
|
if (Array.isArray(node.allOf)) {
|
|
37
|
-
return withAnnotations(Type.Intersect(node.allOf.map(
|
|
45
|
+
return withAnnotations(Type.Intersect(node.allOf.map(convertJsonSchema)), annotations);
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
const type = node.type;
|
|
41
49
|
if (Array.isArray(type)) {
|
|
42
50
|
// e.g. ["string", "null"]
|
|
43
51
|
return withAnnotations(
|
|
44
|
-
Type.Union(type.map((t: string) =>
|
|
52
|
+
Type.Union(type.map((t: string) => convertJsonSchema({ ...node, type: t, enum: undefined }))),
|
|
45
53
|
annotations,
|
|
46
54
|
);
|
|
47
55
|
}
|
|
@@ -70,33 +78,47 @@ export function jsonSchemaToTypeBox(schema: unknown): TSchema {
|
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
function objectSchema(node: Json): TSchema {
|
|
73
|
-
const properties: Record<string, TSchema> =
|
|
81
|
+
const properties: Record<string, TSchema> = Object.create(null);
|
|
74
82
|
const required: string[] = Array.isArray(node.required) ? node.required : [];
|
|
75
83
|
const props = (node.properties ?? {}) as Json;
|
|
76
84
|
for (const [key, value] of Object.entries(props)) {
|
|
77
|
-
const child =
|
|
85
|
+
const child = convertJsonSchema(value);
|
|
78
86
|
properties[key] = required.includes(key) ? child : Type.Optional(child);
|
|
79
87
|
}
|
|
80
88
|
const options: Json = {};
|
|
81
89
|
if (node.additionalProperties === false) options.additionalProperties = false;
|
|
82
90
|
else if (node.additionalProperties && typeof node.additionalProperties === "object") {
|
|
83
|
-
options.additionalProperties =
|
|
91
|
+
options.additionalProperties = convertJsonSchema(node.additionalProperties);
|
|
84
92
|
}
|
|
85
93
|
return Type.Object(properties, options);
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
function arraySchema(node: Json): TSchema {
|
|
89
|
-
const items = node.items ? jsonSchemaToTypeBox(Array.isArray(node.items) ? node.items[0] : node.items) : Type.Unknown();
|
|
90
97
|
const options: Json = {};
|
|
91
98
|
if (typeof node.minItems === "number") options.minItems = node.minItems;
|
|
92
99
|
if (typeof node.maxItems === "number") options.maxItems = node.maxItems;
|
|
93
100
|
if (node.uniqueItems === true) options.uniqueItems = true;
|
|
94
|
-
|
|
101
|
+
if (Array.isArray(node.items)) {
|
|
102
|
+
return Type.Tuple(node.items.map(convertJsonSchema), {
|
|
103
|
+
...options,
|
|
104
|
+
additionalItems: true,
|
|
105
|
+
minItems: options.minItems ?? 0,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return Type.Array(node.items ? convertJsonSchema(node.items) : Type.Unknown(), options);
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
function numericAndStringConstraints(node: Json): Json {
|
|
98
112
|
const out: Json = {};
|
|
99
|
-
for (const key of [
|
|
113
|
+
for (const key of [
|
|
114
|
+
"minimum",
|
|
115
|
+
"maximum",
|
|
116
|
+
"exclusiveMinimum",
|
|
117
|
+
"exclusiveMaximum",
|
|
118
|
+
"multipleOf",
|
|
119
|
+
"minLength",
|
|
120
|
+
"maxLength",
|
|
121
|
+
]) {
|
|
100
122
|
if (node[key] !== undefined) out[key] = node[key];
|
|
101
123
|
}
|
|
102
124
|
return out;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { MAX_ACTIVE_WORKFLOWS_PER_SESSION, WorkflowPolicyError } from "./admission.ts";
|
|
3
|
+
|
|
4
|
+
interface ScopeState {
|
|
5
|
+
activeRunIds: Set<string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const scopes = new Map<string, ScopeState>();
|
|
9
|
+
|
|
10
|
+
export interface WorkflowLease {
|
|
11
|
+
readonly runsDir: string;
|
|
12
|
+
readonly runId: string;
|
|
13
|
+
release(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function acquireWorkflowLease(runsDir: string, runId: string): WorkflowLease {
|
|
17
|
+
const key = normalizeScope(runsDir);
|
|
18
|
+
let state = scopes.get(key);
|
|
19
|
+
if (!state) {
|
|
20
|
+
state = { activeRunIds: new Set() };
|
|
21
|
+
scopes.set(key, state);
|
|
22
|
+
}
|
|
23
|
+
if (state.activeRunIds.has(runId)) {
|
|
24
|
+
throw new WorkflowPolicyError(`workflow runId ${runId} is already active in this session`);
|
|
25
|
+
}
|
|
26
|
+
if (state.activeRunIds.size >= MAX_ACTIVE_WORKFLOWS_PER_SESSION) {
|
|
27
|
+
throw new WorkflowPolicyError(
|
|
28
|
+
`too many active workflows in this session (${state.activeRunIds.size}/${MAX_ACTIVE_WORKFLOWS_PER_SESSION}); wait for one to finish`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
state.activeRunIds.add(runId);
|
|
32
|
+
let released = false;
|
|
33
|
+
return {
|
|
34
|
+
runsDir: key,
|
|
35
|
+
runId,
|
|
36
|
+
release() {
|
|
37
|
+
if (released) return;
|
|
38
|
+
released = true;
|
|
39
|
+
const current = scopes.get(key);
|
|
40
|
+
current?.activeRunIds.delete(runId);
|
|
41
|
+
if (current && current.activeRunIds.size === 0) scopes.delete(key);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function activeWorkflowCount(runsDir: string): number {
|
|
47
|
+
return scopes.get(normalizeScope(runsDir))?.activeRunIds.size ?? 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function clearWorkflowLeasesForTests(): void {
|
|
51
|
+
scopes.clear();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeScope(runsDir: string): string {
|
|
55
|
+
try {
|
|
56
|
+
return path.resolve(runsDir);
|
|
57
|
+
} catch {
|
|
58
|
+
return runsDir;
|
|
59
|
+
}
|
|
60
|
+
}
|