@alint-js/model-adapter-acp 0.4.0 → 0.6.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 +2 -1
- package/dist/index.mjs +105 -52
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -95,4 +95,5 @@ endpoint = "http://127.0.0.1:7419/v1"
|
|
|
95
95
|
|
|
96
96
|
- Use an alint agent adapter when a rule intentionally depends on agent-specific behavior.
|
|
97
97
|
- Do not use this package as an OpenAI Responses, realtime, audio, or image compatibility layer.
|
|
98
|
-
-
|
|
98
|
+
- Sequential tool calls work with streaming and non-streaming chat completions.
|
|
99
|
+
- Parallel tool calls are not supported.
|
package/dist/index.mjs
CHANGED
|
@@ -179,6 +179,11 @@ function inputSchema(tool) {
|
|
|
179
179
|
}
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region src/gateway.ts
|
|
182
|
+
const zeroUsage = {
|
|
183
|
+
inputTokens: 0,
|
|
184
|
+
outputTokens: 0,
|
|
185
|
+
totalTokens: 0
|
|
186
|
+
};
|
|
182
187
|
const openAIFunctionToolSchema = object({
|
|
183
188
|
function: object({
|
|
184
189
|
description: optional(string()),
|
|
@@ -265,6 +270,7 @@ var CompletionTurn = class {
|
|
|
265
270
|
#cancelSession;
|
|
266
271
|
#connection;
|
|
267
272
|
#connectionDisposed = false;
|
|
273
|
+
#deferredText;
|
|
268
274
|
#finished = false;
|
|
269
275
|
#options;
|
|
270
276
|
#outputs = new PromiseQueue();
|
|
@@ -278,6 +284,7 @@ var CompletionTurn = class {
|
|
|
278
284
|
this.#toolCallCount += 1;
|
|
279
285
|
this.#pendingCalls.set(call.id, call);
|
|
280
286
|
options.onPending(call, this);
|
|
287
|
+
this.#flushDeferredText();
|
|
281
288
|
this.#outputs.push({
|
|
282
289
|
call,
|
|
283
290
|
type: "tool-call"
|
|
@@ -336,6 +343,14 @@ var CompletionTurn = class {
|
|
|
336
343
|
this.#finished = true;
|
|
337
344
|
this.#options.onFinished(this, [...this.#pendingCalls.keys()]);
|
|
338
345
|
}
|
|
346
|
+
#flushDeferredText() {
|
|
347
|
+
const chunks = this.#deferredText;
|
|
348
|
+
this.#deferredText = void 0;
|
|
349
|
+
for (const text of chunks ?? []) this.#outputs.push({
|
|
350
|
+
text,
|
|
351
|
+
type: "text-delta"
|
|
352
|
+
});
|
|
353
|
+
}
|
|
339
354
|
#matchesContinuation(request, toolCallId) {
|
|
340
355
|
const initialMessages = this.#options.request.messages;
|
|
341
356
|
const initialPrefix = request.messages.slice(0, initialMessages.length);
|
|
@@ -345,6 +360,16 @@ var CompletionTurn = class {
|
|
|
345
360
|
const toolCalls = assistantMessage.tool_calls;
|
|
346
361
|
return Array.isArray(toolCalls) && toolCalls.some((call) => isRecord(call) && call.id === toolCallId && isRecord(call.function) && call.function.name === pendingCall.name && call.function.arguments === pendingCall.arguments);
|
|
347
362
|
}
|
|
363
|
+
#pushText(text) {
|
|
364
|
+
if (this.#deferredText) {
|
|
365
|
+
this.#deferredText.push(text);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
this.#outputs.push({
|
|
369
|
+
text,
|
|
370
|
+
type: "text-delta"
|
|
371
|
+
});
|
|
372
|
+
}
|
|
348
373
|
async #run() {
|
|
349
374
|
const { cwd, model, origin, request } = this.#options;
|
|
350
375
|
try {
|
|
@@ -373,27 +398,25 @@ var CompletionTurn = class {
|
|
|
373
398
|
this.#assertActive();
|
|
374
399
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
375
400
|
const callCountBeforePrompt = this.#toolCallCount;
|
|
376
|
-
|
|
377
|
-
|
|
401
|
+
this.#deferredText = requiresTool(request.toolChoice) ? [] : void 0;
|
|
402
|
+
const prompt = session.prompt(attempt === 0 ? renderMessages(request) : ["The request requires a request-scoped MCP tool call.", `Call one of these functions before returning an answer: ${this.#options.tools.map((tool) => tool.function.name).join(", ")}.`].join(" "));
|
|
378
403
|
for (;;) {
|
|
379
404
|
const message = await session.nextUpdate();
|
|
380
405
|
if (message.kind === "stop") {
|
|
381
406
|
await prompt;
|
|
382
407
|
if (message.stopReason === "cancelled") throw new Error("The ACP agent cancelled the prompt.");
|
|
383
|
-
if (requiresTool(request.toolChoice) && this.#toolCallCount === callCountBeforePrompt)
|
|
408
|
+
if (requiresTool(request.toolChoice) && this.#toolCallCount === callCountBeforePrompt) {
|
|
409
|
+
this.#deferredText = void 0;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
384
412
|
this.#outputs.push({
|
|
385
413
|
stopReason: message.stopReason,
|
|
386
|
-
text,
|
|
387
414
|
type: "completion",
|
|
388
415
|
usage: message.response.usage
|
|
389
416
|
});
|
|
390
417
|
return;
|
|
391
418
|
}
|
|
392
|
-
if (message.update.sessionUpdate === "agent_message_chunk" && message.update.content.type === "text")
|
|
393
|
-
text: message.update.content.text,
|
|
394
|
-
type: "text-delta"
|
|
395
|
-
});
|
|
396
|
-
else text += message.update.content.text;
|
|
419
|
+
if (message.update.sessionUpdate === "agent_message_chunk" && message.update.content.type === "text") this.#pushText(message.update.content.text);
|
|
397
420
|
}
|
|
398
421
|
}
|
|
399
422
|
throw new Error("The ACP agent did not satisfy tool_choice after one retry.");
|
|
@@ -450,10 +473,6 @@ function createGateway(options) {
|
|
|
450
473
|
}
|
|
451
474
|
const toolResult = lastToolResult(request.messages);
|
|
452
475
|
let turn;
|
|
453
|
-
if (request.stream && (request.tools.length > 0 || toolResult)) {
|
|
454
|
-
event.res.status = 400;
|
|
455
|
-
return openAIError("Streaming tool calls are not supported.", "invalid_request_error");
|
|
456
|
-
}
|
|
457
476
|
if (toolResult) {
|
|
458
477
|
const pendingTurn = pendingTurns.get(toolResult.toolCallId);
|
|
459
478
|
if (!pendingTurn || !pendingTurn.resume(request, toolResult)) {
|
|
@@ -490,25 +509,20 @@ function createGateway(options) {
|
|
|
490
509
|
activeTurns.add(turn);
|
|
491
510
|
turn.start();
|
|
492
511
|
}
|
|
493
|
-
if (request.stream) return streamCompletion(turn, model.id);
|
|
512
|
+
if (request.stream) return streamCompletion(turn, model.id, options.onCompatibilityDiagnostic);
|
|
494
513
|
let output;
|
|
495
514
|
try {
|
|
496
|
-
output = await turn
|
|
515
|
+
output = await bufferCompletion(turn, event.req.signal);
|
|
497
516
|
} catch {
|
|
498
517
|
event.res.status = 500;
|
|
499
518
|
return openAIError("The ACP agent could not complete the request.", "server_error");
|
|
500
519
|
}
|
|
501
520
|
if (output.type === "tool-call") {
|
|
502
|
-
turn
|
|
503
|
-
options.onCompatibilityDiagnostic?.({
|
|
504
|
-
field: "usage",
|
|
505
|
-
message: "ACP usage is unavailable while a deferred tool call is pending; zero usage was reported.",
|
|
506
|
-
value: null
|
|
507
|
-
});
|
|
521
|
+
deferToolCall(turn, output.call, options.onCompatibilityDiagnostic);
|
|
508
522
|
return chatCompletion(turn.modelId, {
|
|
509
523
|
finishReason: "tool_calls",
|
|
510
524
|
message: {
|
|
511
|
-
content: null,
|
|
525
|
+
content: output.text || null,
|
|
512
526
|
role: "assistant",
|
|
513
527
|
tool_calls: [{
|
|
514
528
|
function: {
|
|
@@ -519,13 +533,8 @@ function createGateway(options) {
|
|
|
519
533
|
type: "function"
|
|
520
534
|
}]
|
|
521
535
|
}
|
|
522
|
-
},
|
|
523
|
-
inputTokens: 0,
|
|
524
|
-
outputTokens: 0,
|
|
525
|
-
totalTokens: 0
|
|
526
|
-
});
|
|
536
|
+
}, zeroUsage);
|
|
527
537
|
}
|
|
528
|
-
if (output.type === "text-delta") throw new Error("Text deltas are only emitted for streaming requests.");
|
|
529
538
|
return chatCompletion(turn.modelId, {
|
|
530
539
|
finishReason: finishReason(output.stopReason),
|
|
531
540
|
message: {
|
|
@@ -620,6 +629,20 @@ function authorizeRequestTools(client) {
|
|
|
620
629
|
return { outcome: { outcome: "cancelled" } };
|
|
621
630
|
});
|
|
622
631
|
}
|
|
632
|
+
async function bufferCompletion(turn, signal) {
|
|
633
|
+
let text = "";
|
|
634
|
+
for (;;) {
|
|
635
|
+
const output = await turn.nextOutput(signal);
|
|
636
|
+
if (output.type === "text-delta") {
|
|
637
|
+
text += output.text;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
...output,
|
|
642
|
+
text
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
623
646
|
function chatCompletion(model, choice, usage) {
|
|
624
647
|
return {
|
|
625
648
|
choices: [{
|
|
@@ -631,11 +654,7 @@ function chatCompletion(model, choice, usage) {
|
|
|
631
654
|
id: `chatcmpl-${crypto.randomUUID()}`,
|
|
632
655
|
model,
|
|
633
656
|
object: "chat.completion",
|
|
634
|
-
...usage ? { usage: {
|
|
635
|
-
completion_tokens: usage.outputTokens,
|
|
636
|
-
prompt_tokens: usage.inputTokens,
|
|
637
|
-
total_tokens: usage.totalTokens
|
|
638
|
-
} } : {}
|
|
657
|
+
...usage ? { usage: toOpenAIUsage(usage) } : {}
|
|
639
658
|
};
|
|
640
659
|
}
|
|
641
660
|
function chatCompletionChunk(id, created, model, choices) {
|
|
@@ -664,6 +683,14 @@ function chatCompletionRequest(value) {
|
|
|
664
683
|
function connectWith(client, connection, operation) {
|
|
665
684
|
return connection.kind === "agent-app" ? client.connectWith(connection.agent, operation) : client.connectWith(connection.stream, operation);
|
|
666
685
|
}
|
|
686
|
+
function deferToolCall(turn, call, onCompatibilityDiagnostic) {
|
|
687
|
+
turn.armContinuation(call.id);
|
|
688
|
+
onCompatibilityDiagnostic?.({
|
|
689
|
+
field: "usage",
|
|
690
|
+
message: "ACP usage is unavailable while a deferred tool call is pending; zero usage was reported.",
|
|
691
|
+
value: null
|
|
692
|
+
});
|
|
693
|
+
}
|
|
667
694
|
function enqueueSse(controller, encoder, value) {
|
|
668
695
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(value)}\n\n`));
|
|
669
696
|
}
|
|
@@ -704,11 +731,12 @@ function openAIError(message, type) {
|
|
|
704
731
|
}
|
|
705
732
|
function renderMessages(request) {
|
|
706
733
|
const requiredTool = request.toolChoice && typeof request.toolChoice === "object" ? request.toolChoice.function.name : void 0;
|
|
734
|
+
const toolNames = request.tools.map((tool) => tool.function.name).join(", ");
|
|
707
735
|
return [
|
|
708
736
|
"Process this OpenAI chat transcript. Preserve message order and role intent.",
|
|
709
737
|
"Return only the assistant response requested by the transcript.",
|
|
710
|
-
request.toolChoice === "required" ?
|
|
711
|
-
requiredTool ? `You must call the ${JSON.stringify(requiredTool)}
|
|
738
|
+
request.toolChoice === "required" ? `You must call one of these request-scoped MCP tools before answering: ${toolNames}.` : "",
|
|
739
|
+
requiredTool ? `You must call the request-scoped MCP tool ${JSON.stringify(requiredTool)} before answering.` : "",
|
|
712
740
|
"",
|
|
713
741
|
JSON.stringify({ messages: request.messages })
|
|
714
742
|
].join("\n");
|
|
@@ -727,10 +755,23 @@ function sessionExtensions(extensions) {
|
|
|
727
755
|
}
|
|
728
756
|
};
|
|
729
757
|
}
|
|
730
|
-
function streamCompletion(turn, model) {
|
|
758
|
+
function streamCompletion(turn, model, onCompatibilityDiagnostic) {
|
|
731
759
|
const encoder = new TextEncoder();
|
|
732
760
|
const id = `chatcmpl-${crypto.randomUUID()}`;
|
|
733
761
|
const created = Math.floor(Date.now() / 1e3);
|
|
762
|
+
const finishStream = (controller, reason, usage) => {
|
|
763
|
+
enqueueSse(controller, encoder, chatCompletionChunk(id, created, model, [{
|
|
764
|
+
delta: {},
|
|
765
|
+
finish_reason: reason,
|
|
766
|
+
index: 0
|
|
767
|
+
}]));
|
|
768
|
+
if (usage) enqueueSse(controller, encoder, {
|
|
769
|
+
...chatCompletionChunk(id, created, model, []),
|
|
770
|
+
usage: toOpenAIUsage(usage)
|
|
771
|
+
});
|
|
772
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
773
|
+
controller.close();
|
|
774
|
+
};
|
|
734
775
|
const body = new ReadableStream({
|
|
735
776
|
async cancel() {
|
|
736
777
|
await turn.cancel(/* @__PURE__ */ new Error("The OpenAI stream consumer cancelled the request."));
|
|
@@ -749,22 +790,27 @@ function streamCompletion(turn, model) {
|
|
|
749
790
|
}]));
|
|
750
791
|
continue;
|
|
751
792
|
}
|
|
752
|
-
if (output.type === "tool-call")
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
793
|
+
if (output.type === "tool-call") {
|
|
794
|
+
deferToolCall(turn, output.call, onCompatibilityDiagnostic);
|
|
795
|
+
enqueueSse(controller, encoder, chatCompletionChunk(id, created, model, [{
|
|
796
|
+
delta: {
|
|
797
|
+
role: "assistant",
|
|
798
|
+
tool_calls: [{
|
|
799
|
+
function: {
|
|
800
|
+
arguments: output.call.arguments,
|
|
801
|
+
name: output.call.name
|
|
802
|
+
},
|
|
803
|
+
id: output.call.id,
|
|
804
|
+
index: 0,
|
|
805
|
+
type: "function"
|
|
806
|
+
}]
|
|
807
|
+
},
|
|
808
|
+
index: 0
|
|
809
|
+
}]));
|
|
810
|
+
finishStream(controller, "tool_calls", zeroUsage);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
finishStream(controller, finishReason(output.stopReason), output.usage);
|
|
768
814
|
return;
|
|
769
815
|
}
|
|
770
816
|
} catch (error) {
|
|
@@ -782,5 +828,12 @@ function toolsForChoice(tools, choice) {
|
|
|
782
828
|
if (typeof choice === "object") return tools.filter((tool) => tool.function.name === choice.function.name);
|
|
783
829
|
return tools;
|
|
784
830
|
}
|
|
831
|
+
function toOpenAIUsage(usage) {
|
|
832
|
+
return {
|
|
833
|
+
completion_tokens: usage.outputTokens,
|
|
834
|
+
prompt_tokens: usage.inputTokens,
|
|
835
|
+
total_tokens: usage.totalTokens
|
|
836
|
+
};
|
|
837
|
+
}
|
|
785
838
|
//#endregion
|
|
786
839
|
export { createCommandModel, createGateway, startGateway };
|
package/package.json
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alint-js/model-adapter-acp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
|
+
"description": "OpenAI-compatible HTTP gateway for Agent Client Protocol coding agents",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/moeru-ai/alint.git",
|
|
10
|
+
"directory": "packages/model-adapter-acp"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"alint",
|
|
14
|
+
"agent-client-protocol",
|
|
15
|
+
"acp",
|
|
16
|
+
"openai-compatible",
|
|
17
|
+
"ai",
|
|
18
|
+
"gateway"
|
|
19
|
+
],
|
|
5
20
|
"exports": {
|
|
6
21
|
".": {
|
|
7
22
|
"types": "./dist/index.d.mts",
|
|
@@ -21,7 +36,7 @@
|
|
|
21
36
|
"zod": "^4.4.3"
|
|
22
37
|
},
|
|
23
38
|
"devDependencies": {
|
|
24
|
-
"@alint-js/core": "0.
|
|
39
|
+
"@alint-js/core": "0.6.0"
|
|
25
40
|
},
|
|
26
41
|
"scripts": {
|
|
27
42
|
"build": "tsdown",
|