@oh-my-pi/pi-coding-agent 3.24.0 → 3.25.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 +15 -0
- package/package.json +4 -4
- package/src/core/custom-commands/bundled/wt/index.ts +3 -0
- package/src/core/sdk.ts +7 -0
- package/src/core/tools/complete.ts +131 -0
- package/src/core/tools/index.test.ts +9 -1
- package/src/core/tools/index.ts +18 -5
- package/src/core/tools/jtd-to-json-schema.ts +274 -0
- package/src/core/tools/output.ts +125 -14
- package/src/core/tools/task/artifacts.ts +6 -9
- package/src/core/tools/task/executor.ts +44 -5
- package/src/core/tools/task/index.ts +23 -18
- package/src/core/tools/task/name-generator.ts +247 -0
- package/src/core/tools/task/render.ts +137 -8
- package/src/core/tools/task/types.ts +7 -0
- package/src/core/tools/task/worker-protocol.ts +1 -0
- package/src/core/tools/task/worker.ts +33 -1
- package/src/prompts/task.md +14 -50
- package/src/prompts/tools/output.md +2 -1
- package/src/prompts/tools/task.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.25.0] - 2026-01-07
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `complete` tool for structured subagent output with JSON schema validation
|
|
9
|
+
- Added `query` parameter to output tool for jq-like JSON querying
|
|
10
|
+
- Added `output_schema` parameter to task tool for structured subagent completion
|
|
11
|
+
- Added JTD (JSON Type Definition) to JSON Schema converter for schema flexibility
|
|
12
|
+
- Added memorable two-word task identifiers (e.g., SwiftFalcon) for better task tracking
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Changed task output IDs from `agent_index` format to memorable names for easier reference
|
|
17
|
+
- Changed subagent completion flow to require explicit `complete` tool call with retry reminders
|
|
18
|
+
- Simplified worker agent system prompt to be more concise and focused
|
|
19
|
+
|
|
5
20
|
## [3.24.0] - 2026-01-07
|
|
6
21
|
### Added
|
|
7
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.25.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@mariozechner/pi-ai": "^0.37.4",
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "3.
|
|
45
|
-
"@oh-my-pi/pi-tui": "3.
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "3.25.0",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "3.25.0",
|
|
45
|
+
"@oh-my-pi/pi-tui": "3.25.0",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
|
@@ -9,6 +9,7 @@ import { formatStats, getStats } from "../../../../lib/worktree/stats";
|
|
|
9
9
|
import type { HookCommandContext } from "../../../hooks/types";
|
|
10
10
|
import { discoverAgents, getAgent } from "../../../tools/task/discovery";
|
|
11
11
|
import { runSubprocess } from "../../../tools/task/executor";
|
|
12
|
+
import { generateTaskName } from "../../../tools/task/name-generator";
|
|
12
13
|
import type { AgentDefinition } from "../../../tools/task/types";
|
|
13
14
|
import type { CustomCommand, CustomCommandAPI } from "../../types";
|
|
14
15
|
|
|
@@ -265,6 +266,7 @@ async function handleSpawn(args: SpawnArgs, ctx: HookCommandContext): Promise<st
|
|
|
265
266
|
agent,
|
|
266
267
|
task: args.task,
|
|
267
268
|
index: 0,
|
|
269
|
+
taskId: generateTaskName(),
|
|
268
270
|
context,
|
|
269
271
|
});
|
|
270
272
|
|
|
@@ -322,6 +324,7 @@ async function handleParallel(args: ParallelTask[], ctx: HookCommandContext): Pr
|
|
|
322
324
|
agent,
|
|
323
325
|
task: task.task,
|
|
324
326
|
index,
|
|
327
|
+
taskId: generateTaskName(),
|
|
325
328
|
context: `Scope: ${task.scope}`,
|
|
326
329
|
});
|
|
327
330
|
await updateSession(session.id, {
|
package/src/core/sdk.ts
CHANGED
|
@@ -149,6 +149,11 @@ export interface CreateAgentSessionOptions {
|
|
|
149
149
|
/** Tool names explicitly requested (enables disabled-by-default tools) */
|
|
150
150
|
toolNames?: string[];
|
|
151
151
|
|
|
152
|
+
/** Output schema for structured completion (subagents) */
|
|
153
|
+
outputSchema?: unknown;
|
|
154
|
+
/** Whether to include the complete tool by default */
|
|
155
|
+
requireCompleteTool?: boolean;
|
|
156
|
+
|
|
152
157
|
/** Session manager. Default: SessionManager.create(cwd) */
|
|
153
158
|
sessionManager?: SessionManager;
|
|
154
159
|
|
|
@@ -608,6 +613,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
608
613
|
hasUI: options.hasUI ?? false,
|
|
609
614
|
rulebookRules,
|
|
610
615
|
eventBus,
|
|
616
|
+
outputSchema: options.outputSchema,
|
|
617
|
+
requireCompleteTool: options.requireCompleteTool,
|
|
611
618
|
getSessionFile: () => sessionManager.getSessionFile() ?? null,
|
|
612
619
|
getSessionSpawns: () => options.spawns ?? "*",
|
|
613
620
|
settings: settingsManager,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete tool for structured subagent output.
|
|
3
|
+
*
|
|
4
|
+
* Subagents must call this tool to finish and return structured JSON output.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
|
|
10
|
+
import type { ToolSession } from "./index";
|
|
11
|
+
import { jtdToJsonSchema } from "./jtd-to-json-schema";
|
|
12
|
+
import { subprocessToolRegistry } from "./task/subprocess-tool-registry";
|
|
13
|
+
|
|
14
|
+
export interface CompleteDetails {
|
|
15
|
+
data: unknown;
|
|
16
|
+
status: "success" | "aborted";
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
21
|
+
|
|
22
|
+
function normalizeSchema(schema: unknown): { normalized?: unknown; error?: string } {
|
|
23
|
+
if (schema === undefined || schema === null) return {};
|
|
24
|
+
if (typeof schema === "string") {
|
|
25
|
+
try {
|
|
26
|
+
return { normalized: JSON.parse(schema) };
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { normalized: schema };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatSchema(schema: unknown): string {
|
|
35
|
+
if (schema === undefined) return "No schema provided.";
|
|
36
|
+
if (typeof schema === "string") return schema;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(schema, null, 2);
|
|
39
|
+
} catch {
|
|
40
|
+
return "[unserializable schema]";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {
|
|
45
|
+
if (!errors || errors.length === 0) return "Unknown schema validation error.";
|
|
46
|
+
return errors
|
|
47
|
+
.map((err) => {
|
|
48
|
+
const path = err.instancePath ? `${err.instancePath}: ` : "";
|
|
49
|
+
return `${path}${err.message ?? "invalid"}`;
|
|
50
|
+
})
|
|
51
|
+
.join("; ");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createCompleteTool(session: ToolSession) {
|
|
55
|
+
const schemaResult = normalizeSchema(session.outputSchema);
|
|
56
|
+
// Convert JTD to JSON Schema if needed (auto-detected)
|
|
57
|
+
const normalizedSchema =
|
|
58
|
+
schemaResult.normalized !== undefined ? jtdToJsonSchema(schemaResult.normalized) : undefined;
|
|
59
|
+
let validate: ValidateFunction | undefined;
|
|
60
|
+
let schemaError = schemaResult.error;
|
|
61
|
+
|
|
62
|
+
if (normalizedSchema !== undefined && !schemaError) {
|
|
63
|
+
try {
|
|
64
|
+
validate = ajv.compile(normalizedSchema as any);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
schemaError = err instanceof Error ? err.message : String(err);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const schemaHint = formatSchema(normalizedSchema ?? session.outputSchema);
|
|
71
|
+
|
|
72
|
+
// Use actual schema if provided, otherwise fall back to Type.Any
|
|
73
|
+
// Merge description into the JSON schema for better tool documentation
|
|
74
|
+
const dataSchema = normalizedSchema
|
|
75
|
+
? Type.Unsafe({
|
|
76
|
+
...(normalizedSchema as object),
|
|
77
|
+
description: "Structured output matching the schema:\n" + schemaHint,
|
|
78
|
+
})
|
|
79
|
+
: Type.Any({ description: "Structured JSON output (no schema specified)" });
|
|
80
|
+
|
|
81
|
+
const completeParams = Type.Object({
|
|
82
|
+
data: dataSchema,
|
|
83
|
+
status: Type.Optional(
|
|
84
|
+
Type.Union([Type.Literal("success"), Type.Literal("aborted")], {
|
|
85
|
+
default: "success",
|
|
86
|
+
description: "Use 'aborted' if the task cannot be completed",
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
error: Type.Optional(Type.String({ description: "Error message when status is 'aborted'" })),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const tool: AgentTool<typeof completeParams, CompleteDetails> = {
|
|
93
|
+
name: "complete",
|
|
94
|
+
label: "Complete",
|
|
95
|
+
description:
|
|
96
|
+
"Finish the task with structured JSON output. Call exactly once at the end of the task.\n\n" +
|
|
97
|
+
"If you cannot complete the task, call with status='aborted' and an error message.",
|
|
98
|
+
parameters: completeParams,
|
|
99
|
+
execute: async (_toolCallId, params) => {
|
|
100
|
+
const status = params.status ?? "success";
|
|
101
|
+
|
|
102
|
+
// Skip schema validation when aborting - the agent is giving up
|
|
103
|
+
if (status === "success") {
|
|
104
|
+
if (schemaError) {
|
|
105
|
+
throw new Error(`Invalid output schema: ${schemaError}`);
|
|
106
|
+
}
|
|
107
|
+
if (validate && !validate(params.data)) {
|
|
108
|
+
throw new Error(`Output does not match schema: ${formatAjvErrors(validate.errors)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const responseText =
|
|
113
|
+
status === "aborted"
|
|
114
|
+
? `Task aborted: ${params.error || "No reason provided"}`
|
|
115
|
+
: "Completion recorded.";
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text: responseText }],
|
|
119
|
+
details: { data: params.data, status, error: params.error },
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return tool;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Register subprocess tool handler for extraction + termination.
|
|
128
|
+
subprocessToolRegistry.register<CompleteDetails>("complete", {
|
|
129
|
+
extractData: (event) => event.result?.details as CompleteDetails | undefined,
|
|
130
|
+
shouldTerminate: () => true,
|
|
131
|
+
});
|
|
@@ -50,6 +50,14 @@ describe("createTools", () => {
|
|
|
50
50
|
expect(names).toEqual(["report_finding"]);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it("includes complete tool when required", async () => {
|
|
54
|
+
const session = createTestSession({ requireCompleteTool: true });
|
|
55
|
+
const tools = await createTools(session);
|
|
56
|
+
const names = tools.map((t) => t.name);
|
|
57
|
+
|
|
58
|
+
expect(names).toContain("complete");
|
|
59
|
+
});
|
|
60
|
+
|
|
53
61
|
it("excludes ask tool when hasUI is false", async () => {
|
|
54
62
|
const session = createTestSession({ hasUI: false });
|
|
55
63
|
const tools = await createTools(session);
|
|
@@ -175,6 +183,6 @@ describe("createTools", () => {
|
|
|
175
183
|
});
|
|
176
184
|
|
|
177
185
|
it("HIDDEN_TOOLS contains review tools", () => {
|
|
178
|
-
expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["report_finding", "submit_review"]);
|
|
186
|
+
expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["complete", "report_finding", "submit_review"]);
|
|
179
187
|
});
|
|
180
188
|
});
|
package/src/core/tools/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { type AskToolDetails, askTool, createAskTool } from "./ask";
|
|
2
2
|
export { type BashToolDetails, createBashTool } from "./bash";
|
|
3
|
+
export { createCompleteTool } from "./complete";
|
|
3
4
|
export { createEditTool } from "./edit";
|
|
4
5
|
// Exa MCP tools (22 tools)
|
|
5
6
|
export { exaTools } from "./exa/index";
|
|
@@ -54,6 +55,7 @@ import type { Rule } from "../../capability/rule";
|
|
|
54
55
|
import type { EventBus } from "../event-bus";
|
|
55
56
|
import { createAskTool } from "./ask";
|
|
56
57
|
import { createBashTool } from "./bash";
|
|
58
|
+
import { createCompleteTool } from "./complete";
|
|
57
59
|
import { createEditTool } from "./edit";
|
|
58
60
|
import { createFindTool } from "./find";
|
|
59
61
|
import { createGitTool } from "./git";
|
|
@@ -83,6 +85,10 @@ export interface ToolSession {
|
|
|
83
85
|
rulebookRules: Rule[];
|
|
84
86
|
/** Event bus for tool/extension communication */
|
|
85
87
|
eventBus?: EventBus;
|
|
88
|
+
/** Output schema for structured completion (subagents) */
|
|
89
|
+
outputSchema?: unknown;
|
|
90
|
+
/** Whether to include the complete tool by default */
|
|
91
|
+
requireCompleteTool?: boolean;
|
|
86
92
|
/** Get session file */
|
|
87
93
|
getSessionFile: () => string | null;
|
|
88
94
|
/** Get session spawns */
|
|
@@ -121,6 +127,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
121
127
|
};
|
|
122
128
|
|
|
123
129
|
export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
130
|
+
complete: createCompleteTool,
|
|
124
131
|
report_finding: () => reportFindingTool,
|
|
125
132
|
submit_review: () => submitReviewTool,
|
|
126
133
|
};
|
|
@@ -131,13 +138,19 @@ export type ToolName = keyof typeof BUILTIN_TOOLS;
|
|
|
131
138
|
* Create tools from BUILTIN_TOOLS registry.
|
|
132
139
|
*/
|
|
133
140
|
export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
|
|
134
|
-
const
|
|
141
|
+
const includeComplete = session.requireCompleteTool === true;
|
|
142
|
+
const requestedTools = toolNames && toolNames.length > 0 ? [...new Set(toolNames)] : undefined;
|
|
135
143
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
144
|
+
if (includeComplete && requestedTools && !requestedTools.includes("complete")) {
|
|
145
|
+
requestedTools.push("complete");
|
|
146
|
+
}
|
|
147
|
+
|
|
136
148
|
const entries = requestedTools
|
|
137
|
-
? requestedTools
|
|
138
|
-
|
|
139
|
-
.
|
|
140
|
-
|
|
149
|
+
? requestedTools.filter((name) => name in allTools).map((name) => [name, allTools[name]] as const)
|
|
150
|
+
: [
|
|
151
|
+
...Object.entries(BUILTIN_TOOLS),
|
|
152
|
+
...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : []),
|
|
153
|
+
];
|
|
141
154
|
const results = await Promise.all(entries.map(([, factory]) => factory(session)));
|
|
142
155
|
const tools = results.filter((t): t is Tool => t !== null);
|
|
143
156
|
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert JSON Type Definition (JTD) to JSON Schema.
|
|
3
|
+
*
|
|
4
|
+
* JTD (RFC 8927) is a simpler schema format. This converter allows users to
|
|
5
|
+
* write schemas in JTD and have them converted to JSON Schema for model APIs.
|
|
6
|
+
*
|
|
7
|
+
* @see https://jsontypedef.com/
|
|
8
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8927
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type JTDPrimitive =
|
|
12
|
+
| "boolean"
|
|
13
|
+
| "string"
|
|
14
|
+
| "timestamp"
|
|
15
|
+
| "float32"
|
|
16
|
+
| "float64"
|
|
17
|
+
| "int8"
|
|
18
|
+
| "uint8"
|
|
19
|
+
| "int16"
|
|
20
|
+
| "uint16"
|
|
21
|
+
| "int32"
|
|
22
|
+
| "uint32";
|
|
23
|
+
|
|
24
|
+
interface JTDType {
|
|
25
|
+
type: JTDPrimitive;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface JTDEnum {
|
|
29
|
+
enum: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface JTDElements {
|
|
33
|
+
elements: JTDSchema;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface JTDValues {
|
|
37
|
+
values: JTDSchema;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface JTDProperties {
|
|
41
|
+
properties?: Record<string, JTDSchema>;
|
|
42
|
+
optionalProperties?: Record<string, JTDSchema>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface JTDDiscriminator {
|
|
46
|
+
discriminator: string;
|
|
47
|
+
mapping: Record<string, JTDProperties>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface JTDRef {
|
|
51
|
+
ref: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface JTDEmpty {}
|
|
55
|
+
|
|
56
|
+
type JTDSchema =
|
|
57
|
+
| JTDType
|
|
58
|
+
| JTDEnum
|
|
59
|
+
| JTDElements
|
|
60
|
+
| JTDValues
|
|
61
|
+
| JTDProperties
|
|
62
|
+
| JTDDiscriminator
|
|
63
|
+
| JTDRef
|
|
64
|
+
| JTDEmpty;
|
|
65
|
+
|
|
66
|
+
const primitiveMap: Record<JTDPrimitive, string> = {
|
|
67
|
+
boolean: "boolean",
|
|
68
|
+
string: "string",
|
|
69
|
+
timestamp: "string", // ISO 8601
|
|
70
|
+
float32: "number",
|
|
71
|
+
float64: "number",
|
|
72
|
+
int8: "integer",
|
|
73
|
+
uint8: "integer",
|
|
74
|
+
int16: "integer",
|
|
75
|
+
uint16: "integer",
|
|
76
|
+
int32: "integer",
|
|
77
|
+
uint32: "integer",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function isJTDType(schema: unknown): schema is JTDType {
|
|
81
|
+
return typeof schema === "object" && schema !== null && "type" in schema;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isJTDEnum(schema: unknown): schema is JTDEnum {
|
|
85
|
+
return typeof schema === "object" && schema !== null && "enum" in schema;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isJTDElements(schema: unknown): schema is JTDElements {
|
|
89
|
+
return typeof schema === "object" && schema !== null && "elements" in schema;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isJTDValues(schema: unknown): schema is JTDValues {
|
|
93
|
+
return typeof schema === "object" && schema !== null && "values" in schema;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isJTDProperties(schema: unknown): schema is JTDProperties {
|
|
97
|
+
return (
|
|
98
|
+
typeof schema === "object" &&
|
|
99
|
+
schema !== null &&
|
|
100
|
+
("properties" in schema || "optionalProperties" in schema)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isJTDDiscriminator(schema: unknown): schema is JTDDiscriminator {
|
|
105
|
+
return typeof schema === "object" && schema !== null && "discriminator" in schema;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isJTDRef(schema: unknown): schema is JTDRef {
|
|
109
|
+
return typeof schema === "object" && schema !== null && "ref" in schema;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function convertSchema(schema: unknown): unknown {
|
|
113
|
+
if (schema === null || typeof schema !== "object") {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Type form: { type: "string" } → { type: "string" }
|
|
118
|
+
if (isJTDType(schema)) {
|
|
119
|
+
const jsonType = primitiveMap[schema.type as JTDPrimitive];
|
|
120
|
+
if (!jsonType) {
|
|
121
|
+
return { type: schema.type };
|
|
122
|
+
}
|
|
123
|
+
const result: Record<string, unknown> = { type: jsonType };
|
|
124
|
+
// Add format for timestamp
|
|
125
|
+
if (schema.type === "timestamp") {
|
|
126
|
+
result.format = "date-time";
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Enum form: { enum: ["a", "b"] } → { enum: ["a", "b"] }
|
|
132
|
+
if (isJTDEnum(schema)) {
|
|
133
|
+
return { enum: schema.enum };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Elements form: { elements: { type: "string" } } → { type: "array", items: ... }
|
|
137
|
+
if (isJTDElements(schema)) {
|
|
138
|
+
return {
|
|
139
|
+
type: "array",
|
|
140
|
+
items: convertSchema(schema.elements),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Values form: { values: { type: "string" } } → { type: "object", additionalProperties: ... }
|
|
145
|
+
if (isJTDValues(schema)) {
|
|
146
|
+
return {
|
|
147
|
+
type: "object",
|
|
148
|
+
additionalProperties: convertSchema(schema.values),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Properties form: { properties: {...}, optionalProperties: {...} }
|
|
153
|
+
if (isJTDProperties(schema)) {
|
|
154
|
+
const properties: Record<string, unknown> = {};
|
|
155
|
+
const required: string[] = [];
|
|
156
|
+
|
|
157
|
+
// Required properties
|
|
158
|
+
if (schema.properties) {
|
|
159
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
160
|
+
properties[key] = convertSchema(value);
|
|
161
|
+
required.push(key);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Optional properties
|
|
166
|
+
if (schema.optionalProperties) {
|
|
167
|
+
for (const [key, value] of Object.entries(schema.optionalProperties)) {
|
|
168
|
+
properties[key] = convertSchema(value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const result: Record<string, unknown> = {
|
|
173
|
+
type: "object",
|
|
174
|
+
properties,
|
|
175
|
+
additionalProperties: false,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (required.length > 0) {
|
|
179
|
+
result.required = required;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Discriminator form: { discriminator: "type", mapping: { ... } }
|
|
186
|
+
if (isJTDDiscriminator(schema)) {
|
|
187
|
+
const oneOf: unknown[] = [];
|
|
188
|
+
|
|
189
|
+
for (const [tag, props] of Object.entries(schema.mapping)) {
|
|
190
|
+
const converted = convertSchema(props) as Record<string, unknown>;
|
|
191
|
+
// Add the discriminator property
|
|
192
|
+
const properties = (converted.properties || {}) as Record<string, unknown>;
|
|
193
|
+
properties[schema.discriminator] = { const: tag };
|
|
194
|
+
|
|
195
|
+
const required = ((converted.required as string[]) || []).slice();
|
|
196
|
+
if (!required.includes(schema.discriminator)) {
|
|
197
|
+
required.push(schema.discriminator);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
oneOf.push({
|
|
201
|
+
...converted,
|
|
202
|
+
properties,
|
|
203
|
+
required,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { oneOf };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Ref form: { ref: "MyType" } → { $ref: "#/$defs/MyType" }
|
|
211
|
+
if (isJTDRef(schema)) {
|
|
212
|
+
return { $ref: `#/$defs/${schema.ref}` };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Empty form: {} → {} (accepts anything)
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Detect if a schema is JTD format (vs JSON Schema).
|
|
221
|
+
*
|
|
222
|
+
* JTD schemas use: type (primitives), properties, optionalProperties, elements, values, enum, discriminator, ref
|
|
223
|
+
* JSON Schema uses: type: "object", type: "array", items, additionalProperties, etc.
|
|
224
|
+
*/
|
|
225
|
+
export function isJTDSchema(schema: unknown): boolean {
|
|
226
|
+
if (schema === null || typeof schema !== "object") {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const obj = schema as Record<string, unknown>;
|
|
231
|
+
|
|
232
|
+
// JTD-specific keywords
|
|
233
|
+
if ("elements" in obj) return true;
|
|
234
|
+
if ("values" in obj) return true;
|
|
235
|
+
if ("optionalProperties" in obj) return true;
|
|
236
|
+
if ("discriminator" in obj) return true;
|
|
237
|
+
if ("ref" in obj) return true;
|
|
238
|
+
|
|
239
|
+
// JTD type primitives (JSON Schema doesn't have int32, float64, etc.)
|
|
240
|
+
if ("type" in obj) {
|
|
241
|
+
const jtdPrimitives = [
|
|
242
|
+
"timestamp",
|
|
243
|
+
"float32",
|
|
244
|
+
"float64",
|
|
245
|
+
"int8",
|
|
246
|
+
"uint8",
|
|
247
|
+
"int16",
|
|
248
|
+
"uint16",
|
|
249
|
+
"int32",
|
|
250
|
+
"uint32",
|
|
251
|
+
];
|
|
252
|
+
if (jtdPrimitives.includes(obj.type as string)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// JTD properties form without type: "object" (JSON Schema requires it)
|
|
258
|
+
if ("properties" in obj && !("type" in obj)) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Convert JTD schema to JSON Schema.
|
|
267
|
+
* If already JSON Schema, returns as-is.
|
|
268
|
+
*/
|
|
269
|
+
export function jtdToJsonSchema(schema: unknown): unknown {
|
|
270
|
+
if (!isJTDSchema(schema)) {
|
|
271
|
+
return schema;
|
|
272
|
+
}
|
|
273
|
+
return convertSchema(schema);
|
|
274
|
+
}
|