feat(neuron): kernel with knowledge, backlog, orchestrator; wire transport proven live
This commit is contained in:
@@ -3,72 +3,102 @@ export * as Aspect from "./aspect"
|
|||||||
import type { Graph } from "./graph"
|
import type { Graph } from "./graph"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The AOP kit. Cross-cutting concerns are stages you decorate onto
|
* The AOP kit - one registry, applied seamlessly to any function.
|
||||||
* functions - never code scattered inside them.
|
|
||||||
*
|
*
|
||||||
* const run = traced("runner.turn", metered("turns", guarded(claims, scope, work)))
|
* // composition site (once):
|
||||||
|
* Aspect.use("llm", Aspect.traced("llm"), Aspect.metered("llm"))
|
||||||
|
* Aspect.use("tool", Aspect.timed())
|
||||||
*
|
*
|
||||||
* Every stage records what it did as edges in the graph ledger:
|
* // anywhere, the function opts into its category once:
|
||||||
* kind "trace" - a span with duration
|
* export const complete = Aspect.wrap("llm", async (input) => {...})
|
||||||
* kind "metric" - a named observation with value
|
*
|
||||||
* The wire remembers; nothing is scraped after the fact.
|
* Change instrumentation = change the composition list. Zero call sites
|
||||||
|
* are touched. That is the entire point.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Claims {
|
type AsyncFn<A extends unknown[], R> = (...args: A) => Promise<R>
|
||||||
principal: string
|
|
||||||
authed: boolean
|
/** A stage observes/wraps a call. It may time, log, deny, or pass. */
|
||||||
scopes: string[]
|
export type Stage = (meta: { name: string; category: string }) => <A extends unknown[], R>(
|
||||||
|
fn: AsyncFn<A, R>,
|
||||||
|
) => AsyncFn<A, R>
|
||||||
|
|
||||||
|
interface Entry {
|
||||||
|
category: string
|
||||||
|
stage: Stage
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AsyncFn<A extends unknown[], R> = (...args: A) => Promise<R>
|
const registry: Entry[] = []
|
||||||
|
|
||||||
function record(graph: Graph | undefined, kind: string, payload: Record<string, unknown>) {
|
/** Declare which aspects apply to a category. Call once at composition. */
|
||||||
if (!graph) return
|
export function use(category: string, ...stages: Stage[]): void {
|
||||||
graph.ensureNode("telemetry", `telemetry:${process.pid}`)
|
for (const stage of stages) registry.push({ category, stage })
|
||||||
graph.edge(`telemetry:${process.pid}`, `telemetry:${process.pid}`, kind, { ...payload, at: Date.now() })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Trace: time the call and record a span edge. */
|
/** Wrap any function so its category's aspects apply to every invocation. */
|
||||||
export function traced<A extends unknown[], R>(
|
export function wrap<A extends unknown[], R>(
|
||||||
graph: Graph | undefined,
|
category: string,
|
||||||
name: string,
|
name: string,
|
||||||
fn: AsyncFn<A, R>,
|
fn: AsyncFn<A, R>,
|
||||||
): AsyncFn<A, R> {
|
): AsyncFn<A, R> {
|
||||||
return async (...args: A) => {
|
const stages = registry.filter((e) => e.category === category).map((e) => e.stage)
|
||||||
|
return async (...args: A): Promise<R> => {
|
||||||
|
let chain: AsyncFn<A, R> = fn
|
||||||
|
// Applied outermost-last so the first listed stage is outermost.
|
||||||
|
for (let i = stages.length - 1; i >= 0; i--) {
|
||||||
|
const inner = chain
|
||||||
|
chain = stages[i]({ name, category })(inner)
|
||||||
|
}
|
||||||
|
return chain(...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- built-in stages ----------
|
||||||
|
|
||||||
|
/** Record duration + outcome of every call into the graph ledger. */
|
||||||
|
export function traced(graph?: Graph): Stage {
|
||||||
|
return () => (fn) => async (...args: any[]) => {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
try {
|
try {
|
||||||
const result = await fn(...args)
|
const result = await fn(...args)
|
||||||
record(graph, "trace", { name, ms: Date.now() - start, ok: true })
|
if (graph) {
|
||||||
|
const t = graph.ensureNode("telemetry", `telemetry:${process.pid}`)
|
||||||
|
graph.edge(t.id, t.id, "trace", { fn: fn.name || "anonymous", ms: Date.now() - start, ok: true })
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
record(graph, "trace", { name, ms: Date.now() - start, ok: false, error: String(error).slice(0, 200) })
|
if (graph) {
|
||||||
|
const t = graph.ensureNode("telemetry", `telemetry:${process.pid}`)
|
||||||
|
graph.edge(t.id, t.id, "trace", {
|
||||||
|
fn: fn.name || "anonymous",
|
||||||
|
ms: Date.now() - start,
|
||||||
|
ok: false,
|
||||||
|
error: String(error).slice(0, 200),
|
||||||
|
})
|
||||||
|
}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Meter: count invocations of a named metric. */
|
/** Count invocations of a named metric into the ledger. */
|
||||||
export function metered<A extends unknown[], R>(graph: Graph | undefined, name: string, fn: AsyncFn<A, R>): AsyncFn<A, R> {
|
export function metered(graph?: Graph): Stage {
|
||||||
return async (...args: A) => {
|
return (meta) => (fn) => async (...args: any[]) => {
|
||||||
record(graph, "metric", { name, at: Date.now() })
|
if (graph) {
|
||||||
|
const t = graph.ensureNode("telemetry", `telemetry:${process.pid}`)
|
||||||
|
graph.edge(t.id, t.id, "metric", { name: meta.name, at: Date.now() })
|
||||||
|
}
|
||||||
return fn(...args)
|
return fn(...args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Policy {
|
/** Deny calls whose claims don't satisfy the requirement list. */
|
||||||
claims: Claims
|
export function guarded(
|
||||||
/** Claim requirements, e.g. ["authed:true", "scope:fs.write"]. */
|
|
||||||
require?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Guard: one verdict from the principal's claims, stamped on failure too. */
|
|
||||||
export function guarded<A extends unknown[], R>(
|
|
||||||
graph: Graph | undefined,
|
graph: Graph | undefined,
|
||||||
policy: () => Policy,
|
policy: () => { claims: { principal: string; authed: boolean; scopes: string[] }; require: string[] },
|
||||||
fn: AsyncFn<A, R>,
|
): Stage {
|
||||||
): AsyncFn<A, R> {
|
return () => (fn) => async (...args: any[]) => {
|
||||||
return async (...args: A) => {
|
const { claims, require } = policy()
|
||||||
const { claims, require = [] } = policy()
|
|
||||||
for (const requirement of require) {
|
for (const requirement of require) {
|
||||||
const [key, expected] = requirement.split(":")
|
const [key, expected] = requirement.split(":")
|
||||||
const actual =
|
const actual =
|
||||||
@@ -78,7 +108,10 @@ export function guarded<A extends unknown[], R>(
|
|||||||
? String(claims.authed)
|
? String(claims.authed)
|
||||||
: String((claims as any)[key] ?? "")
|
: String((claims as any)[key] ?? "")
|
||||||
if (actual !== (expected ?? "true")) {
|
if (actual !== (expected ?? "true")) {
|
||||||
record(graph, "denied", { requirement, principal: claims.principal })
|
if (graph) {
|
||||||
|
const t = graph.ensureNode("telemetry", `telemetry:${process.pid}`)
|
||||||
|
graph.edge(t.id, t.id, "denied", { requirement, principal: claims.principal })
|
||||||
|
}
|
||||||
throw new Error(`guard denied: ${requirement}`)
|
throw new Error(`guard denied: ${requirement}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import { Graph } from "./graph"
|
||||||
|
import type { Node } from "./graph"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain operations on the graph.
|
||||||
|
*
|
||||||
|
* Knowledge is what you learned. A project organises work. A backlog
|
||||||
|
* holds intentions. The orchestrator decomposes intent into executable
|
||||||
|
* steps. All of it lives as nodes and typed edges in one store.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Kernel {
|
||||||
|
private graph: Graph
|
||||||
|
|
||||||
|
constructor(graph: Graph) {
|
||||||
|
this.graph = graph
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- knowledge ----
|
||||||
|
|
||||||
|
learn(fact: string, source?: string): Node {
|
||||||
|
const addr = `knowledge:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 6)}`
|
||||||
|
const node = this.graph.ensureNode("knowledge", addr)
|
||||||
|
this.graph.edge(node.id, node.id, "content", { fact, source })
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
linkKnowledge(knowledgeNodeID: string, targetNodeID: string, relationship: string) {
|
||||||
|
this.graph.edge(targetNodeID, knowledgeNodeID, relationship, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
queryKnowledge(): Array<{ id: string; fact: string; source?: string }> {
|
||||||
|
return (this.graph as any).db
|
||||||
|
.query(`SELECT n.id, e.payload FROM nodes n JOIN edges e ON e.from_node = n.id AND e.kind = 'content' WHERE n.kind = 'knowledge' ORDER BY e.at DESC`)
|
||||||
|
.all()
|
||||||
|
.map((row: any) => ({
|
||||||
|
id: row.id,
|
||||||
|
fact: JSON.parse(row.payload)?.fact ?? "",
|
||||||
|
source: JSON.parse(row.payload)?.source,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- backlog ----
|
||||||
|
|
||||||
|
createBacklog(name: string): Node {
|
||||||
|
return this.graph.ensureNode("backlog", `backlog:${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(backlogName: string, text: string): Node {
|
||||||
|
const backlog = this.graph.getNode(`backlog:${backlogName}`)
|
||||||
|
if (!backlog) throw new Error(`backlog not found: ${backlogName}`)
|
||||||
|
const item = this.graph.ensureNode("backlog-item", `item:${crypto.randomUUID()}`)
|
||||||
|
this.graph.edge(backlog.id, item.id, "contains", { text })
|
||||||
|
this.graph.edge(item.id, backlog.id, "status", { state: "proposed" })
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(itemAddress: string, state: string) {
|
||||||
|
const item = this.graph.getNode(itemAddress)
|
||||||
|
if (!item) throw new Error(`item not found: ${itemAddress}`)
|
||||||
|
this.graph.edge(item.id, item.id, "status", { state })
|
||||||
|
}
|
||||||
|
|
||||||
|
backlogItems(backlogName: string): Array<{ id: string; state: string; text: string }> {
|
||||||
|
const backlog = this.graph.getNode(`backlog:${backlogName}`)
|
||||||
|
if (!backlog) return []
|
||||||
|
return this.graph.outgoing(backlog.id, "contains").map((e) => {
|
||||||
|
const item = this.graph.getNode(e.to_node)
|
||||||
|
if (!item) return { id: "", state: "", text: "" }
|
||||||
|
const statusEdges = this.graph.incident(item.id, "status")
|
||||||
|
const lastStatus = statusEdges.length > 0 ? JSON.parse(statusEdges[statusEdges.length - 1].payload || "{}") : {}
|
||||||
|
const payload = this.graph.incident(backlog.id, "contains").find((c) => c.to_node === item.id)
|
||||||
|
const text = payload ? JSON.parse(payload.payload || "{}").text : ""
|
||||||
|
return { id: item.id, state: lastStatus?.state ?? "proposed", text }
|
||||||
|
}).filter((x) => x.id !== "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- orchestrator ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decompose intent into steps, execute each, learn from outcomes.
|
||||||
|
* Steps run in dependency order; each publishes its completion.
|
||||||
|
*/
|
||||||
|
async orchestrate(intent: string, executors: Map<string, (step: Step) => Promise<string>>): Promise<Array<{ step: string; result: string }>> {
|
||||||
|
const steps = this.decompose(intent)
|
||||||
|
const done = new Set<string>()
|
||||||
|
const results: Array<{ step: string; result: string }> = []
|
||||||
|
|
||||||
|
while (done.size < steps.length) {
|
||||||
|
let progressed = false
|
||||||
|
for (const step of steps) {
|
||||||
|
if (done.has(step.id)) continue
|
||||||
|
if (!step.dependsOn.every((d) => done.has(d))) continue
|
||||||
|
|
||||||
|
const executor = executors.get(step.id)
|
||||||
|
const result = executor ? await executor(step) : `step ${step.id} completed`
|
||||||
|
results.push({ step: step.id, result })
|
||||||
|
done.add(step.id)
|
||||||
|
progressed = true
|
||||||
|
}
|
||||||
|
if (!progressed && done.size < steps.length) {
|
||||||
|
throw new Error("dependency cycle in plan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
private decompose(intent: string): Array<{ id: string; dependsOn: string[] }> {
|
||||||
|
// Simple sequential decomposition for now.
|
||||||
|
// Real decomposition will come from the LLM once wired to dialects.
|
||||||
|
return [
|
||||||
|
{ id: "step-1", dependsOn: [] },
|
||||||
|
{ id: "step-2", dependsOn: ["step-1"] },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
export * as LLMClient from "./client"
|
||||||
|
|
||||||
|
import type { Dialect } from "../provider/dialects/types"
|
||||||
|
import type { Graph } from "../kernel/graph"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The LLM call is a PIPELINE. You compose stages - each stage observes or
|
||||||
|
* enriches the call context - and the last stage does the wire work.
|
||||||
|
*
|
||||||
|
* const llm = createLLMPipeline(dialect, [traced(), metered()])
|
||||||
|
* const result = await llm({ system, messages, tools })
|
||||||
|
*
|
||||||
|
* Every stage sees the same context: request, response, tokens, timing.
|
||||||
|
* Telemetry stages publish spans/metrics onto the bus; nothing scrapes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Usage {
|
||||||
|
input: number
|
||||||
|
output: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolCall {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
args: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompletionResult {
|
||||||
|
text: string
|
||||||
|
toolCalls: ToolCall[]
|
||||||
|
finish: string | null
|
||||||
|
usage: Usage
|
||||||
|
ms: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A stage wraps the continuation: observe, enrich, short-circuit, or pass. */
|
||||||
|
export type Stage = (ctx: CallContext, next: () => Promise<void>) => Promise<void>
|
||||||
|
|
||||||
|
export interface CallContext {
|
||||||
|
input: {
|
||||||
|
baseURL: string
|
||||||
|
authHeaders: Record<string, string>
|
||||||
|
model: string
|
||||||
|
system: string[]
|
||||||
|
messages: unknown[]
|
||||||
|
tools?: unknown[]
|
||||||
|
}
|
||||||
|
/** Filled by the wire stage. */
|
||||||
|
text: string
|
||||||
|
toolCalls: ToolCall[]
|
||||||
|
finish: string | null
|
||||||
|
usage: Usage
|
||||||
|
ms: number
|
||||||
|
ok: boolean
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WireDialect = Pick<Dialect, "request" | "parser">
|
||||||
|
|
||||||
|
async function wireStage(ctx: CallContext, dialect: WireDialect): Promise<void> {
|
||||||
|
const start = Date.now()
|
||||||
|
const req = dialect.request({
|
||||||
|
baseURL: ctx.input.baseURL,
|
||||||
|
apiKey: "",
|
||||||
|
authHeaders: ctx.input.authHeaders,
|
||||||
|
model: ctx.input.model,
|
||||||
|
system: ctx.input.system,
|
||||||
|
messages: ctx.input.messages,
|
||||||
|
tools: ctx.input.tools,
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await fetch(req.url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: req.headers,
|
||||||
|
body: JSON.stringify(req.body),
|
||||||
|
})
|
||||||
|
if (!res.ok || !res.body) {
|
||||||
|
const detail = await res.text().catch(() => "")
|
||||||
|
throw new Error(`provider HTTP ${res.status}: ${detail.slice(0, 400)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = dialect.parser()
|
||||||
|
const reader = res.body.getReader()
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
let carry = ""
|
||||||
|
|
||||||
|
outer: while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
if (done) break
|
||||||
|
carry += decoder.decode(value, { stream: true })
|
||||||
|
let nl: number
|
||||||
|
while ((nl = carry.indexOf("\n")) !== -1) {
|
||||||
|
const line = carry.slice(0, nl).replace(/\r$/, "")
|
||||||
|
carry = carry.slice(nl + 1)
|
||||||
|
if (!line.startsWith("data:")) continue
|
||||||
|
const payload = line.slice(5).trim()
|
||||||
|
if (!payload || payload === "[DONE]") continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(payload)
|
||||||
|
const u = raw.usage ?? raw.response?.usage
|
||||||
|
if (u) {
|
||||||
|
ctx.usage.input =
|
||||||
|
Number(u.input_tokens ?? u.prompt_tokens ?? 0) || ctx.usage.input
|
||||||
|
ctx.usage.output =
|
||||||
|
Number(u.output_tokens ?? u.completion_tokens ?? 0) || ctx.usage.output
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
for (const event of parser.parse(payload)) {
|
||||||
|
switch (event.type) {
|
||||||
|
case "text-delta":
|
||||||
|
ctx.text += event.text
|
||||||
|
break
|
||||||
|
case "tool-call": {
|
||||||
|
const existing = ctx.toolCalls.find((c) => c.id === event.id)
|
||||||
|
if (existing) existing.args += event.arguments
|
||||||
|
else ctx.toolCalls.push({ id: event.id, name: event.name, args: event.arguments })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "finish":
|
||||||
|
ctx.finish = event.reason
|
||||||
|
break outer
|
||||||
|
case "error":
|
||||||
|
throw new Error(`provider error: ${event.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.ms = Date.now() - start
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose the pipeline. Order given at composition is order of execution;
|
||||||
|
* every stage is named code in this file - nothing hidden.
|
||||||
|
*/
|
||||||
|
export function createLLMPipeline(
|
||||||
|
dialect: WireDialect,
|
||||||
|
stages: Array<Stage> = [],
|
||||||
|
): (input: {
|
||||||
|
baseURL: string
|
||||||
|
authHeaders: Record<string, string>
|
||||||
|
model: string
|
||||||
|
system: string[]
|
||||||
|
messages: unknown[]
|
||||||
|
tools?: unknown[]
|
||||||
|
}) => Promise<CompletionResult> {
|
||||||
|
return async (input) => {
|
||||||
|
const start = Date.now()
|
||||||
|
const ctx: CallContext = {
|
||||||
|
input,
|
||||||
|
text: "",
|
||||||
|
toolCalls: [],
|
||||||
|
finish: null,
|
||||||
|
usage: { input: 0, output: 0 },
|
||||||
|
ms: 0,
|
||||||
|
ok: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Onion: stages run in order, each may pass to the rest.
|
||||||
|
const runStage = async (index: number): Promise<void> => {
|
||||||
|
if (index >= stages.length) {
|
||||||
|
await wireStage(ctx, dialect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await stages[index](ctx, () => runStage(index + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
let failure: unknown
|
||||||
|
try {
|
||||||
|
await runStage(0)
|
||||||
|
} catch (error) {
|
||||||
|
failure = error
|
||||||
|
ctx.ok = false
|
||||||
|
ctx.error = String(error).slice(0, 300)
|
||||||
|
}
|
||||||
|
ctx.ms = Date.now() - start
|
||||||
|
|
||||||
|
if (!ctx.ok && failure !== undefined) {
|
||||||
|
// Errors are data too: rethrow with full context attached.
|
||||||
|
const err = new Error(`LLM call failed after ${ctx.ms}ms: ${ctx.error}`)
|
||||||
|
;(err as any).usage = ctx.usage
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: ctx.text,
|
||||||
|
toolCalls: ctx.toolCalls,
|
||||||
|
finish: ctx.finish,
|
||||||
|
usage: ctx.usage,
|
||||||
|
ms: ctx.ms,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import * as Conversation from "../conversation"
|
|||||||
import * as Tools from "../tools"
|
import * as Tools from "../tools"
|
||||||
import * as Auth from "../auth"
|
import * as Auth from "../auth"
|
||||||
import * as Context from "./context"
|
import * as Context from "./context"
|
||||||
|
import { createLLMPipeline } from "../llm/client"
|
||||||
|
import * as Aspect from "../kernel/aspect"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2 Session Core - admission is durable and separate from execution.
|
* V2 Session Core - admission is durable and separate from execution.
|
||||||
@@ -200,63 +202,65 @@ export function open(input: {
|
|||||||
const calls = new Map<string, { id: string; name: string; args: string }>()
|
const calls = new Map<string, { id: string; name: string; args: string }>()
|
||||||
let finished = false
|
let finished = false
|
||||||
|
|
||||||
const req = provider.dialect.request({
|
await Aspect.wrap("llm", "llm.turn", async () => {
|
||||||
baseURL: provider.baseURL,
|
const req = provider.dialect.request({
|
||||||
apiKey: "",
|
baseURL: provider.baseURL,
|
||||||
authHeaders: provider.authHeaders,
|
apiKey: "",
|
||||||
model: provider.model,
|
authHeaders: provider.authHeaders,
|
||||||
system,
|
model: provider.model,
|
||||||
messages: wireMessages,
|
system,
|
||||||
tools: provider.dialect.toWireTools(tools),
|
messages: wireMessages,
|
||||||
})
|
tools: provider.dialect.toWireTools(tools),
|
||||||
|
})
|
||||||
const res = await fetch(req.url, { method: "POST", headers: req.headers, body: JSON.stringify(req.body) })
|
const res = await fetch(req.url, {
|
||||||
if (!res.ok || !res.body) {
|
method: "POST",
|
||||||
const detail = await res.text().catch(() => "")
|
headers: req.headers,
|
||||||
if (process.env.NEURON_DEBUG)
|
body: JSON.stringify(req.body),
|
||||||
console.error("[wire-dump]", JSON.stringify({ url: req.url, body: req.body }).slice(0, 2000))
|
})
|
||||||
throw new Error(`provider HTTP ${res.status}: ${detail.slice(0, 400)}`)
|
if (!res.ok || !res.body) {
|
||||||
}
|
const detail = await res.text().catch(() => "")
|
||||||
|
throw new Error(`provider HTTP ${res.status}: ${detail.slice(0, 400)}`)
|
||||||
const parser = provider.dialect.parser()
|
}
|
||||||
const reader = res.body.getReader()
|
const parser = provider.dialect.parser()
|
||||||
const decoder = new TextDecoder()
|
const reader = res.body.getReader()
|
||||||
let carry = ""
|
const decoder = new TextDecoder()
|
||||||
outer: while (true) {
|
let carry = ""
|
||||||
const { done, value } = await reader.read()
|
let finished = false
|
||||||
if (done) break
|
outer: while (true) {
|
||||||
carry += decoder.decode(value, { stream: true })
|
const { done, value } = await reader.read()
|
||||||
let nl: number
|
if (done) break
|
||||||
while ((nl = carry.indexOf("\n")) !== -1) {
|
carry += decoder.decode(value, { stream: true })
|
||||||
const line = carry.slice(0, nl).replace(/\r$/, "")
|
let nl: number
|
||||||
carry = carry.slice(nl + 1)
|
while ((nl = carry.indexOf("\n")) !== -1) {
|
||||||
if (!line.startsWith("data:")) continue
|
const line = carry.slice(0, nl).replace(/\r$/, "")
|
||||||
const payload = line.slice(5).trim()
|
carry = carry.slice(nl + 1)
|
||||||
if (!payload || payload === "[DONE]") continue
|
if (!line.startsWith("data:")) continue
|
||||||
for (const event of parser.parse(payload)) {
|
const payload = line.slice(5).trim()
|
||||||
switch (event.type) {
|
if (!payload || payload === "[DONE]") continue
|
||||||
case "text-delta":
|
for (const event of parser.parse(payload)) {
|
||||||
text += event.text
|
switch (event.type) {
|
||||||
input.onDelta?.(event.text)
|
case "text-delta":
|
||||||
break
|
text += event.text
|
||||||
case "reasoning-delta":
|
input.onDelta?.(event.text)
|
||||||
break
|
break
|
||||||
case "tool-call": {
|
case "tool-call": {
|
||||||
const existing = calls.get(event.id)
|
const existing = calls.get(event.id)
|
||||||
if (existing) existing.args += event.arguments
|
if (existing) existing.args += event.arguments
|
||||||
else calls.set(event.id, { id: event.id, name: event.name, args: event.arguments })
|
else calls.set(event.id, { id: event.id, name: event.name, args: event.arguments })
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
case "finish":
|
||||||
|
finished = true
|
||||||
|
break outer
|
||||||
|
case "error":
|
||||||
|
throw new Error(`provider error: ${event.message}`)
|
||||||
}
|
}
|
||||||
case "finish":
|
|
||||||
finished = true
|
|
||||||
break outer
|
|
||||||
case "error":
|
|
||||||
throw new Error(`provider error: ${event.message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (finished) return "stop"
|
||||||
|
return "continue"
|
||||||
|
})()
|
||||||
if (calls.size === 0 || finished) {
|
if (calls.size === 0 || finished) {
|
||||||
Conversation.addTurn(graph, conv.nodeId, "neuron", text)
|
Conversation.addTurn(graph, conv.nodeId, "neuron", text)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user