skyloom 1.14.6 → 1.15.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/.github/workflows/ci.yml +2 -2
- package/.github/workflows/publish.yml +74 -0
- package/CONVERSION_PLAN.md +191 -191
- package/README.md +523 -220
- package/config/default.yaml +46 -43
- package/config/models.yaml +928 -155
- package/config/providers.yaml +109 -6
- package/dist/agents/snow.d.ts +2 -0
- package/dist/agents/snow.d.ts.map +1 -1
- package/dist/agents/snow.js +36 -5
- package/dist/agents/snow.js.map +1 -1
- package/dist/cli/loom_chat.d.ts.map +1 -1
- package/dist/cli/loom_chat.js +207 -1
- package/dist/cli/loom_chat.js.map +1 -1
- package/dist/cli/main.js +190 -40
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +6 -31
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent.d.ts +6 -4
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -20
- package/dist/core/agent.js.map +1 -1
- package/dist/core/catalog.d.ts.map +1 -1
- package/dist/core/catalog.js +30 -9
- package/dist/core/catalog.js.map +1 -1
- package/dist/core/commands.d.ts +110 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +633 -0
- package/dist/core/commands.js.map +1 -0
- package/dist/core/concurrency.d.ts +38 -0
- package/dist/core/concurrency.d.ts.map +1 -0
- package/dist/core/concurrency.js +65 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/factory.js +16 -16
- package/dist/core/file_checkpoint.d.ts +9 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -1
- package/dist/core/file_checkpoint.js +33 -1
- package/dist/core/file_checkpoint.js.map +1 -1
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +66 -13
- package/dist/core/llm.js.map +1 -1
- package/dist/core/memory.js +51 -51
- package/dist/core/schemas.d.ts +16 -0
- package/dist/core/schemas.d.ts.map +1 -1
- package/dist/core/schemas.js +32 -0
- package/dist/core/schemas.js.map +1 -1
- package/dist/core/security.d.ts.map +1 -1
- package/dist/core/security.js +27 -0
- package/dist/core/security.js.map +1 -1
- package/dist/core/skymd.js +14 -14
- package/dist/core/trace.d.ts +105 -0
- package/dist/core/trace.d.ts.map +1 -0
- package/dist/core/trace.js +213 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/tools/builtin.d.ts +2 -6
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +180 -125
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/extra.d.ts +13 -0
- package/dist/tools/extra.d.ts.map +1 -0
- package/dist/tools/extra.js +827 -0
- package/dist/tools/extra.js.map +1 -0
- package/dist/tools/guards.d.ts +12 -0
- package/dist/tools/guards.d.ts.map +1 -0
- package/dist/tools/guards.js +143 -0
- package/dist/tools/guards.js.map +1 -0
- package/dist/tools/model_tool.d.ts.map +1 -1
- package/dist/tools/model_tool.js +24 -4
- package/dist/tools/model_tool.js.map +1 -1
- package/dist/web/markdown.d.ts +32 -0
- package/dist/web/markdown.d.ts.map +1 -0
- package/dist/web/markdown.js +202 -0
- package/dist/web/markdown.js.map +1 -0
- package/dist/web/server.d.ts +4 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +14 -582
- package/dist/web/server.js.map +1 -1
- package/dist/web/ui.d.ts +31 -0
- package/dist/web/ui.d.ts.map +1 -0
- package/dist/web/ui.js +1009 -0
- package/dist/web/ui.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -152
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +68 -68
- package/src/agents/snow.ts +38 -5
- package/src/cli/commands_md.ts +112 -112
- package/src/cli/input_macros.ts +83 -83
- package/src/cli/loom.ts +1041 -1041
- package/src/cli/loom_chat.ts +772 -603
- package/src/cli/main.ts +853 -723
- package/src/cli/tui.ts +264 -289
- package/src/core/agent/guard.ts +133 -133
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +1630 -1590
- package/src/core/agent_helpers.ts +500 -500
- package/src/core/bus.ts +221 -221
- package/src/core/cache.ts +153 -153
- package/src/core/catalog.ts +199 -178
- package/src/core/circuit_breaker.ts +119 -119
- package/src/core/commands.ts +704 -0
- package/src/core/concurrency.ts +73 -0
- package/src/core/config.ts +365 -365
- package/src/core/constants.ts +95 -95
- package/src/core/factory.ts +656 -656
- package/src/core/file_checkpoint.ts +163 -136
- package/src/core/hooks.ts +126 -126
- package/src/core/llm.ts +972 -915
- package/src/core/logger.ts +143 -143
- package/src/core/mcp.ts +1001 -1001
- package/src/core/memory.ts +1201 -1201
- package/src/core/middleware.ts +350 -350
- package/src/core/model_config.ts +159 -159
- package/src/core/pipelines.ts +424 -424
- package/src/core/schemas.ts +319 -282
- package/src/core/security.ts +27 -0
- package/src/core/semantic.ts +211 -211
- package/src/core/skill.ts +384 -384
- package/src/core/skymd.ts +143 -143
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +457 -457
- package/src/core/trace.ts +236 -0
- package/src/core/verify.ts +71 -71
- package/src/plugins/loader.ts +91 -91
- package/src/skills/loader.ts +75 -75
- package/src/tools/builtin.ts +571 -493
- package/src/tools/computer.ts +279 -279
- package/src/tools/extra.ts +662 -0
- package/src/tools/guards.ts +82 -0
- package/src/tools/model_tool.ts +93 -74
- package/src/tools/todo.ts +76 -76
- package/src/web/markdown.ts +193 -0
- package/src/web/server.ts +117 -693
- package/src/web/ui.ts +949 -0
- package/tests/agent.test.ts +211 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -124
- package/tests/claude_compat.test.ts +110 -110
- package/tests/commands.test.ts +103 -0
- package/tests/concurrency.test.ts +102 -0
- package/tests/config.test.ts +41 -41
- package/tests/extra_tools.test.ts +212 -0
- package/tests/fence_plugin.test.ts +52 -52
- package/tests/guard.test.ts +75 -75
- package/tests/loom.test.ts +337 -337
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -109
- package/tests/skymd.test.ts +146 -146
- package/tests/ssrf.test.ts +38 -38
- package/tests/structured_retry.test.ts +87 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -94
- package/tests/trace.test.ts +128 -0
- package/tests/tui.test.ts +67 -67
- package/tests/web.test.ts +169 -0
- package/tsconfig.json +38 -38
package/src/core/schemas.ts
CHANGED
|
@@ -1,282 +1,319 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight structured output schemas for LLM response validation.
|
|
3
|
-
*
|
|
4
|
-
* Why: LLM JSON output is inherently fragile — models emit markdown fences,
|
|
5
|
-
* trailing commas, unquoted keys, or hallucinated fields. Rather than layering
|
|
6
|
-
* heuristic repair (which silently passes corrupted data), we define typed
|
|
7
|
-
* schemas and validate on ingress. Parsing failures surface immediately so the
|
|
8
|
-
* caller can retry with a corrected prompt instead of propagating garbage.
|
|
9
|
-
*
|
|
10
|
-
* Zero external dependencies: uses only JSON and TypeScript types.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Error raised when an LLM response fails schema validation.
|
|
15
|
-
* Carries both a human-readable message and the raw text so callers
|
|
16
|
-
* can log / retry with full context.
|
|
17
|
-
*/
|
|
18
|
-
export class SchemaValidationError extends Error {
|
|
19
|
-
raw: string;
|
|
20
|
-
|
|
21
|
-
constructor(message: string, raw: string = "") {
|
|
22
|
-
super(message);
|
|
23
|
-
this.name = "SchemaValidationError";
|
|
24
|
-
this.raw = raw;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* One step in a task plan (mirrors PipelineStep / Task)
|
|
30
|
-
*/
|
|
31
|
-
export interface TaskStepSchema {
|
|
32
|
-
id: string | number;
|
|
33
|
-
description: string;
|
|
34
|
-
agent?: string;
|
|
35
|
-
depends_on?: string[];
|
|
36
|
-
priority?: "low" | "medium" | "high";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Full task plan output from Snow's orchestrator
|
|
41
|
-
*/
|
|
42
|
-
export interface TaskPlanSchema {
|
|
43
|
-
goal: string;
|
|
44
|
-
steps: TaskStepSchema[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* A single extracted fact for long-term memory
|
|
49
|
-
*/
|
|
50
|
-
export interface FactSchema {
|
|
51
|
-
key: string;
|
|
52
|
-
value: string;
|
|
53
|
-
category?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Structured fact-extraction output from the LLM
|
|
58
|
-
*/
|
|
59
|
-
export interface ExtractionResultSchema {
|
|
60
|
-
facts: FactSchema[];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Tool call schema for LLM responses
|
|
65
|
-
*/
|
|
66
|
-
export interface ToolCallSchema {
|
|
67
|
-
name: string;
|
|
68
|
-
arguments: Record<string, unknown>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Message schema for agent communication
|
|
73
|
-
*/
|
|
74
|
-
export interface MessageSchema {
|
|
75
|
-
role: "user" | "assistant" | "tool";
|
|
76
|
-
content: string;
|
|
77
|
-
tool_calls?: ToolCallSchema[];
|
|
78
|
-
tool_call_id?: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Coerce a value to a target type with best-effort conversion
|
|
83
|
-
*/
|
|
84
|
-
function coerceType(value: unknown, targetType: string): unknown {
|
|
85
|
-
if (value === null || value === undefined) {
|
|
86
|
-
return value;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
switch (targetType) {
|
|
90
|
-
case "string":
|
|
91
|
-
return String(value);
|
|
92
|
-
case "number":
|
|
93
|
-
return Number(value);
|
|
94
|
-
case "boolean":
|
|
95
|
-
return Boolean(value);
|
|
96
|
-
case "array":
|
|
97
|
-
return Array.isArray(value) ? value : [];
|
|
98
|
-
case "object":
|
|
99
|
-
return typeof value === "object" ? value : {};
|
|
100
|
-
default:
|
|
101
|
-
return value;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Extract JSON object/array from a potentially malformed string
|
|
107
|
-
*/
|
|
108
|
-
function extractJSON(text: string): string {
|
|
109
|
-
let cleaned = text.trim();
|
|
110
|
-
|
|
111
|
-
// Strip markdown code fences
|
|
112
|
-
if (cleaned.includes("```")) {
|
|
113
|
-
for (const fence of ["```json", "```"]) {
|
|
114
|
-
if (cleaned.includes(fence)) {
|
|
115
|
-
const after = cleaned.split(fence, 1)[1];
|
|
116
|
-
if (after && after.includes("```")) {
|
|
117
|
-
cleaned = after.split("```")[0].trim();
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Find first JSON object or array
|
|
125
|
-
let objStart = -1;
|
|
126
|
-
let depth = 0;
|
|
127
|
-
for (let i = 0; i < cleaned.length; i++) {
|
|
128
|
-
const ch = cleaned[i];
|
|
129
|
-
if (ch === "{") {
|
|
130
|
-
if (objStart < 0) objStart = i;
|
|
131
|
-
depth++;
|
|
132
|
-
} else if (ch === "}") {
|
|
133
|
-
depth--;
|
|
134
|
-
if (depth === 0 && objStart >= 0) {
|
|
135
|
-
return cleaned.substring(objStart, i + 1);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// If still not closed, close with extra braces
|
|
141
|
-
if (objStart >= 0) {
|
|
142
|
-
return cleaned.substring(objStart) + "}".repeat(depth);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
throw new SchemaValidationError("No valid JSON found in response", text);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Repair common JSON issues (trailing commas, unquoted keys, etc.)
|
|
150
|
-
*/
|
|
151
|
-
function repairJSON(text: string): string {
|
|
152
|
-
let repaired = text;
|
|
153
|
-
|
|
154
|
-
// Remove trailing commas
|
|
155
|
-
repaired = repaired.replace(/,\s*([}\]])/g, "$1");
|
|
156
|
-
|
|
157
|
-
// Quote unquoted keys
|
|
158
|
-
repaired = repaired.replace(/(?<!["\'\w])(\w[\w\d_]*)(\s*:)/g, '"$1"$2');
|
|
159
|
-
|
|
160
|
-
// Normalize quotes
|
|
161
|
-
repaired = repaired.replace(/'/g, '"').replace(/`/g, '"');
|
|
162
|
-
|
|
163
|
-
return repaired;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Parse a raw LLM response string into a typed JSON object.
|
|
168
|
-
* Handles markdown fences, leading/trailing text, and minor JSON quirks.
|
|
169
|
-
* Raises SchemaValidationError on failure.
|
|
170
|
-
*/
|
|
171
|
-
export function parseSchema<T extends Record<string, unknown>>(
|
|
172
|
-
raw: string,
|
|
173
|
-
schemaType?: new () => T
|
|
174
|
-
): T {
|
|
175
|
-
if (!raw || !raw.trim()) {
|
|
176
|
-
throw new SchemaValidationError("empty response", raw);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
const extracted = extractJSON(raw);
|
|
181
|
-
let data = JSON.parse(extracted);
|
|
182
|
-
return data as T;
|
|
183
|
-
} catch (error) {
|
|
184
|
-
if (error instanceof SchemaValidationError) throw error;
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const repaired = repairJSON(raw);
|
|
188
|
-
const data = JSON.parse(repaired);
|
|
189
|
-
return data as T;
|
|
190
|
-
} catch (repairError) {
|
|
191
|
-
throw new SchemaValidationError(
|
|
192
|
-
`JSON parse failed: ${(error as Error).message}`,
|
|
193
|
-
raw
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Validate task plan schema
|
|
201
|
-
*/
|
|
202
|
-
export function validateTaskPlan(data: unknown): TaskPlanSchema {
|
|
203
|
-
if (!data || typeof data !== "object") {
|
|
204
|
-
throw new SchemaValidationError("Invalid task plan: must be an object");
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const plan = data as Record<string, unknown>;
|
|
208
|
-
|
|
209
|
-
if (typeof plan.goal !== "string") {
|
|
210
|
-
throw new SchemaValidationError("Invalid task plan: goal must be a string");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (!Array.isArray(plan.steps)) {
|
|
214
|
-
throw new SchemaValidationError("Invalid task plan: steps must be an array");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const steps = (plan.steps as unknown[]).map((step: unknown) => {
|
|
218
|
-
if (!step || typeof step !== "object") {
|
|
219
|
-
throw new SchemaValidationError("Invalid task plan: step must be an object");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const s = step as Record<string, unknown>;
|
|
223
|
-
if (typeof s.id !== "string" && typeof s.id !== "number") {
|
|
224
|
-
throw new SchemaValidationError("Invalid task plan: step.id must be string or number");
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (typeof s.description !== "string") {
|
|
228
|
-
throw new SchemaValidationError("Invalid task plan: step.description must be a string");
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
id: s.id,
|
|
233
|
-
description: s.description,
|
|
234
|
-
agent: typeof s.agent === "string" ? s.agent : "rain",
|
|
235
|
-
depends_on: Array.isArray(s.depends_on) ? (s.depends_on as string[]) : [],
|
|
236
|
-
priority: (["low", "medium", "high"].includes(s.priority as string)
|
|
237
|
-
? s.priority
|
|
238
|
-
: "medium") as "low" | "medium" | "high",
|
|
239
|
-
};
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
goal: plan.goal,
|
|
244
|
-
steps,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Validate extraction result schema
|
|
250
|
-
*/
|
|
251
|
-
export function validateExtractionResult(data: unknown): ExtractionResultSchema {
|
|
252
|
-
if (!data || typeof data !== "object") {
|
|
253
|
-
throw new SchemaValidationError("Invalid extraction result: must be an object");
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const result = data as Record<string, unknown>;
|
|
257
|
-
|
|
258
|
-
if (!Array.isArray(result.facts)) {
|
|
259
|
-
throw new SchemaValidationError("Invalid extraction result: facts must be an array");
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const facts = (result.facts as unknown[]).map((fact: unknown) => {
|
|
263
|
-
if (!fact || typeof fact !== "object") {
|
|
264
|
-
throw new SchemaValidationError("Invalid extraction result: fact must be an object");
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const f = fact as Record<string, unknown>;
|
|
268
|
-
if (typeof f.key !== "string" || typeof f.value !== "string") {
|
|
269
|
-
throw new SchemaValidationError(
|
|
270
|
-
"Invalid extraction result: fact must have key and value strings"
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
key: f.key,
|
|
276
|
-
value: f.value,
|
|
277
|
-
category: typeof f.category === "string" ? f.category : "auto_extracted",
|
|
278
|
-
};
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
return { facts };
|
|
282
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight structured output schemas for LLM response validation.
|
|
3
|
+
*
|
|
4
|
+
* Why: LLM JSON output is inherently fragile — models emit markdown fences,
|
|
5
|
+
* trailing commas, unquoted keys, or hallucinated fields. Rather than layering
|
|
6
|
+
* heuristic repair (which silently passes corrupted data), we define typed
|
|
7
|
+
* schemas and validate on ingress. Parsing failures surface immediately so the
|
|
8
|
+
* caller can retry with a corrected prompt instead of propagating garbage.
|
|
9
|
+
*
|
|
10
|
+
* Zero external dependencies: uses only JSON and TypeScript types.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Error raised when an LLM response fails schema validation.
|
|
15
|
+
* Carries both a human-readable message and the raw text so callers
|
|
16
|
+
* can log / retry with full context.
|
|
17
|
+
*/
|
|
18
|
+
export class SchemaValidationError extends Error {
|
|
19
|
+
raw: string;
|
|
20
|
+
|
|
21
|
+
constructor(message: string, raw: string = "") {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "SchemaValidationError";
|
|
24
|
+
this.raw = raw;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One step in a task plan (mirrors PipelineStep / Task)
|
|
30
|
+
*/
|
|
31
|
+
export interface TaskStepSchema {
|
|
32
|
+
id: string | number;
|
|
33
|
+
description: string;
|
|
34
|
+
agent?: string;
|
|
35
|
+
depends_on?: string[];
|
|
36
|
+
priority?: "low" | "medium" | "high";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Full task plan output from Snow's orchestrator
|
|
41
|
+
*/
|
|
42
|
+
export interface TaskPlanSchema {
|
|
43
|
+
goal: string;
|
|
44
|
+
steps: TaskStepSchema[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A single extracted fact for long-term memory
|
|
49
|
+
*/
|
|
50
|
+
export interface FactSchema {
|
|
51
|
+
key: string;
|
|
52
|
+
value: string;
|
|
53
|
+
category?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Structured fact-extraction output from the LLM
|
|
58
|
+
*/
|
|
59
|
+
export interface ExtractionResultSchema {
|
|
60
|
+
facts: FactSchema[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tool call schema for LLM responses
|
|
65
|
+
*/
|
|
66
|
+
export interface ToolCallSchema {
|
|
67
|
+
name: string;
|
|
68
|
+
arguments: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Message schema for agent communication
|
|
73
|
+
*/
|
|
74
|
+
export interface MessageSchema {
|
|
75
|
+
role: "user" | "assistant" | "tool";
|
|
76
|
+
content: string;
|
|
77
|
+
tool_calls?: ToolCallSchema[];
|
|
78
|
+
tool_call_id?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Coerce a value to a target type with best-effort conversion
|
|
83
|
+
*/
|
|
84
|
+
function coerceType(value: unknown, targetType: string): unknown {
|
|
85
|
+
if (value === null || value === undefined) {
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
switch (targetType) {
|
|
90
|
+
case "string":
|
|
91
|
+
return String(value);
|
|
92
|
+
case "number":
|
|
93
|
+
return Number(value);
|
|
94
|
+
case "boolean":
|
|
95
|
+
return Boolean(value);
|
|
96
|
+
case "array":
|
|
97
|
+
return Array.isArray(value) ? value : [];
|
|
98
|
+
case "object":
|
|
99
|
+
return typeof value === "object" ? value : {};
|
|
100
|
+
default:
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extract JSON object/array from a potentially malformed string
|
|
107
|
+
*/
|
|
108
|
+
function extractJSON(text: string): string {
|
|
109
|
+
let cleaned = text.trim();
|
|
110
|
+
|
|
111
|
+
// Strip markdown code fences
|
|
112
|
+
if (cleaned.includes("```")) {
|
|
113
|
+
for (const fence of ["```json", "```"]) {
|
|
114
|
+
if (cleaned.includes(fence)) {
|
|
115
|
+
const after = cleaned.split(fence, 1)[1];
|
|
116
|
+
if (after && after.includes("```")) {
|
|
117
|
+
cleaned = after.split("```")[0].trim();
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Find first JSON object or array
|
|
125
|
+
let objStart = -1;
|
|
126
|
+
let depth = 0;
|
|
127
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
128
|
+
const ch = cleaned[i];
|
|
129
|
+
if (ch === "{") {
|
|
130
|
+
if (objStart < 0) objStart = i;
|
|
131
|
+
depth++;
|
|
132
|
+
} else if (ch === "}") {
|
|
133
|
+
depth--;
|
|
134
|
+
if (depth === 0 && objStart >= 0) {
|
|
135
|
+
return cleaned.substring(objStart, i + 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// If still not closed, close with extra braces
|
|
141
|
+
if (objStart >= 0) {
|
|
142
|
+
return cleaned.substring(objStart) + "}".repeat(depth);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw new SchemaValidationError("No valid JSON found in response", text);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Repair common JSON issues (trailing commas, unquoted keys, etc.)
|
|
150
|
+
*/
|
|
151
|
+
function repairJSON(text: string): string {
|
|
152
|
+
let repaired = text;
|
|
153
|
+
|
|
154
|
+
// Remove trailing commas
|
|
155
|
+
repaired = repaired.replace(/,\s*([}\]])/g, "$1");
|
|
156
|
+
|
|
157
|
+
// Quote unquoted keys
|
|
158
|
+
repaired = repaired.replace(/(?<!["\'\w])(\w[\w\d_]*)(\s*:)/g, '"$1"$2');
|
|
159
|
+
|
|
160
|
+
// Normalize quotes
|
|
161
|
+
repaired = repaired.replace(/'/g, '"').replace(/`/g, '"');
|
|
162
|
+
|
|
163
|
+
return repaired;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parse a raw LLM response string into a typed JSON object.
|
|
168
|
+
* Handles markdown fences, leading/trailing text, and minor JSON quirks.
|
|
169
|
+
* Raises SchemaValidationError on failure.
|
|
170
|
+
*/
|
|
171
|
+
export function parseSchema<T extends Record<string, unknown>>(
|
|
172
|
+
raw: string,
|
|
173
|
+
schemaType?: new () => T
|
|
174
|
+
): T {
|
|
175
|
+
if (!raw || !raw.trim()) {
|
|
176
|
+
throw new SchemaValidationError("empty response", raw);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const extracted = extractJSON(raw);
|
|
181
|
+
let data = JSON.parse(extracted);
|
|
182
|
+
return data as T;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof SchemaValidationError) throw error;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const repaired = repairJSON(raw);
|
|
188
|
+
const data = JSON.parse(repaired);
|
|
189
|
+
return data as T;
|
|
190
|
+
} catch (repairError) {
|
|
191
|
+
throw new SchemaValidationError(
|
|
192
|
+
`JSON parse failed: ${(error as Error).message}`,
|
|
193
|
+
raw
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validate task plan schema
|
|
201
|
+
*/
|
|
202
|
+
export function validateTaskPlan(data: unknown): TaskPlanSchema {
|
|
203
|
+
if (!data || typeof data !== "object") {
|
|
204
|
+
throw new SchemaValidationError("Invalid task plan: must be an object");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const plan = data as Record<string, unknown>;
|
|
208
|
+
|
|
209
|
+
if (typeof plan.goal !== "string") {
|
|
210
|
+
throw new SchemaValidationError("Invalid task plan: goal must be a string");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!Array.isArray(plan.steps)) {
|
|
214
|
+
throw new SchemaValidationError("Invalid task plan: steps must be an array");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const steps = (plan.steps as unknown[]).map((step: unknown) => {
|
|
218
|
+
if (!step || typeof step !== "object") {
|
|
219
|
+
throw new SchemaValidationError("Invalid task plan: step must be an object");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const s = step as Record<string, unknown>;
|
|
223
|
+
if (typeof s.id !== "string" && typeof s.id !== "number") {
|
|
224
|
+
throw new SchemaValidationError("Invalid task plan: step.id must be string or number");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (typeof s.description !== "string") {
|
|
228
|
+
throw new SchemaValidationError("Invalid task plan: step.description must be a string");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
id: s.id,
|
|
233
|
+
description: s.description,
|
|
234
|
+
agent: typeof s.agent === "string" ? s.agent : "rain",
|
|
235
|
+
depends_on: Array.isArray(s.depends_on) ? (s.depends_on as string[]) : [],
|
|
236
|
+
priority: (["low", "medium", "high"].includes(s.priority as string)
|
|
237
|
+
? s.priority
|
|
238
|
+
: "medium") as "low" | "medium" | "high",
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
goal: plan.goal,
|
|
244
|
+
steps,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Validate extraction result schema
|
|
250
|
+
*/
|
|
251
|
+
export function validateExtractionResult(data: unknown): ExtractionResultSchema {
|
|
252
|
+
if (!data || typeof data !== "object") {
|
|
253
|
+
throw new SchemaValidationError("Invalid extraction result: must be an object");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const result = data as Record<string, unknown>;
|
|
257
|
+
|
|
258
|
+
if (!Array.isArray(result.facts)) {
|
|
259
|
+
throw new SchemaValidationError("Invalid extraction result: facts must be an array");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const facts = (result.facts as unknown[]).map((fact: unknown) => {
|
|
263
|
+
if (!fact || typeof fact !== "object") {
|
|
264
|
+
throw new SchemaValidationError("Invalid extraction result: fact must be an object");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const f = fact as Record<string, unknown>;
|
|
268
|
+
if (typeof f.key !== "string" || typeof f.value !== "string") {
|
|
269
|
+
throw new SchemaValidationError(
|
|
270
|
+
"Invalid extraction result: fact must have key and value strings"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
key: f.key,
|
|
276
|
+
value: f.value,
|
|
277
|
+
category: typeof f.category === "string" ? f.category : "auto_extracted",
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return { facts };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* ════════════════════════════════════════
|
|
285
|
+
Structured-output retry
|
|
286
|
+
════════════════════════════════════════ */
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Ask an LLM for structured output and validate it, retrying with the parse
|
|
290
|
+
* error fed back as a correction when it comes back malformed — the pattern
|
|
291
|
+
* every production agent framework uses (LangGraph structured output, Pydantic
|
|
292
|
+
* AI, OpenAI Agents SDK output types). Without it, a single bad JSON response
|
|
293
|
+
* silently degrades the run (e.g. a multi-step plan collapses to one task).
|
|
294
|
+
*
|
|
295
|
+
* `ask(priorError, attempt)` produces the raw model text; on a retry it receives
|
|
296
|
+
* the previous validation error so the caller can append a correction to the
|
|
297
|
+
* prompt. `parse` must THROW (ideally SchemaValidationError) on invalid input.
|
|
298
|
+
* Returns the first valid value, or throws after exhausting `retries`.
|
|
299
|
+
*/
|
|
300
|
+
export async function parseWithRetry<T>(
|
|
301
|
+
ask: (priorError: string | null, attempt: number) => Promise<string>,
|
|
302
|
+
parse: (raw: string) => T,
|
|
303
|
+
opts?: { retries?: number; onRetry?: (attempt: number, error: string) => void },
|
|
304
|
+
): Promise<T> {
|
|
305
|
+
const retries = Math.max(0, opts?.retries ?? 2);
|
|
306
|
+
let lastError = "";
|
|
307
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
308
|
+
const raw = await ask(attempt === 0 ? null : lastError, attempt);
|
|
309
|
+
try {
|
|
310
|
+
return parse(raw);
|
|
311
|
+
} catch (e) {
|
|
312
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
313
|
+
if (attempt < retries) opts?.onRetry?.(attempt + 1, lastError);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
throw new SchemaValidationError(
|
|
317
|
+
`structured output still invalid after ${retries + 1} attempt(s): ${lastError}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
package/src/core/security.ts
CHANGED
|
@@ -59,11 +59,34 @@ const TOOL_DANGER_MAP: Record<string, DangerLevel> = {
|
|
|
59
59
|
list_skills: DangerLevel.SAFE,
|
|
60
60
|
recall_facts: DangerLevel.SAFE,
|
|
61
61
|
mcp_list_servers: DangerLevel.SAFE,
|
|
62
|
+
// read-only introspection (extra.ts)
|
|
63
|
+
file_info: DangerLevel.SAFE,
|
|
64
|
+
hash: DangerLevel.SAFE,
|
|
65
|
+
base64: DangerLevel.SAFE,
|
|
66
|
+
json_query: DangerLevel.SAFE,
|
|
67
|
+
dns_lookup: DangerLevel.SAFE,
|
|
68
|
+
port_check: DangerLevel.SAFE,
|
|
69
|
+
env_get: DangerLevel.SAFE,
|
|
70
|
+
disk_usage: DangerLevel.SAFE,
|
|
71
|
+
clipboard_read: DangerLevel.SAFE,
|
|
72
|
+
which: DangerLevel.SAFE,
|
|
73
|
+
diff_files: DangerLevel.SAFE,
|
|
74
|
+
uuid: DangerLevel.SAFE,
|
|
75
|
+
random_string: DangerLevel.SAFE,
|
|
76
|
+
current_time: DangerLevel.SAFE,
|
|
62
77
|
|
|
63
78
|
write_file: DangerLevel.LOW,
|
|
64
79
|
edit_file: DangerLevel.LOW,
|
|
65
80
|
copy_file: DangerLevel.LOW,
|
|
66
81
|
move_file: DangerLevel.LOW,
|
|
82
|
+
make_directory: DangerLevel.LOW,
|
|
83
|
+
append_file: DangerLevel.LOW,
|
|
84
|
+
replace_in_file: DangerLevel.LOW,
|
|
85
|
+
sqlite_query: DangerLevel.LOW,
|
|
86
|
+
gzip_file: DangerLevel.LOW,
|
|
87
|
+
gunzip_file: DangerLevel.LOW,
|
|
88
|
+
clipboard_write: DangerLevel.LOW,
|
|
89
|
+
git_branch: DangerLevel.LOW,
|
|
67
90
|
http_get: DangerLevel.LOW,
|
|
68
91
|
fetch_page: DangerLevel.LOW,
|
|
69
92
|
web_search: DangerLevel.LOW,
|
|
@@ -76,6 +99,8 @@ const TOOL_DANGER_MAP: Record<string, DangerLevel> = {
|
|
|
76
99
|
git_commit: DangerLevel.MEDIUM,
|
|
77
100
|
git_checkout: DangerLevel.MEDIUM,
|
|
78
101
|
http_post: DangerLevel.MEDIUM,
|
|
102
|
+
http_request: DangerLevel.MEDIUM,
|
|
103
|
+
download_file: DangerLevel.MEDIUM,
|
|
79
104
|
mcp_add_server: DangerLevel.MEDIUM,
|
|
80
105
|
mcp_remove_server: DangerLevel.MEDIUM,
|
|
81
106
|
launch_app: DangerLevel.MEDIUM,
|
|
@@ -88,6 +113,8 @@ const TOOL_DANGER_MAP: Record<string, DangerLevel> = {
|
|
|
88
113
|
package_manager: DangerLevel.HIGH,
|
|
89
114
|
service_control: DangerLevel.HIGH,
|
|
90
115
|
delegate_to: DangerLevel.HIGH,
|
|
116
|
+
git_push: DangerLevel.HIGH,
|
|
117
|
+
git_pull: DangerLevel.HIGH,
|
|
91
118
|
mcp_scaffold_server: DangerLevel.HIGH,
|
|
92
119
|
};
|
|
93
120
|
|