@danypops/papyrus 0.46.0 → 0.46.2
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/package.json +1 -1
- package/src/handlers/playbooks.ts +6 -2
- package/src/handlers/shared.ts +110 -23
- package/src/handlers/tasks.ts +11 -9
package/package.json
CHANGED
|
@@ -44,6 +44,10 @@ import {
|
|
|
44
44
|
} from "./shared.ts";
|
|
45
45
|
|
|
46
46
|
const OWNER = "playbooks";
|
|
47
|
+
const jsonObjectProp = {
|
|
48
|
+
type: ["object", "string"],
|
|
49
|
+
description: "A JSON object; a JSON-encoded object string is also accepted for tool-calling compatibility.",
|
|
50
|
+
} as const;
|
|
47
51
|
|
|
48
52
|
export interface PlaybooksVehicleDeps {
|
|
49
53
|
artifacts: ArtifactStore;
|
|
@@ -126,7 +130,7 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
|
|
|
126
130
|
"preview",
|
|
127
131
|
"Renders a Playbook's whole composition tree as text, with no side effects.",
|
|
128
132
|
"read",
|
|
129
|
-
{ id: stringProp, name: stringProp, arguments:
|
|
133
|
+
{ id: stringProp, name: stringProp, arguments: jsonObjectProp },
|
|
130
134
|
[],
|
|
131
135
|
(input) => {
|
|
132
136
|
normalizeJsonEncodedField(input, "arguments");
|
|
@@ -138,7 +142,7 @@ export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, de
|
|
|
138
142
|
"invoke",
|
|
139
143
|
"Compiles the Playbook's steps and composition tree into real Tasks wired with dependsOn, and focuses the first one -- one step surfaces at a time as it becomes focused, exactly like any other Task. `arguments` supplies known values as {name: value}; if a declared REQUIRED argument is still missing, nothing is created and missingArguments is returned instead -- ask the human for these (discuss tool, live:true) and invoke again, never guess. Drive the returned entryTaskId forward with the tasks tool (start/submit/complete).",
|
|
140
144
|
"local-write",
|
|
141
|
-
{ id: stringProp, name: stringProp, run_id: stringProp, arguments:
|
|
145
|
+
{ id: stringProp, name: stringProp, run_id: stringProp, arguments: jsonObjectProp, project_root: stringProp },
|
|
142
146
|
[],
|
|
143
147
|
(input) => {
|
|
144
148
|
normalizeJsonEncodedField(input, "arguments");
|
package/src/handlers/shared.ts
CHANGED
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
bindVehicleOperation,
|
|
8
8
|
defineVehicleOperation,
|
|
9
9
|
defineVehicleSchema,
|
|
10
|
+
type JsonSchema,
|
|
10
11
|
type VehicleContentBlock,
|
|
11
12
|
VehicleError,
|
|
12
13
|
type VehicleLimits,
|
|
13
14
|
type VehicleOperationContext,
|
|
14
15
|
type VehicleSchemaCodec,
|
|
16
|
+
type VehicleSchemaIssue,
|
|
15
17
|
} from "@danypops/vehicle-core";
|
|
16
18
|
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
17
19
|
import type { Artifact } from "../artifact/artifact.ts";
|
|
@@ -21,33 +23,118 @@ import { InvalidSessionSecretError } from "../session-identity/session-identity-
|
|
|
21
23
|
import { TaskCreateIdempotencyConflictError } from "../stores/task-create-request-store.ts";
|
|
22
24
|
import { TaskDependencyCycleError, TaskExecutionBoundExceededError, type TaskExecutionPlan } from "../task/task-execution.ts";
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
interface OperationSchemaNode {
|
|
27
|
+
readonly type?: string | readonly string[];
|
|
28
|
+
readonly enum?: readonly unknown[];
|
|
29
|
+
readonly properties?: Readonly<Record<string, OperationSchemaNode>>;
|
|
30
|
+
readonly required?: readonly string[];
|
|
31
|
+
readonly additionalProperties?: boolean | OperationSchemaNode;
|
|
32
|
+
readonly items?: OperationSchemaNode;
|
|
33
|
+
readonly minLength?: number;
|
|
34
|
+
readonly maxLength?: number;
|
|
35
|
+
readonly minimum?: number;
|
|
36
|
+
readonly maximum?: number;
|
|
37
|
+
readonly minItems?: number;
|
|
38
|
+
readonly maxItems?: number;
|
|
39
|
+
readonly description?: string;
|
|
40
|
+
readonly [key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function schemaIssue(path: readonly (string | number)[], message: string): VehicleSchemaIssue[] {
|
|
44
|
+
return [{ path, message }];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function matchesSchemaType(value: unknown, type: string): boolean {
|
|
48
|
+
if (type === "object") return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
+
if (type === "array") return Array.isArray(value);
|
|
50
|
+
if (type === "string") return typeof value === "string";
|
|
51
|
+
if (type === "number") return typeof value === "number" && Number.isFinite(value);
|
|
52
|
+
if (type === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
53
|
+
if (type === "boolean") return typeof value === "boolean";
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateSchemaValue(value: unknown, schema: OperationSchemaNode, path: readonly (string | number)[]): VehicleSchemaIssue[] {
|
|
58
|
+
const label = path.length === 0 ? "input" : String(path.at(-1));
|
|
59
|
+
const declaredTypes = typeof schema.type === "string" ? [schema.type] : (schema.type ?? []);
|
|
60
|
+
const type = declaredTypes.find((candidate) => matchesSchemaType(value, candidate));
|
|
61
|
+
if (declaredTypes.length > 0 && type === undefined) {
|
|
62
|
+
const accepted = declaredTypes.map((candidate) =>
|
|
63
|
+
candidate === "integer" ? "an integer" : `${candidate === "object" ? "an" : "a"} ${candidate}`,
|
|
64
|
+
);
|
|
65
|
+
return schemaIssue(path, `${label} must be ${accepted.join(" or ")}`);
|
|
66
|
+
}
|
|
67
|
+
if (type === "object") {
|
|
68
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return schemaIssue(path, `${label} must be an object`);
|
|
69
|
+
const record = value as Record<string, unknown>;
|
|
70
|
+
for (const key of schema.required ?? []) {
|
|
71
|
+
if (!(key in record)) {
|
|
72
|
+
const acceptedShape = schema.description ? `; ${schema.description}` : "";
|
|
73
|
+
return schemaIssue([...path, key], `${key} is required${acceptedShape}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
77
|
+
if (!(key in record)) continue;
|
|
78
|
+
const issues = validateSchemaValue(record[key], child, [...path, key]);
|
|
79
|
+
if (issues.length > 0) return issues;
|
|
80
|
+
}
|
|
81
|
+
for (const key of Object.keys(record)) {
|
|
82
|
+
if (key in (schema.properties ?? {})) continue;
|
|
83
|
+
if (schema.additionalProperties === false) return schemaIssue([...path, key], `${key} is not allowed`);
|
|
84
|
+
if (typeof schema.additionalProperties === "object") {
|
|
85
|
+
const issues = validateSchemaValue(record[key], schema.additionalProperties, [...path, key]);
|
|
86
|
+
if (issues.length > 0) return issues;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} else if (type === "array") {
|
|
90
|
+
const entries = value as unknown[];
|
|
91
|
+
if (schema.minItems !== undefined && entries.length < schema.minItems) {
|
|
92
|
+
return schemaIssue(path, `${label} must contain at least ${schema.minItems} item(s)`);
|
|
93
|
+
}
|
|
94
|
+
if (schema.maxItems !== undefined && entries.length > schema.maxItems) {
|
|
95
|
+
return schemaIssue(path, `${label} cannot contain more than ${schema.maxItems} item(s)`);
|
|
96
|
+
}
|
|
97
|
+
if (schema.items) {
|
|
98
|
+
for (const [index, entry] of entries.entries()) {
|
|
99
|
+
const issues = validateSchemaValue(entry, schema.items, [...path, index]);
|
|
100
|
+
if (issues.length > 0) return issues;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else if (type === "string") {
|
|
104
|
+
const text = value as string;
|
|
105
|
+
if (schema.minLength !== undefined && text.length < schema.minLength) {
|
|
106
|
+
return schemaIssue(path, `${label} must contain at least ${schema.minLength} character(s)`);
|
|
107
|
+
}
|
|
108
|
+
if (schema.maxLength !== undefined && text.length > schema.maxLength) {
|
|
109
|
+
return schemaIssue(path, `${label} cannot exceed ${schema.maxLength} character(s)`);
|
|
110
|
+
}
|
|
111
|
+
} else if (type === "number" || type === "integer") {
|
|
112
|
+
const number = value as number;
|
|
113
|
+
if (schema.minimum !== undefined && number < schema.minimum) {
|
|
114
|
+
return schemaIssue(path, `${label} must be at least ${schema.minimum}`);
|
|
115
|
+
}
|
|
116
|
+
if (schema.maximum !== undefined && number > schema.maximum) {
|
|
117
|
+
return schemaIssue(path, `${label} cannot exceed ${schema.maximum}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
121
|
+
return schemaIssue(path, `${label} must be one of ${schema.enum.join(", ")}`);
|
|
122
|
+
}
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** VehicleRegistry executes this codec before resolving or dispatching an operation. Keep the
|
|
127
|
+
* recursive runtime checks aligned with the same JSON Schema clients and tools_man receive. */
|
|
30
128
|
export function looseObjectSchema(
|
|
31
|
-
properties: Record<string,
|
|
129
|
+
properties: Readonly<Record<string, OperationSchemaNode>>,
|
|
32
130
|
required: readonly string[] = [],
|
|
33
131
|
): VehicleSchemaCodec<Record<string, unknown>> {
|
|
132
|
+
const schema = { type: "object", properties, required: [...required], additionalProperties: false } as const;
|
|
34
133
|
return defineVehicleSchema<Record<string, unknown>>({
|
|
35
|
-
jsonSchema:
|
|
134
|
+
jsonSchema: schema as unknown as JsonSchema,
|
|
36
135
|
safeParse(value) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
const input = value as Record<string, unknown>;
|
|
41
|
-
for (const key of required) {
|
|
42
|
-
if (!(key in input)) return { success: false, issues: [{ path: [key], message: `${key} is required` }] };
|
|
43
|
-
}
|
|
44
|
-
for (const [key, schema] of Object.entries(properties)) {
|
|
45
|
-
if (!schema.enum || !(key in input)) continue;
|
|
46
|
-
if (!schema.enum.includes(input[key] as string)) {
|
|
47
|
-
return { success: false, issues: [{ path: [key], message: `${key} must be one of ${schema.enum.join(", ")}` }] };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return { success: true, value: input };
|
|
136
|
+
const issues = validateSchemaValue(value, schema, []);
|
|
137
|
+
return issues.length > 0 ? { success: false, issues } : { success: true, value: value as Record<string, unknown> };
|
|
51
138
|
},
|
|
52
139
|
});
|
|
53
140
|
}
|
|
@@ -249,7 +336,7 @@ export function buildWorkflowRunContent(
|
|
|
249
336
|
|
|
250
337
|
export type OperationSchemaProperties = Record<
|
|
251
338
|
string,
|
|
252
|
-
{ type: string; enum?: readonly string[]; description?: string; [key: string]: unknown }
|
|
339
|
+
{ type: string | readonly string[]; enum?: readonly string[]; description?: string; [key: string]: unknown }
|
|
253
340
|
>;
|
|
254
341
|
|
|
255
342
|
export type DefineOperation = (
|
package/src/handlers/tasks.ts
CHANGED
|
@@ -81,6 +81,7 @@ const gateProp = {
|
|
|
81
81
|
description: "Validation gates run by tasks.run_gates and tasks.complete.",
|
|
82
82
|
items: {
|
|
83
83
|
type: "object",
|
|
84
|
+
description: "Accepted gate shape: {type, target, expect?, timeoutMs?}.",
|
|
84
85
|
properties: {
|
|
85
86
|
type: { type: "string", enum: GATE_TYPES, description: "Gate evaluator." },
|
|
86
87
|
target: { type: "string", minLength: 1, description: "Path, command, text target, or test command." },
|
|
@@ -109,6 +110,7 @@ const checklistProp = {
|
|
|
109
110
|
minItems: 1,
|
|
110
111
|
items: {
|
|
111
112
|
type: "object",
|
|
113
|
+
description: "Accepted proof shape: {type, target, expect?}.",
|
|
112
114
|
properties: {
|
|
113
115
|
type: { type: "string", enum: PROOF_TYPES },
|
|
114
116
|
target: { type: "string", minLength: 1 },
|
|
@@ -286,7 +288,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
286
288
|
|
|
287
289
|
define(
|
|
288
290
|
"create",
|
|
289
|
-
|
|
291
|
+
'Creates a Task -- work: desired outcomes, gates, checklists, and dependencies. Gates are {type, target, expect?, timeoutMs?}; for example [{type: "command", target: "bun run typecheck", timeoutMs: 60000}]. Checklist criteria are {proof: [{type, target, expect?}]}. project_root is required (no ambient cwd server-side). Prefer parent_name/depends_on_names over parent_id/depends_on -- resolved server-side.',
|
|
290
292
|
"local-write",
|
|
291
293
|
{
|
|
292
294
|
title: stringProp,
|
|
@@ -752,11 +754,11 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
752
754
|
|
|
753
755
|
define(
|
|
754
756
|
"claim",
|
|
755
|
-
"Claims this Task's lease under owner (defaults to session_id). Returns taskName (the reusable artifact alias) and taskTitle instead of exposing its backend UUID. Throws if a different owner already holds one.",
|
|
757
|
+
"Claims this Task's lease under owner (defaults to session_id). Prefer name over id. Returns taskName (the reusable artifact alias) and taskTitle instead of exposing its backend UUID. Throws if a different owner already holds one.",
|
|
756
758
|
"local-write",
|
|
757
759
|
{
|
|
758
|
-
id: stringProp,
|
|
759
760
|
name: stringProp,
|
|
761
|
+
id: stringProp,
|
|
760
762
|
owner: stringProp,
|
|
761
763
|
ttl_ms: numberProp,
|
|
762
764
|
note: stringProp,
|
|
@@ -772,11 +774,11 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
772
774
|
);
|
|
773
775
|
define(
|
|
774
776
|
"heartbeat_lease",
|
|
775
|
-
"Extends this Task's lease -- needs the exact owner/token claim() returned
|
|
777
|
+
"Extends this Task's lease -- needs the exact owner/token claim() returned. Prefer name over id. Returns the reusable taskName plus taskTitle.",
|
|
776
778
|
"local-write",
|
|
777
779
|
{
|
|
778
|
-
id: stringProp,
|
|
779
780
|
name: stringProp,
|
|
781
|
+
id: stringProp,
|
|
780
782
|
owner: stringProp,
|
|
781
783
|
token: stringProp,
|
|
782
784
|
ttl_ms: numberProp,
|
|
@@ -791,9 +793,9 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
791
793
|
);
|
|
792
794
|
define(
|
|
793
795
|
"release_lease",
|
|
794
|
-
"Releases this Task's lease -- needs the exact owner/token claim() returned.",
|
|
796
|
+
"Releases this Task's lease -- needs the exact owner/token claim() returned. Prefer name over id.",
|
|
795
797
|
"local-write",
|
|
796
|
-
{
|
|
798
|
+
{ name: stringProp, id: stringProp, owner: stringProp, token: stringProp, project_root: stringProp, session_id: stringProp },
|
|
797
799
|
["owner", "token"],
|
|
798
800
|
(input) => ({
|
|
799
801
|
...input,
|
|
@@ -802,9 +804,9 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
802
804
|
);
|
|
803
805
|
define(
|
|
804
806
|
"lease",
|
|
805
|
-
"Shows this Task's current lease, if any
|
|
807
|
+
"Shows this Task's current lease, if any. Prefer name over id. The result is identified by reusable taskName plus taskTitle rather than its backend UUID.",
|
|
806
808
|
"read",
|
|
807
|
-
{
|
|
809
|
+
{ name: stringProp, id: stringProp, project_root: stringProp },
|
|
808
810
|
[],
|
|
809
811
|
(input) => ({
|
|
810
812
|
...input,
|