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.
@@ -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 common JSON Schema subset; anything unrecognized falls back to
7
- * `Type.Unsafe`, which preserves the raw schema for the model without crashing.
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
- const literals = node.enum.map((value: unknown) => Type.Literal(value as any));
25
- return withAnnotations(literals.length === 1 ? literals[0] : Type.Union(literals), annotations);
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.Literal(node.const), annotations);
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(jsonSchemaToTypeBox)), annotations);
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(jsonSchemaToTypeBox)), annotations);
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) => jsonSchemaToTypeBox({ ...node, type: t, enum: undefined }))),
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 = jsonSchemaToTypeBox(value);
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 = jsonSchemaToTypeBox(node.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
- return Type.Array(items, options);
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 ["minimum", "maximum", "minLength", "maxLength", "pattern", "format"]) {
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
+ }