toolnexus 0.27.0 → 0.28.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/README.md +6 -2
- package/dist/acp.d.ts +6 -0
- package/dist/acp.js +38 -16
- package/dist/agents/home.d.ts +85 -4
- package/dist/agents/home.js +347 -52
- package/dist/classifier.d.ts +11 -3
- package/dist/classifier.js +120 -3
- package/dist/client.d.ts +109 -0
- package/dist/client.js +380 -75
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp.js +13 -1
- package/dist/nodefetch.d.ts +1 -0
- package/dist/nodefetch.js +156 -0
- package/dist/transcript_search.d.ts +27 -0
- package/dist/transcript_search.js +165 -0
- package/package.json +2 -2
- package/src/acp.ts +42 -15
- package/src/agents/home.ts +381 -53
- package/src/classifier.ts +117 -6
- package/src/client.ts +368 -57
- package/src/index.ts +1 -0
- package/src/mcp.ts +18 -1
- package/src/nodefetch.ts +143 -0
- package/src/transcript_search.ts +172 -0
package/src/classifier.ts
CHANGED
|
@@ -34,7 +34,7 @@ export const DEFAULT_BATCH_CONCURRENCY = 16
|
|
|
34
34
|
* `typesafe/jev-1.14`, and mixing the two is issue #91. `custom` is a self-hosted System One
|
|
35
35
|
* endpoint and requires `baseUrl` + `supports` (SPEC §8B "Backends, images, cache and validation").
|
|
36
36
|
*/
|
|
37
|
-
export type ClassifierBackend = "typesafe" | "openrouter" | "cloudflare" | "vercel" | "custom"
|
|
37
|
+
export type ClassifierBackend = "typesafe" | "openrouter" | "cloudflare" | "vercel" | "openai" | "custom"
|
|
38
38
|
|
|
39
39
|
/** What a backend+model can take. DATA: a fail-fast for what the table says, never a claim about
|
|
40
40
|
* the vendor. Override per classifier with `ClassifierOptions.supports`. */
|
|
@@ -48,8 +48,9 @@ export interface ClassifierBackendPreset {
|
|
|
48
48
|
baseUrl: string
|
|
49
49
|
model: string
|
|
50
50
|
apiKeyEnv: string
|
|
51
|
-
/** `systemone`: POST {baseUrl}/systemone. `cloudflare`: POST {baseUrl}, `/ai/run` envelope.
|
|
52
|
-
|
|
51
|
+
/** `systemone`: POST {baseUrl}/systemone. `cloudflare`: POST {baseUrl}, `/ai/run` envelope.
|
|
52
|
+
* `decisions`: POST {baseUrl}/decisions, OpenAI's Decisions API (#157). */
|
|
53
|
+
wire: "systemone" | "cloudflare" | "decisions"
|
|
53
54
|
/** Send `TypeSafe-Danger-Zone-Images: true` when a request carries images. */
|
|
54
55
|
imagesHeader: boolean
|
|
55
56
|
/** The default support for a model on this backend; absent on `custom`. */
|
|
@@ -101,6 +102,14 @@ export const CLASSIFIER_BACKENDS: Readonly<Record<ClassifierBackend, Readonly<Cl
|
|
|
101
102
|
imagesHeader: true,
|
|
102
103
|
supports: () => ({ images: false }),
|
|
103
104
|
},
|
|
105
|
+
openai: {
|
|
106
|
+
baseUrl: "https://api.openai.com/v1",
|
|
107
|
+
model: "gpt-6-luna",
|
|
108
|
+
apiKeyEnv: "OPENAI_API_KEY",
|
|
109
|
+
wire: "decisions",
|
|
110
|
+
imagesHeader: false,
|
|
111
|
+
supports: () => ({ images: true }),
|
|
112
|
+
},
|
|
104
113
|
custom: {
|
|
105
114
|
baseUrl: "",
|
|
106
115
|
model: DEFAULT_CLASSIFIER_MODEL,
|
|
@@ -690,7 +699,7 @@ export class Classifier {
|
|
|
690
699
|
this.backend = opts.backend ?? "typesafe"
|
|
691
700
|
if (!Object.prototype.hasOwnProperty.call(CLASSIFIER_BACKENDS, this.backend)) {
|
|
692
701
|
throw new Error(
|
|
693
|
-
`classifier: unknown backend "${this.backend}" (want "typesafe", "openrouter", "cloudflare", "vercel" or "custom")`,
|
|
702
|
+
`classifier: unknown backend "${this.backend}" (want "typesafe", "openrouter", "cloudflare", "vercel", "openai" or "custom")`,
|
|
694
703
|
)
|
|
695
704
|
}
|
|
696
705
|
const preset = CLASSIFIER_BACKENDS[this.backend]
|
|
@@ -920,7 +929,9 @@ export class Classifier {
|
|
|
920
929
|
const qs: Record<string, unknown> = {}
|
|
921
930
|
for (const [key, q] of Object.entries(questions)) qs[key] = questionWire(q)
|
|
922
931
|
let body: Record<string, any>
|
|
923
|
-
if (this.preset.wire === "
|
|
932
|
+
if (this.preset.wire === "decisions") {
|
|
933
|
+
body = { model: this.model, input: decisionsInput(p), questions: decisionsQuestions(this.backend, this.model, questions) }
|
|
934
|
+
} else if (this.preset.wire === "cloudflare") {
|
|
924
935
|
const input: Record<string, unknown> = { questions: qs, state: p.state }
|
|
925
936
|
if (p.images.length > 0) input.images = p.images.map((im) => `data:image/${im.format};base64,${im.data}`)
|
|
926
937
|
body = { model: this.model, input }
|
|
@@ -995,6 +1006,9 @@ export class Classifier {
|
|
|
995
1006
|
if (this.preset.wire === "cloudflare") {
|
|
996
1007
|
parsed = unwrapCloudflare(parsed)
|
|
997
1008
|
text = canonicalJson(parsed)
|
|
1009
|
+
} else if (this.preset.wire === "decisions") {
|
|
1010
|
+
text = decisionsReply(parsed, questions)
|
|
1011
|
+
parsed = JSON.parse(text)
|
|
998
1012
|
}
|
|
999
1013
|
const d = decodeDecision(parsed, this.model)
|
|
1000
1014
|
const verr = validateDecision(questions, d)
|
|
@@ -1020,7 +1034,8 @@ export class Classifier {
|
|
|
1020
1034
|
// `endpoint` is what errors name: the CONFIGURED base, ${ENV} unexpanded, so an account id in
|
|
1021
1035
|
// a templated URL never reaches an error. `url` is what is dialled, expanded at call time.
|
|
1022
1036
|
let endpoint = this.baseUrl.replace(/\/+$/, "")
|
|
1023
|
-
if (this.preset.wire
|
|
1037
|
+
if (this.preset.wire === "decisions") endpoint += "/decisions"
|
|
1038
|
+
else if (this.preset.wire !== "cloudflare") endpoint += "/systemone"
|
|
1024
1039
|
const url = endpoint.includes("${") ? expandEnv(endpoint) : endpoint
|
|
1025
1040
|
const retries = this.opts.retries ?? 2
|
|
1026
1041
|
const base = this.opts.retryBaseMs ?? 500
|
|
@@ -1559,3 +1574,99 @@ export function chunkQuestions<Q extends AnyQuestion>(questions: Record<string,
|
|
|
1559
1574
|
}
|
|
1560
1575
|
return out
|
|
1561
1576
|
}
|
|
1577
|
+
|
|
1578
|
+
// ---------------------------------------------------------------- the Decisions wire (#157)
|
|
1579
|
+
|
|
1580
|
+
/** The Decisions API's per-request question cap. */
|
|
1581
|
+
export const MAX_DECISIONS_QUESTIONS = 400
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* Maps the question set to the Decisions API list, in ASCII key order. Caller keys are NOT sent
|
|
1585
|
+
* (no `name`): §8B keys are addressing, not content, and answers come back in request order.
|
|
1586
|
+
* Every check fires before any request.
|
|
1587
|
+
*/
|
|
1588
|
+
function decisionsQuestions(backend: string, model: string, questions: Record<string, AnyQuestion>): unknown[] {
|
|
1589
|
+
const keys = Object.keys(questions).sort()
|
|
1590
|
+
if (keys.length > MAX_DECISIONS_QUESTIONS) {
|
|
1591
|
+
throw new Error(`classifier: the decisions wire takes at most ${MAX_DECISIONS_QUESTIONS} questions, got ${keys.length}`)
|
|
1592
|
+
}
|
|
1593
|
+
return keys.map((k) => {
|
|
1594
|
+
const q = questions[k]
|
|
1595
|
+
if (isRawQuestion(q)) throw new ClassifierUnsupported(backend, model, `raw question type "${q.type}"`)
|
|
1596
|
+
const m = q as Question
|
|
1597
|
+
switch (m.type) {
|
|
1598
|
+
case "noul": {
|
|
1599
|
+
let instructions = m.instructions
|
|
1600
|
+
if (m.criteria !== undefined) instructions += `\n\nTrue means: ${m.criteria.true}\nFalse means: ${m.criteria.false}`
|
|
1601
|
+
return { type: "predicate", instructions }
|
|
1602
|
+
}
|
|
1603
|
+
case "choice": {
|
|
1604
|
+
const ids = Object.keys(m.criteria ?? {}).sort()
|
|
1605
|
+
if (ids.length < 2) {
|
|
1606
|
+
throw new Error(`classifier: question "${k}": the decisions wire needs 2..${MAX_CHOICE_OPTIONS} choice options, got ${ids.length}`)
|
|
1607
|
+
}
|
|
1608
|
+
const choices = ids.map((id) => (m.criteria[id] === "" ? { value: id } : { value: id, description: m.criteria[id] }))
|
|
1609
|
+
return { type: "choice", instructions: m.instructions, choices }
|
|
1610
|
+
}
|
|
1611
|
+
case "score":
|
|
1612
|
+
return { type: "score", instructions: m.instructions, levels: m.criteria.map((label) => ({ label })) }
|
|
1613
|
+
}
|
|
1614
|
+
})
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/** The state as sent: a string verbatim, else canonical JSON text; with images, one user message. */
|
|
1618
|
+
function decisionsInput(p: PreparedRequest): unknown {
|
|
1619
|
+
const text = typeof p.state === "string" ? p.state : canonicalJson(p.state)
|
|
1620
|
+
if (p.images.length === 0) return text
|
|
1621
|
+
const content: unknown[] = [{ type: "input_text", text }]
|
|
1622
|
+
for (const im of p.images) content.push({ type: "input_image", image_url: `data:image/${im.format};base64,${im.data}` })
|
|
1623
|
+
return [{ role: "user", content }]
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/** A choice value or level index as the §8B key: string as written, boolean "true"/"false", number shortest. */
|
|
1627
|
+
function decisionsValue(v: unknown): string {
|
|
1628
|
+
return typeof v === "string" ? v : String(v)
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Reads a Decisions API reply back into a System One-shaped body (canonical JSON), so decode,
|
|
1633
|
+
* validation and the cache are the one path. `calibrated` is false. Exported for conformance tests.
|
|
1634
|
+
*/
|
|
1635
|
+
export function decisionsReply(reply: unknown, questions: Record<string, AnyQuestion>): string {
|
|
1636
|
+
const r = reply as any
|
|
1637
|
+
if (r === null || typeof r !== "object" || Array.isArray(r) || !Array.isArray(r.answers)) {
|
|
1638
|
+
throw new Error("classifier: decisions: malformed reply")
|
|
1639
|
+
}
|
|
1640
|
+
const keys = Object.keys(questions).sort()
|
|
1641
|
+
if (r.answers.length !== keys.length) {
|
|
1642
|
+
throw new Error(`classifier: decisions: ${r.answers.length} answers for ${keys.length} questions`)
|
|
1643
|
+
}
|
|
1644
|
+
const num = (v: unknown): number => (typeof v === "number" ? v : 0)
|
|
1645
|
+
const want: Record<string, string> = { noul: "predicate", choice: "choice", score: "score" }
|
|
1646
|
+
const answers: Record<string, unknown> = {}
|
|
1647
|
+
keys.forEach((k, i) => {
|
|
1648
|
+
const a = r.answers[i]
|
|
1649
|
+
if (a === null || typeof a !== "object" || Array.isArray(a)) throw new Error("classifier: decisions: malformed reply")
|
|
1650
|
+
if (a.type === "refusal") throw new Error(`classifier: decisions: question "${k}" was refused`)
|
|
1651
|
+
const q = questions[k]
|
|
1652
|
+
const w = want[q.type] ?? ""
|
|
1653
|
+
if (a.type !== w) throw new Error(`classifier: decisions: answer "${k}" is a ${a.type} answer, not ${w}`)
|
|
1654
|
+
const probs: Record<string, number> = {}
|
|
1655
|
+
for (const p of Array.isArray(a.probabilities) ? a.probabilities : []) probs[decisionsValue(p?.value)] = num(p?.probability)
|
|
1656
|
+
if (q.type === "noul") answers[k] = { type: "noul", noul: num(a.probability) }
|
|
1657
|
+
else if (q.type === "choice") {
|
|
1658
|
+
answers[k] = { type: "choice", choice: decisionsValue(a.choice), probabilities: probs, confidence: num(a.confidence) }
|
|
1659
|
+
} else {
|
|
1660
|
+
const legend: Record<string, string> = {}
|
|
1661
|
+
;(q as ScoreQuestion).criteria.forEach((l, j) => (legend[String(j)] = l))
|
|
1662
|
+
answers[k] = { type: "score", score: num(a.score), legend, probabilities: probs, confidence: num(a.confidence) }
|
|
1663
|
+
}
|
|
1664
|
+
})
|
|
1665
|
+
const u = r.usage ?? {}
|
|
1666
|
+
return canonicalJson({
|
|
1667
|
+
answers,
|
|
1668
|
+
calibrated: false,
|
|
1669
|
+
model: typeof r.model === "string" ? r.model : "",
|
|
1670
|
+
usage: { input_tokens: num(u.input_tokens), output_tokens: num(u.output_tokens) },
|
|
1671
|
+
})
|
|
1672
|
+
}
|