sygnal 6.0.0 → 6.1.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 +70 -0
- package/dist/ai.cjs.js +115 -0
- package/dist/ai.cjs.js.map +1 -0
- package/dist/ai.esm.js +2 -0
- package/dist/ai.esm.js.map +1 -0
- package/dist/astro/index.cjs.js +599 -4
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +599 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js +413 -4
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.esm.js +413 -5
- package/dist/devtools.esm.js.map +1 -1
- package/dist/diagnostics.cjs.js +170 -12
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +170 -12
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/accessibility.md +54 -1
- package/dist/guide/agent.md +588 -0
- package/dist/guide/ai-chat.md +693 -0
- package/dist/guide/ai-decisions.md +330 -0
- package/dist/guide/forms-reference.md +28 -0
- package/dist/guide/forms.md +2 -1
- package/dist/guide/http.md +2 -0
- package/dist/guide/mcp-apps.md +272 -0
- package/dist/guide/recipes/ai-form-fill.md +163 -0
- package/dist/guide/recipes/ai-summarize.md +155 -0
- package/dist/guide/recipes/ai-support-inbox.md +190 -0
- package/dist/guide/recipes/overview.md +10 -0
- package/dist/guide/webmcp.md +226 -0
- package/dist/index.cjs.js +7841 -3370
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1288 -6
- package/dist/index.esm.js +7139 -2696
- package/dist/index.esm.js.map +1 -1
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vite/plugin.cjs.js +599 -4
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +599 -4
- package/dist/vite/plugin.mjs.map +1 -1
- package/llms.txt +7 -1
- package/package.json +15 -1
- package/src/ai.d.ts +1186 -0
- package/src/ai.ts +23 -0
- package/src/core/hooks.ts +1 -1
- package/src/devtools.d.ts +20 -1
- package/src/devtools.ts +3 -0
- package/src/extra/ai/agent/index.ts +484 -0
- package/src/extra/ai/answers.ts +115 -0
- package/src/extra/ai/chat/behavior.ts +336 -0
- package/src/extra/ai/chat/driver.ts +271 -0
- package/src/extra/ai/chat/memoryTransport.ts +61 -0
- package/src/extra/ai/chat/output.ts +37 -0
- package/src/extra/ai/commandBar.ts +281 -0
- package/src/extra/ai/decide.ts +104 -0
- package/src/extra/ai/index.ts +42 -0
- package/src/extra/ai/link.ts +90 -0
- package/src/extra/ai/mcpApp.ts +314 -0
- package/src/extra/ai/messages.ts +51 -0
- package/src/extra/ai/schema/index.ts +166 -0
- package/src/extra/ai/schema/jsonSchema.ts +72 -0
- package/src/extra/ai/schema/strict.ts +152 -0
- package/src/extra/ai/transports/agui.ts +177 -0
- package/src/extra/ai/transports/anthropicMessages.ts +175 -0
- package/src/extra/ai/transports/chatCompletions.ts +105 -0
- package/src/extra/ai/transports/chromePrompt.ts +77 -0
- package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
- package/src/extra/ai/transports/fromAISDK.ts +126 -0
- package/src/extra/ai/transports/openResponses.ts +105 -0
- package/src/extra/ai/transports/shared.ts +125 -0
- package/src/extra/ai/transports/tools.ts +59 -0
- package/src/extra/ai/transports/uiMessageStream.ts +107 -0
- package/src/extra/ai/webmcp.ts +260 -0
- package/src/extra/copyAsTest.ts +2 -2
- package/src/extra/devMcp.ts +386 -0
- package/src/extra/devtoolsActions.ts +1 -1
- package/src/extra/diagnostics/checks/actionLog.ts +3 -3
- package/src/extra/diagnostics/checks/chat.ts +71 -0
- package/src/extra/diagnostics/checks/forms.ts +8 -0
- package/src/extra/diagnostics/checks/index.ts +6 -0
- package/src/extra/diagnostics/checks/public.d.ts +19 -2
- package/src/extra/diagnostics/checks/wiring.ts +5 -2
- package/src/extra/diagnostics/codes.ts +67 -0
- package/src/extra/form.ts +22 -6
- package/src/extra/formTool.ts +150 -0
- package/src/extra/testing.ts +125 -8
- package/src/index.d.ts +116 -3
- package/src/index.ts +21 -1
- package/src/shared.ts +9 -0
- package/src/vite/mcp.ts +568 -0
- package/src/vite/plugin.d.ts +27 -2
- package/src/vite/plugin.ts +43 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-6 A-1 (0-S4): the input contract of agent tools and of structured chat output.
|
|
3
|
+
*
|
|
4
|
+
* An `input` (or a chat request's `output`) is a Standard Schema that also implements Standard
|
|
5
|
+
* JSON Schema (Zod 4.2+, ArkType 2.1.28+, Valibot through `toStandardJsonSchema()`), or a plain
|
|
6
|
+
* JSON Schema wrapped by `jsonSchema()` (./jsonSchema.ts, D262). The model always gets the
|
|
7
|
+
* INPUT-side JSON Schema (it produces what `validate` receives), after the portable
|
|
8
|
+
* normalization below, which every consumer (the agent layer, WebMCP, the chat behavior, the
|
|
9
|
+
* chat driver's `output`, t.tools()) shares:
|
|
10
|
+
* - `$schema` / `$id` dropped, `oneOf` -> `anyOf`, Zod's safe-integer bounds dropped, a closed
|
|
11
|
+
* tuple's `items: false` -> `maxItems` (Ollama rejects boolean subschemas);
|
|
12
|
+
* - a root self-reference (`"$ref": "#"`, Zod's recursion) moved into `$defs.__root` (Ollama
|
|
13
|
+
* rejects `#`, and wrapping changes what it means); ArkType's `$ref`-only root inlined;
|
|
14
|
+
* - a non-object root wrapped as `{ value }` (`required`, `additionalProperties: false`); the
|
|
15
|
+
* unwrap is lenient: arguments without a `value` key are taken as the value itself (0-S4: both
|
|
16
|
+
* local models sent a wrapped object union bare).
|
|
17
|
+
* A schema part with no JSON Schema form (refinements, a Date) is retried with the library's
|
|
18
|
+
* lossy option (`lossy`: SYG243, a dev warning; validation still enforces it); no form at all is
|
|
19
|
+
* `error` (SYG240). Conversions are cached by schema object (ArkType rebuilds `~standard` on
|
|
20
|
+
* every read, G-608). The strict layer for OpenAI / Anthropic is the transports' (Phase 2), opt-in
|
|
21
|
+
through `strictSchemas` (./strict.ts, D285); both walk schemas with `walk` below.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The result of converting a schema: the tool schema, or why there is none (SYG240) */
|
|
25
|
+
export interface Converted {
|
|
26
|
+
/** the normalized, input-side JSON Schema; always an object root */
|
|
27
|
+
schema?: any
|
|
28
|
+
/** the root was wrapped as `{ value }` */
|
|
29
|
+
wrapped?: boolean
|
|
30
|
+
/** a lossy conversion (SYG243): what the library couldn't express */
|
|
31
|
+
lossy?: string
|
|
32
|
+
/** no JSON Schema form (SYG240) */
|
|
33
|
+
error?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// each library's "give me what you can" switch (Standard JSON Schema's libraryOptions)
|
|
37
|
+
const LOSSY: Record<string, any> = {zod: {unrepresentable: 'any'}, valibot: {errorMode: 'ignore'}, arktype: {fallback: (c: any) => c.base}}
|
|
38
|
+
const SAFE = 9007199254740991
|
|
39
|
+
const cache = new WeakMap<object, Converted>()
|
|
40
|
+
|
|
41
|
+
const msg = (e: any) => String(e?.message || e).split('\n')[0].slice(0, 200)
|
|
42
|
+
export const isObj = (x: any): boolean => !!x && typeof x == 'object' && !Array.isArray(x)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The JSON Schema a model gets for `schema` (a Standard Schema with Standard JSON Schema, or
|
|
46
|
+
* `jsonSchema()`): `{ schema, wrapped, lossy? }`, or `{ error }` when it has no JSON Schema form.
|
|
47
|
+
* Cached by schema object.
|
|
48
|
+
*/
|
|
49
|
+
export function toJsonSchema(input: any): Converted {
|
|
50
|
+
if (!input || (typeof input != 'object' && typeof input != 'function')) return {error: 'not a Standard Schema'}
|
|
51
|
+
let c = cache.get(input)
|
|
52
|
+
if (!c) cache.set(input, c = convert(input))
|
|
53
|
+
return c
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function convert(input: any): Converted {
|
|
57
|
+
const st = input['~standard']
|
|
58
|
+
if (typeof st?.validate != 'function') return {error: input.type || input.properties || input.anyOf || input.enum ? 'a plain JSON Schema: wrap it with jsonSchema()' : 'not a Standard Schema'}
|
|
59
|
+
const conv = st.jsonSchema?.input, v = st.vendor
|
|
60
|
+
if (typeof conv != 'function') return {error: `the ${v} schema has no JSON Schema form` + (v == 'valibot' ? ': wrap it with toStandardJsonSchema() from @valibot/to-json-schema' : v == 'zod' ? ' (Zod Mini): use jsonSchema(z.toJSONSchema(s), { validate: s })' : '')}
|
|
61
|
+
let js: any, lossy: string | undefined
|
|
62
|
+
try { js = conv({target: 'draft-2020-12'}) } catch (e) {
|
|
63
|
+
try { if (!LOSSY[v]) throw 0; js = conv({target: 'draft-2020-12', libraryOptions: LOSSY[v]}) } catch (_) { return {error: 'no JSON Schema form: ' + msg(e)} }
|
|
64
|
+
lossy = msg(e)
|
|
65
|
+
}
|
|
66
|
+
return {...normalize(js), ...(lossy && {lossy})}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// G-629: every subschema, bottom-up (shared with the strict layer, ./strict.ts). The values of
|
|
70
|
+
// `properties` / `$defs` / ... maps are schemas, the maps themselves aren't (a property named
|
|
71
|
+
// `oneOf` or `items` is a name, not a keyword), and `const` / `enum` / `default` / `examples` are
|
|
72
|
+
// data (a default of `{ oneOf: 1 }` stays as it is)
|
|
73
|
+
const MAPS = /^(properties|\$defs|definitions|patternProperties|dependentSchemas)$/
|
|
74
|
+
const DATA = /^(const|enum|default|examples)$/
|
|
75
|
+
export const walk = (s: any, f: (o: any) => any): any => {
|
|
76
|
+
if (Array.isArray(s)) return s.map(x => walk(x, f))
|
|
77
|
+
if (!s || typeof s != 'object') return s
|
|
78
|
+
const o: any = {}
|
|
79
|
+
for (const k in s) {
|
|
80
|
+
const v = s[k]
|
|
81
|
+
o[k] = DATA.test(k) ? v : MAPS.test(k) && isObj(v) ? Object.fromEntries(Object.entries(v).map(([n, x]) => [n, walk(x, f)])) : walk(v, f)
|
|
82
|
+
}
|
|
83
|
+
return f(o)
|
|
84
|
+
}
|
|
85
|
+
const rewriteRefs = (s: any, to: string) => walk(s, o => {
|
|
86
|
+
const r = o.$ref
|
|
87
|
+
if (typeof r == 'string' && (r == '#' || r[1] == '/') && !/^#\/(\$defs|definitions)\//.test(r)) o.$ref = to + r.slice(1)
|
|
88
|
+
return o
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/** the portable normalization (see the header): `{ schema, wrapped }` */
|
|
92
|
+
export function normalize(schema: any): {schema: any; wrapped: boolean} {
|
|
93
|
+
let {$schema, $id, ...s} = walk(schema, o => {
|
|
94
|
+
if ('oneOf' in o && !('anyOf' in o)) { o.anyOf = o.oneOf; delete o.oneOf }
|
|
95
|
+
if (o.type == 'integer' && o.minimum == -SAFE && o.maximum == SAFE) { delete o.minimum; delete o.maximum }
|
|
96
|
+
if (o.prefixItems && o.items === false) { delete o.items; o.maxItems ??= o.prefixItems.length }
|
|
97
|
+
return o
|
|
98
|
+
})
|
|
99
|
+
const m = typeof s.$ref == 'string' && /^#\/\$defs\/(.+)$/.exec(s.$ref)
|
|
100
|
+
if (m && s.$defs?.[m[1]]?.type == 'object' && Object.keys(s).length == 2) s = {...s.$defs[m[1]], $defs: s.$defs}
|
|
101
|
+
const {$defs, description, ...rest} = s
|
|
102
|
+
const defs: any = {...$defs}
|
|
103
|
+
const selfRef = /"\$ref":"#(?!\/\$defs\/|\/definitions\/)/.test(JSON.stringify(s))
|
|
104
|
+
if (selfRef) {
|
|
105
|
+
for (const k in defs) defs[k] = rewriteRefs(defs[k], '#/$defs/__root')
|
|
106
|
+
defs.__root = rewriteRefs(rest, '#/$defs/__root')
|
|
107
|
+
}
|
|
108
|
+
if (s.type == 'object') return {schema: selfRef ? {...defs.__root, ...(description && {description}), $defs: defs} : s, wrapped: false}
|
|
109
|
+
let value = selfRef ? {$ref: '#/$defs/__root'} : rest
|
|
110
|
+
if (description) value = {...value, description}
|
|
111
|
+
const out: any = {type: 'object', properties: {value}, required: ['value'], additionalProperties: false}
|
|
112
|
+
if (Object.keys(defs).length) out.$defs = defs
|
|
113
|
+
return {schema: out, wrapped: true}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const resolve = (s: any, root: any) => {
|
|
117
|
+
const m = /^#\/\$defs\/(.+)$/.exec(s.$ref)
|
|
118
|
+
return m ? root.$defs?.[m[1]] : s.$ref == '#' ? root : s
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* D264: small local models send "3" for an integer and "true" for a boolean. Where the JSON
|
|
123
|
+
* Schema says number / integer / boolean (and not string), a string that parses as one becomes
|
|
124
|
+
* one. Nothing else changes; validation still decides.
|
|
125
|
+
*/
|
|
126
|
+
export function repair(value: any, schema: any, root: any = schema): any {
|
|
127
|
+
if (schema?.$ref) schema = resolve(schema, root)
|
|
128
|
+
if (!schema) return value
|
|
129
|
+
const types = [].concat(schema.type ?? []) as string[]
|
|
130
|
+
if (typeof value == 'string' && !types.includes('string')) {
|
|
131
|
+
const t = value.trim()
|
|
132
|
+
if ((types.includes('integer') || types.includes('number')) && /^-?\d+(\.\d+)?$/.test(t)) {
|
|
133
|
+
const n = Number(t)
|
|
134
|
+
if (types.includes('number') || Number.isInteger(n)) return n
|
|
135
|
+
}
|
|
136
|
+
if (types.includes('boolean') && (t == 'true' || t == 'false')) return t == 'true'
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(value)) return value.map((x, i) => repair(x, schema.prefixItems?.[i] ?? schema.items, root))
|
|
139
|
+
if (isObj(value)) {
|
|
140
|
+
const o = {...value}
|
|
141
|
+
for (const b of schema.anyOf || [schema]) {
|
|
142
|
+
const bb = b?.$ref ? resolve(b, root) : b
|
|
143
|
+
for (const k in bb?.properties) if (k in o) o[k] = repair(o[k], bb.properties[k], root)
|
|
144
|
+
}
|
|
145
|
+
return o
|
|
146
|
+
}
|
|
147
|
+
if (typeof value == 'string' && schema.anyOf) for (const b of schema.anyOf) { const r = repair(value, b, root); if (r !== value) return r }
|
|
148
|
+
return value
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** One validation failure: the schema's message and the path as keys */
|
|
152
|
+
export interface Issue {message: string; path?: PropertyKey[]}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A model's arguments for `schema` -> the validated value: the lenient `{ value }` unwrap,
|
|
156
|
+
* `repair` (unless `repair: false`), then the schema's own validation. Resolves with
|
|
157
|
+
* `{ value }` (the schema's output), `{ issues }`, or `{ error }` (no JSON Schema form)
|
|
158
|
+
*/
|
|
159
|
+
export async function parseInput(schema: any, args: any, options: {repair?: boolean} = {}): Promise<{value?: any; issues?: Issue[]; error?: string}> {
|
|
160
|
+
const c = toJsonSchema(schema)
|
|
161
|
+
if (c.error) return {error: c.error}
|
|
162
|
+
let v = c.wrapped && isObj(args) && 'value' in args ? args.value : args
|
|
163
|
+
if (options.repair !== false) v = repair(v, c.wrapped ? c.schema.properties.value : c.schema, c.schema)
|
|
164
|
+
const r = await schema['~standard'].validate(v)
|
|
165
|
+
return r.issues ? {issues: r.issues.map((i: any) => ({message: i.message, ...(i.path && {path: i.path.map((p: any) => isObj(p) ? p.key : p)})}))} : {value: r.value}
|
|
166
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-6 A-1 (D262, 0-S4 §4): plain JSON Schema as a tool `input` (or a chat `output`), only
|
|
3
|
+
* through `jsonSchema(json[, { validate }])`. It returns a Standard Schema that is also a
|
|
4
|
+
* Standard JSON Schema, so it takes the same path as Zod / Valibot / ArkType. Without
|
|
5
|
+
* `validate`, a subset validator checks the value: type (integer and type arrays too), enum,
|
|
6
|
+
* const, required, properties, additionalProperties (false or a schema), items, prefixItems,
|
|
7
|
+
* anyOf / oneOf, minLength / maxLength, pattern, minimum / maximum, minItems / maxItems and local
|
|
8
|
+
* `$ref`s (`#`, `#/$defs/…`, `#/definitions/…`). format, allOf, not, if/then/else, multipleOf,
|
|
9
|
+
* uniqueItems, exclusive* and the rest are accepted unchecked. `{ validate: s }` keeps a
|
|
10
|
+
* library's own validation (Zod Mini: `jsonSchema(z.toJSONSchema(s), { validate: s })`).
|
|
11
|
+
* A separate module: tree-shaken unless imported.
|
|
12
|
+
*/
|
|
13
|
+
import type {Issue} from './index'
|
|
14
|
+
|
|
15
|
+
const T = (v: any) => v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v
|
|
16
|
+
const is = (t: string, v: any) => t == 'integer' ? Number.isInteger(v) : t == 'number' ? typeof v == 'number' && isFinite(v) : T(v) == t
|
|
17
|
+
const S = JSON.stringify
|
|
18
|
+
|
|
19
|
+
/** the subset check: the issues of `v` against `s` (empty when valid) */
|
|
20
|
+
export function check(s: any, v: any, root: any = s, path: PropertyKey[] = [], out: Issue[] = []): Issue[] {
|
|
21
|
+
const bad = (message: string) => (out.push({message, path}), out)
|
|
22
|
+
if (s === false) return bad('no value allowed')
|
|
23
|
+
if (!s || s === true) return out
|
|
24
|
+
if (s.$ref) return check(s.$ref == '#' ? root : root.$defs?.[s.$ref.slice(8)] ?? root.definitions?.[s.$ref.slice(14)], v, root, path, out)
|
|
25
|
+
const types = [].concat(s.type ?? []) as string[]
|
|
26
|
+
if (types.length && !types.some(t => is(t, v))) return bad(`expected ${types.join(' or ')}, got ${T(v)}`)
|
|
27
|
+
if (s.enum && !s.enum.some((e: any) => S(e) == S(v))) return bad(`expected one of ${S(s.enum)}`)
|
|
28
|
+
if ('const' in s && S(s.const) != S(v)) return bad(`expected ${S(s.const)}`)
|
|
29
|
+
const any = s.anyOf ?? s.oneOf
|
|
30
|
+
if (any && !any.some((b: any) => !check(b, v, root, path).length)) return bad('matches no alternative')
|
|
31
|
+
if (typeof v == 'string') {
|
|
32
|
+
if (v.length < (s.minLength ?? 0)) bad(`at least ${s.minLength} characters`)
|
|
33
|
+
if (v.length > (s.maxLength ?? Infinity)) bad(`at most ${s.maxLength} characters`)
|
|
34
|
+
if (s.pattern && !new RegExp(s.pattern, 'u').test(v)) bad(`must match ${s.pattern}`)
|
|
35
|
+
}
|
|
36
|
+
if (typeof v == 'number') {
|
|
37
|
+
if (v < (s.minimum ?? -Infinity)) bad(`at least ${s.minimum}`)
|
|
38
|
+
if (v > (s.maximum ?? Infinity)) bad(`at most ${s.maximum}`)
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(v)) {
|
|
41
|
+
if (v.length < (s.minItems ?? 0)) bad(`at least ${s.minItems} items`)
|
|
42
|
+
if (v.length > (s.maxItems ?? Infinity)) bad(`at most ${s.maxItems} items`)
|
|
43
|
+
const pre = s.prefixItems ?? []
|
|
44
|
+
v.forEach((x, i) => check(i < pre.length ? pre[i] : s.items, x, root, [...path, i], out))
|
|
45
|
+
} else if (T(v) == 'object') {
|
|
46
|
+
for (const k of s.required ?? []) if (!(k in v)) bad(`${k} is required`)
|
|
47
|
+
const p = s.properties ?? {}
|
|
48
|
+
for (const k in v) check(k in p ? p[k] : s.additionalProperties, v[k], root, [...path, k], out)
|
|
49
|
+
}
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A plain JSON Schema as a Standard Schema with Standard JSON Schema: the schema goes to the
|
|
55
|
+
* model as is (normalized), and the subset validator (or `options.validate`, a Standard Schema)
|
|
56
|
+
* checks the value
|
|
57
|
+
*/
|
|
58
|
+
export function jsonSchema(schema: any, options: {validate?: any} = {}): any {
|
|
59
|
+
const own = options.validate?.['~standard']
|
|
60
|
+
const out = () => schema
|
|
61
|
+
return {
|
|
62
|
+
'~standard': {
|
|
63
|
+
version: 1,
|
|
64
|
+
vendor: 'sygnal',
|
|
65
|
+
validate: own ? (v: any) => own.validate(v) : (value: any) => {
|
|
66
|
+
const issues = check(schema, value)
|
|
67
|
+
return issues.length ? {issues} : {value}
|
|
68
|
+
},
|
|
69
|
+
jsonSchema: {input: out, output: out},
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {resolve, walk, isObj} from './index';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* PLAN-6 L-2 (0-S4 layer 2, D266, D285): the opt-in strict schema layer of the OpenAI- and
|
|
5
|
+
* Anthropic-backed transports. Apps opt in with an import (`openResponses({ strict:
|
|
6
|
+
* strictSchemas })`), so the transports don't carry it otherwise (≈ 1.1 KB, D285); unstrict is
|
|
7
|
+
* also used by the chat driver's structured output (./chat/output.ts), where it is always safe.
|
|
8
|
+
*
|
|
9
|
+
* strictSchemas(schema, dialect) turns a portable tool / output schema (./index.ts normalize)
|
|
10
|
+
* into a provider's strict subset; both walk with A-1's map-aware `walk` (G-629):
|
|
11
|
+
* - 'openai' (developers.openai.com structured outputs, "Supported schemas", 2026-10):
|
|
12
|
+
* `additionalProperties: false` on every object, every property required, the optional ones
|
|
13
|
+
* made nullable (a type array + null in the enum, or `anyOf [..., { type: 'null' }]`);
|
|
14
|
+
* unsupported keywords and formats moved into the description as JSON (the model still reads
|
|
15
|
+
* them; validation still enforces them), a tuple's `prefixItems` turned into `items: { anyOf }`.
|
|
16
|
+
* - 'anthropic' (platform.claude.com structured outputs; the subset @anthropic-ai/sdk's
|
|
17
|
+
* transformJSONSchema keeps): `additionalProperties: false` on every object, `required` as is
|
|
18
|
+
* (optional keys stay optional), and only type, anyOf (oneOf), allOf, $ref/$defs, enum, const,
|
|
19
|
+
* description, title, properties, required, items, the supported formats and `minItems` 0/1
|
|
20
|
+
* kept; the rest (numeric and string bounds, pattern, other array constraints) moved into the
|
|
21
|
+
* description. A recursive schema or an untyped (any) value has no strict form.
|
|
22
|
+
* Both return `errors` when the schema has no strict form (a record: `additionalProperties` with a
|
|
23
|
+
* schema; a non-object root): the transport then sends that tool non-strict (SYG675, dev info),
|
|
24
|
+
* and `optional` / `unions` (the counts Anthropic limits per request, G-609).
|
|
25
|
+
*
|
|
26
|
+
* unstrict(value, schema) drops the nulls a strict schema forced the model to send for keys the
|
|
27
|
+
* original schema leaves optional and doesn't allow null for, so validation sees the original
|
|
28
|
+
* shape. The transports run it on every tool call they sent strict.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export type StrictDialect = 'openai' | 'anthropic';
|
|
32
|
+
export interface Strictified {schema: any; errors: string[]; optional: number; unions: number}
|
|
33
|
+
/** what a request has left of a provider's per-request strict limits (Anthropic, G-609) */
|
|
34
|
+
export interface StrictLimits {tools: number; optional: number; unions: number}
|
|
35
|
+
|
|
36
|
+
const DROP = ['allOf', 'not', 'if', 'then', 'else', 'dependentRequired', 'dependentSchemas', 'patternProperties',
|
|
37
|
+
'unevaluatedProperties', 'propertyNames', 'minProperties', 'maxProperties', 'unevaluatedItems', 'contains',
|
|
38
|
+
'minContains', 'maxContains', 'uniqueItems', 'default', 'examples', 'title'];
|
|
39
|
+
const FORMATS = ['date-time', 'time', 'date', 'duration', 'email', 'hostname', 'ipv4', 'ipv6', 'uuid'];
|
|
40
|
+
// Anthropic's subset (the keywords @anthropic-ai/sdk's transformJSONSchema keeps)
|
|
41
|
+
const KEEP = /^(type|anyOf|allOf|\$ref|\$defs|enum|const|description|title|properties|required|additionalProperties|items)$/;
|
|
42
|
+
const ANT_FORMATS = /^(date-time|time|date|duration|email|hostname|uri|ipv4|ipv6|uuid)$/;
|
|
43
|
+
|
|
44
|
+
/** one schema object into Anthropic's subset, in place (oneOf -> anyOf): the keywords it moved out */
|
|
45
|
+
export function anthropicKeywords(o: any): any {
|
|
46
|
+
const moved: any = {};
|
|
47
|
+
if (o.oneOf && !o.anyOf) { o.anyOf = o.oneOf; delete o.oneOf; }
|
|
48
|
+
for (const k in o) if (k == 'format' ? !ANT_FORMATS.test(o[k]) : k == 'minItems' ? o[k] > 1 : !KEEP.test(k)) { moved[k] = o[k]; delete o[k]; }
|
|
49
|
+
return moved;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// the moved keywords as JSON at the end of the description: the model still reads them, validation still enforces them
|
|
53
|
+
const describe = (o: any, moved: any) => {
|
|
54
|
+
if (Object.keys(moved).length) {
|
|
55
|
+
const hint = JSON.stringify(moved);
|
|
56
|
+
o.description = o.description ? o.description + ' ' + hint : hint;
|
|
57
|
+
}
|
|
58
|
+
return o;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* G-635: the light, always-on pass anthropicMessages() runs on a non-strict `output` schema
|
|
63
|
+
* (`output_config.format` is always constrained decoding, and a keyword outside Anthropic's subset
|
|
64
|
+
* is a 400): Anthropic's keywords only (the rest moved into the description), and
|
|
65
|
+
* `additionalProperties: false` on every object that isn't a record. No limits, no errors.
|
|
66
|
+
*/
|
|
67
|
+
export const anthropicCompatible = (schema: any): any => walk(schema, o => {
|
|
68
|
+
const moved = anthropicKeywords(o);
|
|
69
|
+
if ((o.type == 'object' || o.properties) && !isObj(o.additionalProperties)) o.additionalProperties = false;
|
|
70
|
+
return describe(o, moved);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const nullable = (p: any): any => {
|
|
74
|
+
const t = p.type, withEnum = (o: any) => p.enum ? {...o, enum: [...p.enum, null]} : o;
|
|
75
|
+
if (Array.isArray(t)) return t.includes('null') ? p : withEnum({...p, type: [...t, 'null']});
|
|
76
|
+
if (typeof t == 'string' && !p.$ref) return withEnum({...p, type: [t, 'null']});
|
|
77
|
+
if (p.anyOf) return p.anyOf.some((b: any) => b.type == 'null') ? p : {...p, anyOf: [...p.anyOf, {type: 'null'}]};
|
|
78
|
+
const {description, ...rest} = p;
|
|
79
|
+
return {anyOf: [rest, {type: 'null'}], ...(description && {description})};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// a `$defs` entry that reaches itself through `$ref`s (or a `#` root reference)
|
|
83
|
+
const recursive = (s: any): boolean => {
|
|
84
|
+
const defs = s.$defs || {}, refs = (x: any): string[] => [...JSON.stringify(x ?? null).matchAll(/"\$ref":"#(?:\/\$defs\/([^"]*))?"/g)].map(m => m[1] ?? '#');
|
|
85
|
+
const to = (a: string, b: string, seen: Set<string>): boolean => !seen.has(a) && (seen.add(a), refs(defs[a]).some(n => n == b || (n in defs && to(n, b, seen))));
|
|
86
|
+
return refs(s).includes('#') || Object.keys(defs).some(k => to(k, k, new Set()));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* `{ schema, errors, optional, unions }`: a provider's strict form of a portable schema, or why
|
|
91
|
+
* there is none. `left` (the transports'): what the request has left of the provider's
|
|
92
|
+
* per-request limits; a schema that fits is counted against it (`tool`: it is a tool), one that
|
|
93
|
+
* doesn't gets an error
|
|
94
|
+
*/
|
|
95
|
+
export function strictSchemas(schema: any, dialect: StrictDialect = 'openai', left?: StrictLimits, tool?: boolean): Strictified {
|
|
96
|
+
const errors: string[] = [], ant = dialect == 'anthropic';
|
|
97
|
+
let optional = 0, unions = 0;
|
|
98
|
+
if (schema?.type != 'object') errors.push('the root is not an object');
|
|
99
|
+
if (ant && schema && recursive(schema)) errors.push('a recursive schema has no strict form');
|
|
100
|
+
const out = walk(schema, o => {
|
|
101
|
+
let moved: any = {};
|
|
102
|
+
if (ant) {
|
|
103
|
+
moved = anthropicKeywords(o);
|
|
104
|
+
if (!o.type && !o.anyOf && !o.allOf && !o.$ref && !o.enum && !('const' in o) && !o.properties) errors.push('a value of any type has no strict form');
|
|
105
|
+
} else {
|
|
106
|
+
for (const k of DROP) if (k in o) { moved[k] = o[k]; delete o[k]; }
|
|
107
|
+
if (o.format && !FORMATS.includes(o.format)) { moved.format = o.format; delete o.format; }
|
|
108
|
+
if (o.prefixItems) { moved.prefixItems = 'tuple'; o.items = {anyOf: o.prefixItems}; delete o.prefixItems; }
|
|
109
|
+
}
|
|
110
|
+
if (o.items === false) delete o.items;
|
|
111
|
+
if (o.type == 'object' || o.properties) {
|
|
112
|
+
if (o.additionalProperties && o.additionalProperties !== false) errors.push('a record (additionalProperties with a schema) has no strict form');
|
|
113
|
+
const props = o.properties ||= {}, req = new Set(o.required || []);
|
|
114
|
+
for (const k in props) {
|
|
115
|
+
const p = props[k];
|
|
116
|
+
if (p?.anyOf || Array.isArray(p?.type)) unions++;
|
|
117
|
+
if (!req.has(k)) { optional++; if (!ant) props[k] = nullable(p); }
|
|
118
|
+
}
|
|
119
|
+
if (!ant) o.required = Object.keys(props);
|
|
120
|
+
o.additionalProperties = false;
|
|
121
|
+
}
|
|
122
|
+
return describe(o, moved);
|
|
123
|
+
});
|
|
124
|
+
if (left && !errors.length) {
|
|
125
|
+
if ((tool && left.tools < 1) || left.optional < optional || left.unions < unions) errors.push('over the per-request strict limits (20 tools, 24 optional and 16 union-typed parameters)');
|
|
126
|
+
else { if (tool) left.tools--; left.optional -= optional; left.unions -= unions; }
|
|
127
|
+
}
|
|
128
|
+
return {schema: out, errors, optional, unions};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const acceptsNull = (p: any, root: any): boolean => {
|
|
132
|
+
p = p?.$ref ? resolve(p, root) : p;
|
|
133
|
+
return !!p && (p.type == 'null' || (Array.isArray(p.type) && p.type.includes('null')) || (p.enum || []).includes(null) ||
|
|
134
|
+
p.const === null || (p.anyOf || p.oneOf || []).some((b: any) => acceptsNull(b, root)));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** strict-mode JSON -> what the original schema accepts: nulls for optional, non-nullable keys dropped */
|
|
138
|
+
export function unstrict(value: any, schema: any, root: any = schema): any {
|
|
139
|
+
if (schema?.$ref) schema = resolve(schema, root);
|
|
140
|
+
if (!schema || value == null || typeof value != 'object') return value;
|
|
141
|
+
if (Array.isArray(value)) return schema.items ? value.map(x => unstrict(x, schema.items, root)) : value;
|
|
142
|
+
const out = {...value};
|
|
143
|
+
for (const b0 of schema.anyOf || schema.oneOf || [schema]) {
|
|
144
|
+
const b = b0?.$ref ? resolve(b0, root) : b0, req = new Set(b?.required || []);
|
|
145
|
+
for (const k in b?.properties) {
|
|
146
|
+
if (!(k in out)) continue;
|
|
147
|
+
if (out[k] === null && !req.has(k) && !acceptsNull(b.properties[k], root)) delete out[k];
|
|
148
|
+
else out[k] = unstrict(out[k], b.properties[k], root);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {post, sse, json, partsOf, toolName, toolOutput} from './shared';
|
|
2
|
+
import type {HttpOptions} from './shared';
|
|
3
|
+
import {outputJsonSchema} from '../chat/output';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* PLAN-6 L-2 (wave 2): agui(url, { threadId?, state?, context?, headers?, fetch?, body?,
|
|
7
|
+
* dangerouslyAllowBrowser? }), a chat transport for an AG-UI agent endpoint (the Agent-User
|
|
8
|
+
* Interaction protocol: TanStack AI, CopilotKit runtimes, LangGraph / Mastra / Pydantic AI
|
|
9
|
+
* servers), checked against @ag-ui/core 1.0.2's types. POST `url` with a RunAgentInput, SSE back
|
|
10
|
+
* (`data: <event JSON>`).
|
|
11
|
+
*
|
|
12
|
+
* Request (RunAgentInput): `threadId` (the request's `chatId`, the option, or one per transport),
|
|
13
|
+
* a new `runId`, `state` (the request's `state`, else the latest state this transport saw, else
|
|
14
|
+
* the option, else {}), `messages` (the instructions as a system message; user / assistant / tool
|
|
15
|
+
* messages: an assistant message per step with its `toolCalls`, a `tool` message per result),
|
|
16
|
+
* `tools` (`{ name, description, parameters }`: client tools the agent may call), `context` (the
|
|
17
|
+
* option), `forwardedProps` (`{ model?, output? }`: the requested model and the `output` JSON
|
|
18
|
+
* Schema, for servers that use them); `body` merged in.
|
|
19
|
+
*
|
|
20
|
+
* Events -> ChatEvents:
|
|
21
|
+
* - TEXT_MESSAGE_START / CONTENT / CHUNK -> start (the first message id) and text (messages with a
|
|
22
|
+
* role other than assistant are skipped); REASONING_MESSAGE_CONTENT / CHUNK -> reasoning;
|
|
23
|
+
* - TOOL_CALL_START / ARGS / END (or CHUNK) -> a tool call, held until the step ends: a call the
|
|
24
|
+
* agent answers (TOOL_CALL_RESULT) is yielded `executed` + its result (the app doesn't run it), a
|
|
25
|
+
* call still open at STEP_FINISHED / RUN_FINISHED / the end of the stream (or when a new message
|
|
26
|
+
* starts) is a client tool call (the `tool` reply);
|
|
27
|
+
* - STATE_SNAPSHOT / STATE_DELTA (RFC 6902, applied here) -> a `data-agui-state` part (id
|
|
28
|
+
* 'state', so it replaces the previous one) holding the agent's whole current state;
|
|
29
|
+
* MESSAGES_SNAPSHOT -> a `data-agui-messages` part (id 'messages') with the agent's messages as
|
|
30
|
+
* sent (the app decides whether to adopt them); ACTIVITY_SNAPSHOT -> a `data-agui-activity` part
|
|
31
|
+
* (id: its message id);
|
|
32
|
+
* - RUN_FINISHED -> finish ('tool-calls' after client calls, 'other' for an interrupt outcome,
|
|
33
|
+
* else 'stop'; usage summed); RUN_ERROR -> a thrown Error. Everything else is ignored.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export interface AguiOptions extends HttpOptions {
|
|
37
|
+
threadId?: string;
|
|
38
|
+
state?: unknown;
|
|
39
|
+
context?: Array<{description: string; value: string}>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let n = 0;
|
|
43
|
+
const uid = (p: string) => `${p}_${Date.now().toString(36)}_${++n}`;
|
|
44
|
+
const clone = (v: any) => v === undefined ? v : JSON.parse(JSON.stringify(v));
|
|
45
|
+
|
|
46
|
+
/** RFC 6902 JSON Patch (add, remove, replace, move, copy; test ignored) on a copy of `doc` */
|
|
47
|
+
export function applyPatch(doc: any, ops: any[]): any {
|
|
48
|
+
let root = clone(doc ?? {});
|
|
49
|
+
const keys = (p: string) => p.split('/').slice(1).map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
50
|
+
const get = (k: string[]) => k.reduce((o, x) => o?.[x], root);
|
|
51
|
+
const set = (k: string[], v: any, insert: boolean) => {
|
|
52
|
+
if (!k.length) { root = v; return; }
|
|
53
|
+
const parent = get(k.slice(0, -1)), last = k[k.length - 1];
|
|
54
|
+
if (Array.isArray(parent)) last == '-' ? parent.push(v) : insert ? parent.splice(+last, 0, v) : (parent[+last] = v);
|
|
55
|
+
else if (parent && typeof parent == 'object') parent[last] = v;
|
|
56
|
+
};
|
|
57
|
+
const del = (k: string[]) => {
|
|
58
|
+
const parent = get(k.slice(0, -1)), last = k[k.length - 1], v = parent?.[last];
|
|
59
|
+
if (Array.isArray(parent)) parent.splice(+last, 1); else if (parent) delete parent[last];
|
|
60
|
+
return v;
|
|
61
|
+
};
|
|
62
|
+
for (const op of ops || []) {
|
|
63
|
+
const to = keys(String(op.path ?? ''));
|
|
64
|
+
if (op.op == 'add') set(to, clone(op.value), true);
|
|
65
|
+
else if (op.op == 'replace') set(to, clone(op.value), false);
|
|
66
|
+
else if (op.op == 'remove') del(to);
|
|
67
|
+
else if (op.op == 'move') set(to, del(keys(op.from)), true);
|
|
68
|
+
else if (op.op == 'copy') set(to, clone(get(keys(op.from))), true);
|
|
69
|
+
}
|
|
70
|
+
return root;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const textOf = (m: any) => partsOf(m).filter(p => p.type == 'text').map(p => p.text).join('');
|
|
74
|
+
|
|
75
|
+
/** a message as AG-UI messages (an assistant message per step with its toolCalls, then a tool message per result) */
|
|
76
|
+
export function toAgui(m: any, i: number): any[] {
|
|
77
|
+
const id = m.id ?? `m${i}`;
|
|
78
|
+
if (m.role != 'assistant') {
|
|
79
|
+
const files = partsOf(m).filter(p => p.type == 'file');
|
|
80
|
+
const text = textOf(m);
|
|
81
|
+
return [{id, role: m.role, content: files.length && m.role == 'user'
|
|
82
|
+
? [...(text ? [{type: 'text', text}] : []), ...files.map(p => ({type: p.mediaType?.startsWith('image/') ? 'image' : 'document', source: {type: 'url', value: p.url, mimeType: p.mediaType}}))]
|
|
83
|
+
: text}];
|
|
84
|
+
}
|
|
85
|
+
const out: any[] = [];
|
|
86
|
+
let text = '', calls: any[] = [], results: any[] = [], step = 0;
|
|
87
|
+
const flush = () => {
|
|
88
|
+
if (text || calls.length) out.push({id: step ? `${id}_${step}` : id, role: 'assistant', ...(text && {content: text}), ...(calls.length && {toolCalls: calls})});
|
|
89
|
+
out.push(...results);
|
|
90
|
+
if (text || calls.length) step++;
|
|
91
|
+
text = ''; calls = []; results = [];
|
|
92
|
+
};
|
|
93
|
+
for (const p of partsOf(m)) {
|
|
94
|
+
const name = toolName(p);
|
|
95
|
+
if (p.type == 'text') { if (calls.length) flush(); text += p.text; }
|
|
96
|
+
else if (name && p.input !== undefined) {
|
|
97
|
+
calls.push({id: p.toolCallId, type: 'function', function: {name, arguments: JSON.stringify(p.input ?? {})}});
|
|
98
|
+
const o = toolOutput(p);
|
|
99
|
+
if (o !== undefined) results.push({id: `${p.toolCallId}_result`, role: 'tool', toolCallId: p.toolCallId, content: o, ...(p.state == 'output-error' && {error: p.errorText ?? 'error'})});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
flush();
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function agui(url: string, options: AguiOptions = {}) {
|
|
107
|
+
const thread = options.threadId ?? uid('thread');
|
|
108
|
+
let state: any = options.state;
|
|
109
|
+
const headers = async (req: any) => ({accept: 'text/event-stream', ...(typeof options.headers == 'function' ? await options.headers(req) : options.headers)});
|
|
110
|
+
return {
|
|
111
|
+
async *stream(req: any, signal: AbortSignal): AsyncGenerator<any, void, any> {
|
|
112
|
+
const o = req.output !== undefined ? outputJsonSchema(req.output) : undefined;
|
|
113
|
+
const res = await post(url, {
|
|
114
|
+
threadId: req.chatId ?? thread,
|
|
115
|
+
runId: uid('run'),
|
|
116
|
+
state: req.state ?? state ?? {},
|
|
117
|
+
messages: [...(req.instructions ? [{id: 'instructions', role: 'system', content: req.instructions}] : []), ...req.messages.flatMap(toAgui)],
|
|
118
|
+
tools: Object.entries(req.tools || {}).map(([name, t]: [string, any]) => ({name, description: t?.description ?? '', parameters: t?.inputSchema ?? {type: 'object', properties: {}}})),
|
|
119
|
+
context: options.context ?? [],
|
|
120
|
+
forwardedProps: {...(req.model && {model: req.model}), ...(o && {output: o})},
|
|
121
|
+
}, req, signal, {...options, headers}, 'agui');
|
|
122
|
+
// tool calls by id as they stream; `open`: ended calls not yet answered, in order
|
|
123
|
+
const calls: Record<string, any> = {}, roles: Record<string, string> = {};
|
|
124
|
+
let open: any[] = [], last: any, started = false, called = 0;
|
|
125
|
+
const start = function* (id: any) { if (!started && id) { started = true; yield {type: 'start', id}; } };
|
|
126
|
+
const release = function* () { for (const c of open) { called++; yield c; } open = []; };
|
|
127
|
+
const take = (id: string) => {
|
|
128
|
+
const i = open.findIndex(c => c.id === id);
|
|
129
|
+
return i < 0 ? [] : [{...open.splice(i, 1)[0], executed: true}];
|
|
130
|
+
};
|
|
131
|
+
const end = (id: string) => {
|
|
132
|
+
const c = calls[id];
|
|
133
|
+
if (!c || c.done) return;
|
|
134
|
+
c.done = 1;
|
|
135
|
+
open.push({type: 'tool-call', id, name: c.name, input: json(c.args || '{}')});
|
|
136
|
+
};
|
|
137
|
+
for await (const {data} of sse(res)) {
|
|
138
|
+
const e = json(data), t = e?.type;
|
|
139
|
+
if (typeof t != 'string') continue;
|
|
140
|
+
if (t == 'TEXT_MESSAGE_START') { roles[e.messageId] = e.role ?? 'assistant'; yield* release(); yield* start(e.messageId); }
|
|
141
|
+
else if (t == 'TEXT_MESSAGE_CONTENT' || t == 'TEXT_MESSAGE_CHUNK') {
|
|
142
|
+
if (t == 'TEXT_MESSAGE_CHUNK' && e.role) roles[e.messageId] = e.role;
|
|
143
|
+
if ((roles[e.messageId] ?? 'assistant') == 'assistant' && e.delta) { yield* release(); yield* start(e.messageId); yield {type: 'text', delta: e.delta}; }
|
|
144
|
+
} else if (t == 'REASONING_MESSAGE_CONTENT' || t == 'REASONING_MESSAGE_CHUNK') { if (e.delta) yield {type: 'reasoning', delta: e.delta}; }
|
|
145
|
+
else if (t == 'TOOL_CALL_START') { yield* start(e.parentMessageId); calls[e.toolCallId] = {name: e.toolCallName, args: ''}; last = e.toolCallId; }
|
|
146
|
+
else if (t == 'TOOL_CALL_ARGS') { if (calls[e.toolCallId]) calls[e.toolCallId].args += e.delta ?? ''; }
|
|
147
|
+
else if (t == 'TOOL_CALL_CHUNK') {
|
|
148
|
+
const id = e.toolCallId ?? last;
|
|
149
|
+
if (e.toolCallId && e.toolCallId != last) { if (last) end(last); yield* start(e.parentMessageId); }
|
|
150
|
+
const c = calls[id] ||= {name: e.toolCallName, args: ''};
|
|
151
|
+
if (e.toolCallName) c.name = e.toolCallName;
|
|
152
|
+
c.args += e.delta ?? '';
|
|
153
|
+
last = id;
|
|
154
|
+
} else if (t == 'TOOL_CALL_END') end(e.toolCallId);
|
|
155
|
+
else if (t == 'TOOL_CALL_RESULT') {
|
|
156
|
+
end(e.toolCallId);
|
|
157
|
+
yield* take(e.toolCallId);
|
|
158
|
+
yield {type: 'tool-result', id: e.toolCallId, output: typeof e.content == 'string' ? json(e.content) : e.content};
|
|
159
|
+
} else if (t == 'STATE_SNAPSHOT' || t == 'STATE_DELTA') {
|
|
160
|
+
state = t == 'STATE_SNAPSHOT' ? e.snapshot : applyPatch(state, e.delta);
|
|
161
|
+
yield {type: 'data-agui-state', id: 'state', data: state};
|
|
162
|
+
} else if (t == 'MESSAGES_SNAPSHOT') yield {type: 'data-agui-messages', id: 'messages', data: e.messages};
|
|
163
|
+
else if (t == 'ACTIVITY_SNAPSHOT') yield {type: 'data-agui-activity', id: e.messageId, data: {activityType: e.activityType, content: e.content}};
|
|
164
|
+
else if (t == 'STEP_FINISHED') { if (last) end(last); yield* release(); }
|
|
165
|
+
else if (t == 'RUN_FINISHED') {
|
|
166
|
+
for (const id in calls) end(id);
|
|
167
|
+
yield* release();
|
|
168
|
+
const u = (e.usage || []).reduce((a: any, x: any) => ({inputTokens: a.inputTokens + (x.inputTokens ?? 0), outputTokens: a.outputTokens + (x.outputTokens ?? 0), totalTokens: a.totalTokens + (x.totalTokens ?? (x.inputTokens ?? 0) + (x.outputTokens ?? 0))}), {inputTokens: 0, outputTokens: 0, totalTokens: 0});
|
|
169
|
+
yield {type: 'finish', reason: e.outcome?.type == 'interrupt' ? 'other' : called ? 'tool-calls' : 'stop', ...(e.usage?.length && {usage: u})};
|
|
170
|
+
return;
|
|
171
|
+
} else if (t == 'RUN_ERROR') throw Object.assign(new Error('agui: ' + (e.message ?? 'the run failed')), {code: e.code});
|
|
172
|
+
}
|
|
173
|
+
for (const id in calls) end(id);
|
|
174
|
+
yield* release();
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|