toolnexus 0.25.0 → 0.27.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 +35 -0
- package/dist/acp.d.ts +10 -2
- package/dist/acp.js +36 -5
- package/dist/classifier.d.ts +199 -31
- package/dist/classifier.js +639 -31
- package/dist/client.d.ts +6 -0
- package/dist/judge.d.ts +6 -3
- package/dist/judge.js +13 -1
- package/dist/mcp.js +16 -6
- package/dist/mcp2026/source.d.ts +5 -0
- package/dist/mcp2026/source.js +11 -2
- package/dist/mcp2026/stdio.d.ts +43 -0
- package/dist/mcp2026/stdio.js +144 -20
- package/dist/mcphandler.js +2 -4
- package/dist/mcpserve.js +2 -1
- package/dist/serve.js +12 -1
- package/dist/skill.d.ts +14 -0
- package/dist/skill.js +33 -2
- package/package.json +2 -2
- package/src/acp.ts +59 -7
- package/src/classifier.ts +723 -47
- package/src/client.ts +6 -0
- package/src/judge.ts +19 -4
- package/src/mcp.ts +18 -6
- package/src/mcp2026/source.ts +14 -4
- package/src/mcp2026/stdio.ts +148 -23
- package/src/mcphandler.ts +2 -3
- package/src/mcpserve.ts +2 -1
- package/src/serve.ts +13 -2
- package/src/skill.ts +51 -2
package/src/acp.ts
CHANGED
|
@@ -86,15 +86,28 @@ export interface ACPOptions {
|
|
|
86
86
|
/** One warm ACP connection: one child process, one session, alive across many
|
|
87
87
|
* turns. `.generate` has exactly the shape {@link InProcessOptions.generate}
|
|
88
88
|
* wants, so it plugs straight into {@link createInProcessClient}. */
|
|
89
|
+
/** Per-call options for {@link ACPClient.generate} / {@link ACPClient.generateStream}. */
|
|
90
|
+
export interface ACPGenerateOptions {
|
|
91
|
+
/** Cancels the turn: when it aborts mid-turn the `session/cancel` notification is sent,
|
|
92
|
+
* the turn's reply is awaited (bounded by `requestTimeoutMs`, default 30s) so the next
|
|
93
|
+
* turn never lands on a busy session, and the call rejects with an `AbortError`. The
|
|
94
|
+
* process and session stay warm. */
|
|
95
|
+
signal?: AbortSignal
|
|
96
|
+
}
|
|
97
|
+
|
|
89
98
|
export interface ACPClient {
|
|
90
|
-
generate: (request: InProcessRequest) => Promise<InProcessResponse>
|
|
99
|
+
generate: (request: InProcessRequest, opts?: ACPGenerateOptions) => Promise<InProcessResponse>
|
|
91
100
|
/** {@link generate}, streaming: `emit` fires as each `session/update` arrives —
|
|
92
101
|
* agent_message_chunk → `text`, agent_thought_chunk → `thought` (via "acp"),
|
|
93
102
|
* tool_call → `tool_call` and a completed/failed tool_call_update → `tool_result`
|
|
94
103
|
* (both via "acp": the agent's own tools, reported, never executed by the loop).
|
|
95
104
|
* Shaped for {@link InProcessOptions.generateStream}. The returned content is the
|
|
96
105
|
* message text only. */
|
|
97
|
-
generateStream: (
|
|
106
|
+
generateStream: (
|
|
107
|
+
request: InProcessRequest,
|
|
108
|
+
emit: (event: StreamEvent) => void,
|
|
109
|
+
opts?: ACPGenerateOptions,
|
|
110
|
+
) => Promise<InProcessResponse>
|
|
98
111
|
/** Terminates the agent's process group. Idempotent — a second call is a no-op. */
|
|
99
112
|
close: () => Promise<void>
|
|
100
113
|
/** The agent's raw `initialize` result. */
|
|
@@ -375,7 +388,12 @@ export async function loadACP(opts: ACPOptions): Promise<ACPClient> {
|
|
|
375
388
|
// queue itself never rejects (a failed turn does not poison later ones).
|
|
376
389
|
let turnQueue: Promise<unknown> = Promise.resolve()
|
|
377
390
|
|
|
378
|
-
async function promptOnce(
|
|
391
|
+
async function promptOnce(
|
|
392
|
+
request: InProcessRequest,
|
|
393
|
+
emit?: (event: StreamEvent) => void,
|
|
394
|
+
signal?: AbortSignal,
|
|
395
|
+
): Promise<InProcessResponse> {
|
|
396
|
+
if (signal?.aborted) throw acpAbortError()
|
|
379
397
|
const text = renderPrompt(request)
|
|
380
398
|
const id = `c-${++nextId}`
|
|
381
399
|
|
|
@@ -440,21 +458,48 @@ export async function loadACP(opts: ACPOptions): Promise<ACPClient> {
|
|
|
440
458
|
)
|
|
441
459
|
})
|
|
442
460
|
|
|
461
|
+
let onAbort: (() => void) | undefined
|
|
462
|
+
const cancelled = new Promise<"cancelled">((resolve) => {
|
|
463
|
+
if (!signal) return
|
|
464
|
+
onAbort = () => resolve("cancelled")
|
|
465
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
466
|
+
})
|
|
467
|
+
|
|
443
468
|
try {
|
|
444
|
-
const msg = await Promise.race([replyPromise, timeout])
|
|
469
|
+
const msg = await Promise.race([replyPromise, timeout, cancelled])
|
|
470
|
+
if (msg === "cancelled") {
|
|
471
|
+
// ACP: session/cancel is a notification (no id); the agent then ends the turn with
|
|
472
|
+
// stopReason "cancelled". Wait (bounded) for that reply so the session is idle again.
|
|
473
|
+
write({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } })
|
|
474
|
+
const waitMs = opts.requestTimeoutMs && opts.requestTimeoutMs > 0 ? opts.requestTimeoutMs : 30000
|
|
475
|
+
let wait: NodeJS.Timeout | undefined
|
|
476
|
+
await Promise.race([
|
|
477
|
+
replyPromise,
|
|
478
|
+
new Promise((r) => {
|
|
479
|
+
wait = setTimeout(r, waitMs)
|
|
480
|
+
}),
|
|
481
|
+
])
|
|
482
|
+
clearTimeout(wait)
|
|
483
|
+
throw acpAbortError()
|
|
484
|
+
}
|
|
445
485
|
if (msg.error && msg.error.code === 0) throw new Error(msg.error.message)
|
|
446
486
|
if (msg.error) throw new Error(`toolnexus: ACP error ${msg.error.code}: ${msg.error.message}`)
|
|
447
487
|
return { content }
|
|
448
488
|
} finally {
|
|
449
489
|
clearTimeout(timer)
|
|
490
|
+
if (onAbort) signal!.removeEventListener("abort", onAbort)
|
|
450
491
|
pending.delete(id)
|
|
451
492
|
updateSink = null
|
|
452
493
|
}
|
|
453
494
|
}
|
|
454
495
|
|
|
455
|
-
const generateStream = (
|
|
496
|
+
const generateStream = (
|
|
497
|
+
request: InProcessRequest,
|
|
498
|
+
emit?: (event: StreamEvent) => void,
|
|
499
|
+
callOpts?: ACPGenerateOptions,
|
|
500
|
+
): Promise<InProcessResponse> => {
|
|
456
501
|
if (closed) return Promise.reject(new Error("toolnexus: ACP client is closed"))
|
|
457
|
-
const turn = turnQueue.then(() => promptOnce(request, emit))
|
|
502
|
+
const turn = turnQueue.then(() => promptOnce(request, emit, callOpts?.signal))
|
|
458
503
|
turnQueue = turn.then(
|
|
459
504
|
() => undefined,
|
|
460
505
|
() => undefined,
|
|
@@ -478,7 +523,8 @@ export async function loadACP(opts: ACPOptions): Promise<ACPClient> {
|
|
|
478
523
|
if (t === "late") await Promise.race([childGone, new Promise((r) => setTimeout(r, 1000).unref())])
|
|
479
524
|
}
|
|
480
525
|
|
|
481
|
-
const generate = (request: InProcessRequest): Promise<InProcessResponse> =>
|
|
526
|
+
const generate = (request: InProcessRequest, callOpts?: ACPGenerateOptions): Promise<InProcessResponse> =>
|
|
527
|
+
generateStream(request, undefined, callOpts)
|
|
482
528
|
|
|
483
529
|
const client: ACPClient = { generate, generateStream, close, initializeResult }
|
|
484
530
|
// Non-enumerable, undocumented — internal test hook only, so the public
|
|
@@ -487,6 +533,12 @@ export async function loadACP(opts: ACPOptions): Promise<ACPClient> {
|
|
|
487
533
|
return client
|
|
488
534
|
}
|
|
489
535
|
|
|
536
|
+
function acpAbortError(): Error {
|
|
537
|
+
const e = new Error("toolnexus: ACP turn cancelled")
|
|
538
|
+
e.name = "AbortError"
|
|
539
|
+
return e
|
|
540
|
+
}
|
|
541
|
+
|
|
490
542
|
/** Signals the child's whole process group (POSIX), else the child itself. Never throws. */
|
|
491
543
|
function killGroup(pid: number, signal: NodeJS.Signals): void {
|
|
492
544
|
if (process.platform !== "win32") {
|