forked from decolua/9router
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtoolParser.js
More file actions
116 lines (100 loc) · 3.87 KB
/
Copy pathtoolParser.js
File metadata and controls
116 lines (100 loc) · 3.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import { createHash } from "node:crypto";
import { asRecord, DevinAgenticBridgeError } from "./types.js";
function stableJson(value) {
if (Array.isArray(value)) return `[${value.map((item) => stableJson(item)).join(",")}]`;
if (value && typeof value === "object") {
return `{${Object.entries(value)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, val]) => `${JSON.stringify(key)}:${stableJson(val)}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
function typeOf(value) {
if (Array.isArray(value)) return "array";
if (value === null) return "null";
return typeof value;
}
function validateSchema(value, schema, path) {
const errors = [];
const expectedType = schema.type;
if (typeof expectedType === "string") {
const actual = typeOf(value);
if (expectedType === "integer") {
if (!Number.isInteger(value)) errors.push(`${path} must be integer`);
} else if (actual !== expectedType) {
errors.push(`${path} must be ${expectedType}, got ${actual}`);
}
}
if (Array.isArray(schema.enum) && !schema.enum.some((item) => item === value)) {
errors.push(
`${path} must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}`
);
}
if (schema.type === "object" || (value && typeof value === "object" && !Array.isArray(value))) {
const record = asRecord(value);
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
for (const key of required) {
if (!(key in record)) errors.push(`${path}.${key} is required`);
}
const properties = asRecord(schema.properties);
for (const [key, propSchema] of Object.entries(properties)) {
if (key in record) errors.push(...validateSchema(record[key], asRecord(propSchema), `${path}.${key}`));
}
if (schema.additionalProperties === false) {
for (const key of Object.keys(record)) {
if (!(key in properties)) errors.push(`${path}.${key} is not allowed`);
}
}
}
if (Array.isArray(value) && schema.items) {
const itemSchema = asRecord(schema.items);
value.forEach((item, index) =>
errors.push(...validateSchema(item, itemSchema, `${path}[${index}]`))
);
}
return errors;
}
export function parseDevinToolRequest(text, tools, idSeed = "") {
const matches = [...text.matchAll(/<tool>\s*([\s\S]*?)\s*<\/tool>/g)];
if (matches.length === 0) return null;
if (matches.length > 1) {
throw new DevinAgenticBridgeError(
"Devin response contained more than one tool request; parallel tool use is not supported",
"multiple_tool_requests"
);
}
if (text.trim() !== matches[0][0].trim()) {
throw new DevinAgenticBridgeError(
"Devin tool request must be a standalone tool envelope without narrative text",
"mixed_tool_narrative"
);
}
let payload;
try {
payload = asRecord(JSON.parse(matches[0][1] || "{}"));
} catch {
throw new DevinAgenticBridgeError("Devin tool request was not valid JSON", "invalid_tool_json");
}
const name = typeof payload.name === "string" ? payload.name.trim() : "";
if (!name)
throw new DevinAgenticBridgeError("Devin tool request is missing name", "missing_tool_name");
const tool = tools.find((candidate) => candidate.name === name);
if (!tool) {
throw new DevinAgenticBridgeError(`Devin requested unknown tool: ${name}`, "unknown_tool");
}
const input = asRecord(payload.arguments);
const schema = tool.input_schema || { type: "object", properties: {} };
const errors = validateSchema(input, schema, "arguments");
if (errors.length > 0) {
throw new DevinAgenticBridgeError(
`Devin tool arguments failed schema validation: ${errors.join("; ")}`,
"invalid_tool_arguments"
);
}
const digest = createHash("sha256")
.update(`${idSeed}:${name}:${stableJson(input)}`)
.digest("hex")
.slice(0, 16);
return { id: `tool_devin_${digest}`, name, input };
}