@monotykamary/dsh-acp 0.1.0-rc.5
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +81 -0
- package/README.zh.md +81 -0
- package/lib/index.js +530 -0
- package/lib/invariant.js +23 -0
- package/lib/types/codec.d.ts +30 -0
- package/lib/types/index.d.ts +33 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/acp/acp/README.md
|
|
5
|
+
README.md: 3cb8aa44decb5829a0a6794e3c4f39c99f12808e
|
|
6
|
+
README.zh.md: 9f4610354d7443f288397b3227f0abb79594a683
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @monotykamary/dsh-acp
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Automation-only [Agent Client Protocol](https://agentclientprotocol.com) server over JSON-RPC stdio. Programmatic clients create fresh harness agents, send text prompts, collect committed assistant text, resolve one-shot permission requests by policy, and cancel work. The primary in-repository client is [`dsh-subagent-acp`](../../subagent/subagent-acp/README.md).
|
|
6
|
+
|
|
7
|
+
This package is a transport adapter, not a UI integration or a capability seam. It does not expose editor navigation, transcript replay, commands, modes, configuration pickers, elicitation, reasoning, plans, titles, or tool presentation. Interactive rendering and human questions belong to the Web host and client modules.
|
|
8
|
+
|
|
9
|
+
## Plugin
|
|
10
|
+
|
|
11
|
+
`apply(ctx, config)` opens an `AgentSideConnection` on stdin/stdout and drives `ctx.agents`. Stdout is reserved for protocol frames.
|
|
12
|
+
|
|
13
|
+
| Config | Default | Meaning |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `provider` | — | Initial provider route for every created agent. |
|
|
16
|
+
| `model` | — | Initial model for every created agent. |
|
|
17
|
+
|
|
18
|
+
Both fields are optional so another agent/request listener may supply the target. The runnable ACP composition requires both.
|
|
19
|
+
|
|
20
|
+
## Protocol contract
|
|
21
|
+
|
|
22
|
+
| Method | Behavior |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `initialize` | Negotiates the supported version and advertises baseline-only prompts (no image, audio, or embedded-context capability). No session, editor, terminal, filesystem, or MCP capability is advertised. |
|
|
25
|
+
| `authenticate` | No-op because the server advertises no authentication methods. |
|
|
26
|
+
| `session/new` | Creates a fresh agent with an absolute primary `cwd`; empty `additionalDirectories` and `mcpServers` are accepted, non-empty values reject. |
|
|
27
|
+
| `session/prompt` | Concatenates text blocks, renders baseline resource links as bracketed textual references, rejects empty or beyond-baseline input, permits one in-flight request per session, and waits for the whole agent to become idle. Normal quiescence reports `end_turn`; explicit ACP cancellation, disposal, or a prompt whose admission was discarded (a turnless slot) reports `cancelled`. |
|
|
28
|
+
| `session/cancel` | Cancels only the addressed agent and settles its pending prompt as `cancelled`; unknown ids are no-ops. |
|
|
29
|
+
| `session/update` | Emits one `agent_message_chunk` per non-empty text block in a committed `assistant/message`. Raw deltas and non-message events are omitted. |
|
|
30
|
+
| `session/request_permission` | Offers one-shot allow/reject choices for bridge-owned approval requests carrying a tool call id. Clients may answer automatically. |
|
|
31
|
+
|
|
32
|
+
One connection may own several sessions. The bridge keys records by branded session id and checks exact agent identity before routing events or permission requests. Each session has an independent prompt slot, workspace, cancellation path, and disposer.
|
|
33
|
+
|
|
34
|
+
Committed-message output intentionally trades token-by-token latency for a clean automation result. Uncommitted provider chunks and retry attempts cannot leak partial text; reasoning and tool activity remain in the session log for observability through other interfaces.
|
|
35
|
+
|
|
36
|
+
## Lifecycle
|
|
37
|
+
|
|
38
|
+
Client disconnect and Cordis disposal share one memoized teardown. The bridge first rejects new sessions and prompts, settles pending prompts, then drains continuable descendants only below this connection's exact owned Agents before disposing those handles in parallel and awaiting every result before reporting any failure. Other frontends sharing the Context retain their continuable forests and admission. An ACP-only plugin reload therefore leaves no orphan agent.
|
|
39
|
+
|
|
40
|
+
ACP requires each prompt response to carry a `stopReason`, but the bridge does not claim a prompt-specific turn outcome. Committed assistant messages stream across the owned activity, and steering or injected work may contribute before idle. Token-limit turn endings therefore do not become prompt-level ACP stop reasons (they settle as `end_turn`); a model error on the correlated turn rejects the prompt immediately.
|
|
41
|
+
|
|
42
|
+
## Running
|
|
43
|
+
|
|
44
|
+
`pnpm --dir /path/to/deepseek-harness run demo:acp` boots the repository's automation server composition. A parent harness can spawn it through [`@monotykamary/dsh-subagent-acp`](../../subagent/subagent-acp/README.md); other ACP clients need only the core methods above.
|
|
45
|
+
|
|
46
|
+
## Model Experience
|
|
47
|
+
|
|
48
|
+
### Prompt text
|
|
49
|
+
|
|
50
|
+
#### What the model sees
|
|
51
|
+
|
|
52
|
+
`session/prompt` text blocks are concatenated verbatim into one user message; a baseline resource link appears in that message as a bracketed `[resource_link name=… uri=…]` reference the model may open with its own tools. Protocol metadata, client capabilities, permission choices, and session ids never enter the model request.
|
|
53
|
+
|
|
54
|
+
#### Token effect
|
|
55
|
+
|
|
56
|
+
Prompt tokens are data-dependent and remain in that session's history until compaction. Concurrent ACP sessions retain independent contexts.
|
|
57
|
+
|
|
58
|
+
#### KV Cache effect
|
|
59
|
+
|
|
60
|
+
Append-only; the new user message follows the reusable request prefix and does not invalidate prior cache entries.
|
|
61
|
+
|
|
62
|
+
### Permission decisions
|
|
63
|
+
|
|
64
|
+
#### What the model sees
|
|
65
|
+
|
|
66
|
+
Nothing directly. The owning tool records its allowed, rejected, cancelled, or unavailable outcome through the normal tool-result path.
|
|
67
|
+
|
|
68
|
+
#### Token effect
|
|
69
|
+
|
|
70
|
+
Only the owning tool result contributes tokens.
|
|
71
|
+
|
|
72
|
+
#### KV Cache effect
|
|
73
|
+
|
|
74
|
+
Append-only through the owning tool result.
|
|
75
|
+
|
|
76
|
+
## Known Limitations and Deferred Work
|
|
77
|
+
|
|
78
|
+
- **Fresh sessions only** — load, list, resume, delete, and fork are unsupported.
|
|
79
|
+
- **Baseline prompts and one workspace only** — images, audio, embedded resources, non-empty additional directories, and MCP servers reject; resource links flatten to textual references rather than fetched content.
|
|
80
|
+
- **Committed answers only** — live progress, reasoning, tool activity, plans, titles, and usage stay off the wire.
|
|
81
|
+
- **Connection-owned lifetime** — one connection releases all of its sessions; per-session close is not implemented.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @monotykamary/dsh-acp
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
通过 JSON-RPC stdio 提供的仅面向自动化的 [ACP(Agent Client Protocol)](https://agentclientprotocol.com) 服务器。程序化客户端可以创建新 harness agent(智能体)、发送文本提示词、收集已提交的 assistant 文本、按策略响应一次性权限请求并取消工作。仓库中的主要客户端是 [`dsh-subagent-acp`](../../subagent/subagent-acp/README.md)。
|
|
6
|
+
|
|
7
|
+
此包是传输适配器,而非 UI 集成或能力 seam。它不公开编辑器导航、transcript(文本记录)回放、命令、模式、配置选择器、信息征集、推理(reasoning)、计划、标题或工具展示。交互式渲染与向用户提问属于 Web 宿主和客户端模块。
|
|
8
|
+
|
|
9
|
+
## 插件
|
|
10
|
+
|
|
11
|
+
`apply(ctx, config)` 在 stdin/stdout 上打开 `AgentSideConnection` 并驱动 `ctx.agents`。Stdout 专用于协议帧。
|
|
12
|
+
|
|
13
|
+
| 配置 | 默认值 | 含义 |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `provider` | 无 | 每个已创建 agent 的初始提供方路由。 |
|
|
16
|
+
| `model` | 无 | 每个已创建 agent 的初始模型。 |
|
|
17
|
+
|
|
18
|
+
两个字段都是可选的,以便由另一个 agent/request 监听器提供目标。可运行的 ACP 组合同时要求两者。
|
|
19
|
+
|
|
20
|
+
## 协议约定
|
|
21
|
+
|
|
22
|
+
| 方法 | 行为 |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `initialize` | 协商受支持的版本,并仅公布基线提示词(无图像、音频或嵌入上下文能力)。不公布会话、编辑器、终端、文件系统或 MCP 能力。 |
|
|
25
|
+
| `authenticate` | 空操作,因为服务器不公布身份验证方法。 |
|
|
26
|
+
| `session/new` | 以绝对路径作为主 `cwd` 创建新 agent;接受空的 `additionalDirectories` 和 `mcpServers`,拒绝非空值。 |
|
|
27
|
+
| `session/prompt` | 拼接文本块,将基线资源链接渲染为带方括号的文本引用,拒绝空输入或超出基线的输入,每个会话只允许一个正在处理的请求,并等待整个 agent 进入空闲状态。正常完全停稳时报告 `end_turn`;显式 ACP 取消、资源释放,或准入被丢弃的提示词(无轮次槽位)时报告 `cancelled`。 |
|
|
28
|
+
| `session/cancel` | 仅取消指定的 agent,并将其待处理提示词结算为 `cancelled`;未知 id 为空操作。 |
|
|
29
|
+
| `session/update` | 为每个非空文本块发出一个 `agent_message_chunk`;这些文本块来自已提交的 `assistant/message`。省略原始增量和非消息事件。 |
|
|
30
|
+
| `session/request_permission` | 为携带工具调用 id、由桥接层拥有的批准请求提供一次性允许/拒绝选项。客户端可以自动回答。 |
|
|
31
|
+
|
|
32
|
+
一个连接可以拥有多个会话。桥接层以带品牌的会话 id 作为记录键,并在路由事件或权限请求前检查 agent 是否为同一对象。每个会话都有独立的提示词槽位、工作区、取消路径和资源释放器。
|
|
33
|
+
|
|
34
|
+
已提交消息输出有意牺牲逐 token 输出的低延迟,以换取干净的自动化结果。未提交的提供方分片和重试尝试无法泄漏部分文本;推理与工具活动仍保留在会话日志中,以便其他界面观测。
|
|
35
|
+
|
|
36
|
+
## 生命周期
|
|
37
|
+
|
|
38
|
+
客户端断开与 Cordis 释放共用同一个记忆化清理流程。桥接层先拒绝新会话和提示词,结算待处理提示词,然后只 drain 此连接确切拥有的 Agent 之下的可继续后代,再并行释放这些 handle,并等待全部结果结算后才报告失败。其他共享该上下文的前端会保留其可继续森林和准入。因此,仅 ACP 的插件重载不会遗留 agent。
|
|
39
|
+
|
|
40
|
+
ACP 要求每个提示词响应都携带 `stopReason`,但桥接层不声称它表示提示词专属的轮次结果。已提交的 assistant 消息会在整个自有活动期间流式输出,agent 进入空闲状态前发生的 steering(中途引导)或注入工作也可能参与其中。因此,因 token 上限而结束的轮次不会成为提示词级 ACP 停止原因(它们以 `end_turn` 结算);关联轮次上的模型错误会立即拒绝该提示词。
|
|
41
|
+
|
|
42
|
+
## 运行
|
|
43
|
+
|
|
44
|
+
`pnpm --dir /path/to/deepseek-harness run demo:acp` 启动仓库的自动化服务器组合。父 harness 可以通过 [`@monotykamary/dsh-subagent-acp`](../../subagent/subagent-acp/README.md) spawn 它;其他 ACP 客户端只需上述核心方法。
|
|
45
|
+
|
|
46
|
+
## 模型体验
|
|
47
|
+
|
|
48
|
+
### 提示词文本
|
|
49
|
+
|
|
50
|
+
#### 模型看到的内容
|
|
51
|
+
|
|
52
|
+
`session/prompt` 文本块会原样拼接为一条用户消息;基线资源链接会在该消息中表示为带方括号的 `[resource_link name=… uri=…]` 引用,模型可以使用自身工具打开它。协议元数据、客户端能力、权限选择和会话 id 绝不进入模型请求。
|
|
53
|
+
|
|
54
|
+
#### Token 影响
|
|
55
|
+
|
|
56
|
+
提示词 token 取决于数据,并保留在该会话的历史中直到上下文压缩(context compaction)。并发 ACP 会话保留独立上下文。
|
|
57
|
+
|
|
58
|
+
#### KV Cache 影响
|
|
59
|
+
|
|
60
|
+
仅追加;新用户消息位于可复用请求前缀之后,不会使先前缓存条目失效。
|
|
61
|
+
|
|
62
|
+
### 权限决策
|
|
63
|
+
|
|
64
|
+
#### 模型看到的内容
|
|
65
|
+
|
|
66
|
+
不会直接看到任何内容。所属工具通过常规工具结果路径记录其结果:允许、拒绝、取消或不可用。
|
|
67
|
+
|
|
68
|
+
#### Token 影响
|
|
69
|
+
|
|
70
|
+
只有所属工具的结果会贡献 token。
|
|
71
|
+
|
|
72
|
+
#### KV Cache 影响
|
|
73
|
+
|
|
74
|
+
仅通过所属工具的结果追加。
|
|
75
|
+
|
|
76
|
+
## 已知限制与暂缓事项
|
|
77
|
+
|
|
78
|
+
- **仅新会话**:不支持加载、列出、恢复、删除和 fork。
|
|
79
|
+
- **仅基线提示词和一个 workspace**:图像、音频、嵌入资源、非空附加目录和 MCP 服务器都会被拒绝;资源链接只会展平为文本引用,不会获取其内容。
|
|
80
|
+
- **仅已提交答案**:实时进度、推理、工具活动、计划、标题和用量不会通过协议传输。
|
|
81
|
+
- **由连接管理的生命周期**:一个连接会释放其所有会话;尚未实现单个会话关闭功能。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { Readable, Writable } from "node:stream";
|
|
5
|
+
import Schema from "@monotykamary/schemastery";
|
|
6
|
+
import "@monotykamary/cordis";
|
|
7
|
+
import { AgentSideConnection, PROTOCOL_VERSION, RequestError, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
8
|
+
import { SessionId } from "@monotykamary/dsh-session";
|
|
9
|
+
//#region ../../llm/llm/src/brand.ts
|
|
10
|
+
/**
|
|
11
|
+
* Brand a message identifier.
|
|
12
|
+
* @param id - the opaque message identifier.
|
|
13
|
+
* @returns the same string, branded; no validation is performed.
|
|
14
|
+
*/
|
|
15
|
+
function MessageId(id) {
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region ../../llm/llm/src/call-config.ts
|
|
20
|
+
/**
|
|
21
|
+
* Deep-freeze a value in place with an iterative traversal, guarding cycles,
|
|
22
|
+
* so later mutation throws without imposing a JavaScript call-stack depth cap.
|
|
23
|
+
* {@link AbortSignal} objects are deliberately skipped because they are the
|
|
24
|
+
* request's live cancellation channel and freezing them breaks abort.
|
|
25
|
+
* @param value - the value to freeze in place.
|
|
26
|
+
* @returns the same value, frozen.
|
|
27
|
+
*/
|
|
28
|
+
function deepFreeze(value) {
|
|
29
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
30
|
+
const pending = [{
|
|
31
|
+
kind: "visit",
|
|
32
|
+
node: value
|
|
33
|
+
}];
|
|
34
|
+
while (pending.length > 0) {
|
|
35
|
+
const task = pending.pop();
|
|
36
|
+
/* v8 ignore next -- the loop condition guarantees one pending task. */
|
|
37
|
+
if (task === void 0) continue;
|
|
38
|
+
if (task.kind === "property") {
|
|
39
|
+
pending.push({
|
|
40
|
+
kind: "visit",
|
|
41
|
+
node: task.source[task.key]
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const node = task.node;
|
|
46
|
+
if (node === null || typeof node !== "object") continue;
|
|
47
|
+
if (node instanceof AbortSignal) continue;
|
|
48
|
+
if (seen.has(node)) continue;
|
|
49
|
+
seen.add(node);
|
|
50
|
+
Object.freeze(node);
|
|
51
|
+
const keys = Object.keys(node);
|
|
52
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
53
|
+
const key = keys[index];
|
|
54
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
55
|
+
if (key === void 0) continue;
|
|
56
|
+
pending.push({
|
|
57
|
+
kind: "property",
|
|
58
|
+
source: node,
|
|
59
|
+
key
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region ../../llm/llm/src/message.ts
|
|
67
|
+
/** Message value types, identity, and immutable construction helpers. */
|
|
68
|
+
/**
|
|
69
|
+
* Detach and deep-freeze a message whose identity already exists.
|
|
70
|
+
* @param message - complete message, including its stable identity.
|
|
71
|
+
* @returns an immutable snapshot that preserves the identity.
|
|
72
|
+
*/
|
|
73
|
+
function freezeMessage(message) {
|
|
74
|
+
return deepFreeze(structuredClone(message));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create one identified message and freeze it before publication.
|
|
78
|
+
* @param input - complete role, content, and source for a new message.
|
|
79
|
+
* @returns an immutable message with a fresh stable identity.
|
|
80
|
+
*/
|
|
81
|
+
function createMessage(input) {
|
|
82
|
+
return freezeMessage({
|
|
83
|
+
...input,
|
|
84
|
+
id: MessageId(crypto.randomUUID())
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Create one identified user-role message and freeze it before publication.
|
|
89
|
+
* @param input - complete content and source for a new user message.
|
|
90
|
+
* @returns an immutable user message with a fresh stable identity.
|
|
91
|
+
*/
|
|
92
|
+
function createUserMessage(input) {
|
|
93
|
+
return createMessage({
|
|
94
|
+
...input,
|
|
95
|
+
role: "user"
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region ../../util/timeout/src/index.ts
|
|
100
|
+
/** Largest delay Node schedules without clamping it to one millisecond. */
|
|
101
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region ../../llm/llm/src/error.ts
|
|
104
|
+
/**
|
|
105
|
+
* Canonical provider-neutral code for a response that completed normally but
|
|
106
|
+
* carried no content blocks at all. Providers occasionally emit a degenerate
|
|
107
|
+
* completion (a terminal stop with zero output); adapters classify it as this
|
|
108
|
+
* failure instead of yielding an empty assistant message, because an empty
|
|
109
|
+
* message silently ends the turn with nothing for the user or the loop to act
|
|
110
|
+
* on. The attempt produced nothing durable, so retry policy treats it as safe
|
|
111
|
+
* to repeat.
|
|
112
|
+
*/
|
|
113
|
+
const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
|
|
114
|
+
new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
|
|
115
|
+
new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
|
|
116
|
+
new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
|
|
117
|
+
/**
|
|
118
|
+
* Render a thrown value with its full `cause` chain and AggregateError
|
|
119
|
+
* members, so transport wrappers like undici's `TypeError: fetch failed`
|
|
120
|
+
* surface the underlying failure instead of masking it. Plain structured
|
|
121
|
+
* failures render their own data-backed `message`. Diagnostic-surface
|
|
122
|
+
* rendering only (messages, notices, logs) — never parse the result; route on
|
|
123
|
+
* {@link HarnessError.code}.
|
|
124
|
+
* @param value - the caught value (`unknown` in catch clauses).
|
|
125
|
+
* @returns the outermost message first, each cause appended with `: ` (skipped
|
|
126
|
+
* when it repeats the wrapper message verbatim), and AggregateError members
|
|
127
|
+
* bracketed and `; `-joined.
|
|
128
|
+
*/
|
|
129
|
+
function errorChain(value) {
|
|
130
|
+
const path = /* @__PURE__ */ new Set();
|
|
131
|
+
const render = (current) => {
|
|
132
|
+
if (path.has(current)) return "<circular cause>";
|
|
133
|
+
path.add(current);
|
|
134
|
+
try {
|
|
135
|
+
if (!(current instanceof Error)) {
|
|
136
|
+
if (typeof current === "object" && current !== null) {
|
|
137
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, "message");
|
|
138
|
+
if (descriptor !== void 0 && "value" in descriptor && typeof descriptor.value === "string") return descriptor.value;
|
|
139
|
+
}
|
|
140
|
+
return String(current);
|
|
141
|
+
}
|
|
142
|
+
const message = current.message === "" ? current.name : current.message;
|
|
143
|
+
const members = current instanceof AggregateError && current.errors.length > 0 ? ` [${current.errors.map(render).join("; ")}]` : "";
|
|
144
|
+
const causeText = current.cause === void 0 || current.cause === null ? "" : render(current.cause);
|
|
145
|
+
return `${message}${members}${causeText === "" || causeText === message ? "" : `: ${causeText}`}`;
|
|
146
|
+
} catch {
|
|
147
|
+
return "<unrenderable value>";
|
|
148
|
+
} finally {
|
|
149
|
+
path.delete(current);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
return render(value);
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region ../../llm/llm/src/retry-policy.ts
|
|
156
|
+
/**
|
|
157
|
+
* Provider-owned request-retry policy configuration and resolution.
|
|
158
|
+
*
|
|
159
|
+
* Adapters expose one resolved policy per registered provider route; the
|
|
160
|
+
* optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
|
|
161
|
+
*
|
|
162
|
+
* @module @monotykamary/dsh-llm/retry-policy
|
|
163
|
+
*/
|
|
164
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
165
|
+
const DEFAULT_INITIAL_DELAY_MS = 500;
|
|
166
|
+
const DEFAULT_MAX_DELAY_MS = 1e4;
|
|
167
|
+
const DEFAULT_JITTER_RATIO = .1;
|
|
168
|
+
const DEFAULT_RETRYABLE_CODES = Object.freeze([
|
|
169
|
+
EMPTY_RESPONSE_CODE,
|
|
170
|
+
"RATE_LIMIT",
|
|
171
|
+
"SERVER",
|
|
172
|
+
"TIMEOUT",
|
|
173
|
+
"TRANSPORT"
|
|
174
|
+
]);
|
|
175
|
+
const backoffSchema = Schema.object({
|
|
176
|
+
initialDelayMs: Schema.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
|
|
177
|
+
maxDelayMs: Schema.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
|
|
178
|
+
jitterRatio: Schema.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
|
|
179
|
+
});
|
|
180
|
+
const normalPolicySchema = Schema.object({
|
|
181
|
+
mode: Schema.const("normal").required(),
|
|
182
|
+
maxRetries: Schema.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
|
|
183
|
+
retryableCodes: Schema.array(Schema.string()).default([...DEFAULT_RETRYABLE_CODES]),
|
|
184
|
+
backoff: backoffSchema
|
|
185
|
+
});
|
|
186
|
+
const alwaysPolicySchema = Schema.object({
|
|
187
|
+
mode: Schema.const("always").required(),
|
|
188
|
+
backoff: backoffSchema
|
|
189
|
+
});
|
|
190
|
+
Schema.union([normalPolicySchema, alwaysPolicySchema]);
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region ../../llm/llm/src/attribution.ts
|
|
193
|
+
/**
|
|
194
|
+
* Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
|
|
195
|
+
* adapters from drifting. See
|
|
196
|
+
* `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
|
197
|
+
*
|
|
198
|
+
* App-attribution vocabulary for provider requests.
|
|
199
|
+
* @module @monotykamary/dsh-llm/attribution
|
|
200
|
+
*/
|
|
201
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region lib/types/codec.js
|
|
204
|
+
/**
|
|
205
|
+
* Pure translation between the harness lifecycle and the automation-only ACP wire.
|
|
206
|
+
* @module @monotykamary/dsh-acp/codec
|
|
207
|
+
*/
|
|
208
|
+
/**
|
|
209
|
+
* Map a harness turn ending to ACP's terminal reason vocabulary.
|
|
210
|
+
* @param reason - harness turn outcome.
|
|
211
|
+
* @returns the closest legal ACP stop reason.
|
|
212
|
+
*/
|
|
213
|
+
function turnEndToStopReason(reason) {
|
|
214
|
+
switch (reason.kind) {
|
|
215
|
+
case "completed": return "end_turn";
|
|
216
|
+
case "max-tokens": return "max_tokens";
|
|
217
|
+
case "aborted": return "end_turn";
|
|
218
|
+
case "interrupted": return "cancelled";
|
|
219
|
+
case "blocked":
|
|
220
|
+
case "error": return "end_turn";
|
|
221
|
+
/* v8 ignore next 2 -- TurnEndReason is closed and every member is handled above */
|
|
222
|
+
default: return "end_turn";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Flatten an ACP prompt's baseline blocks to text. Text blocks concatenate
|
|
227
|
+
* verbatim; resource links become explicit textual references so a baseline
|
|
228
|
+
* client can point at files without the bridge silently dropping that context.
|
|
229
|
+
* @param prompt - supported ACP prompt blocks.
|
|
230
|
+
* @returns text in wire order, with resource links rendered as bracketed references.
|
|
231
|
+
*/
|
|
232
|
+
function acpPromptToText(prompt) {
|
|
233
|
+
return prompt.flatMap((block) => {
|
|
234
|
+
switch (block.type) {
|
|
235
|
+
case "text": return [block.text];
|
|
236
|
+
case "resource_link": return [`\n[resource_link name=${JSON.stringify(block.name)} uri=${JSON.stringify(block.uri)}]\n`];
|
|
237
|
+
default: return [];
|
|
238
|
+
}
|
|
239
|
+
}).join("");
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Whether a prompt carries content beyond the ACP baseline. The spec requires
|
|
243
|
+
* every agent to accept `text` and `resource_link`; richer inline payloads
|
|
244
|
+
* (image, audio, embedded resource) are optional capabilities this bridge does
|
|
245
|
+
* not advertise, so they are rejected rather than silently dropped.
|
|
246
|
+
* @param prompt - ACP prompt blocks to inspect.
|
|
247
|
+
* @returns `true` when any block is neither `text` nor `resource_link`.
|
|
248
|
+
*/
|
|
249
|
+
function promptHasUnsupportedContent(prompt) {
|
|
250
|
+
return prompt.some((block) => block.type !== "text" && block.type !== "resource_link");
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region lib/types/index.js
|
|
254
|
+
/**
|
|
255
|
+
* Automation-only Agent Client Protocol server over JSON-RPC stdio.
|
|
256
|
+
*
|
|
257
|
+
* The bridge exposes fresh harness sessions to trusted programmatic clients. It
|
|
258
|
+
* carries prompt text, committed assistant text, cancellation, and one-shot
|
|
259
|
+
* permission decisions; presentation and human-interaction features stay with
|
|
260
|
+
* the harness's UI modules.
|
|
261
|
+
*
|
|
262
|
+
* @module @monotykamary/dsh-acp
|
|
263
|
+
*/
|
|
264
|
+
const name = "acp";
|
|
265
|
+
/** The bridge creates and owns agents; every other concern is carried by the agent composition. */
|
|
266
|
+
const inject = ["agents"];
|
|
267
|
+
/** Preserve invalid-parameter detail in the SDK wire error message. */
|
|
268
|
+
function invalidParams(detail) {
|
|
269
|
+
return RequestError.invalidParams(void 0, detail);
|
|
270
|
+
}
|
|
271
|
+
/** Preserve failed-turn detail; plain handler errors become a generic wire internal error. */
|
|
272
|
+
function internalError(detail) {
|
|
273
|
+
return RequestError.internalError(void 0, detail);
|
|
274
|
+
}
|
|
275
|
+
const Config = Schema.object({
|
|
276
|
+
provider: Schema.string(),
|
|
277
|
+
model: Schema.string()
|
|
278
|
+
});
|
|
279
|
+
/**
|
|
280
|
+
* Mount the automation-only ACP server.
|
|
281
|
+
* @param ctx - Cordis context carrying the agent factory and session events.
|
|
282
|
+
* @param config - Initial provider/model selection and optional test transport.
|
|
283
|
+
*/
|
|
284
|
+
function apply(ctx, config) {
|
|
285
|
+
const agents = ctx.agents;
|
|
286
|
+
const logger = ctx.logger;
|
|
287
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
288
|
+
let closed = false;
|
|
289
|
+
let conn;
|
|
290
|
+
/** Return the bridge-owned record for an agent, rejecting same-id impostors. */
|
|
291
|
+
const ownedRecord = (agent) => {
|
|
292
|
+
const record = sessions.get(agent.session.id);
|
|
293
|
+
return record?.agent === agent ? record : void 0;
|
|
294
|
+
};
|
|
295
|
+
const assertOpen = () => {
|
|
296
|
+
if (closed) throw internalError("the ACP bridge has been disposed");
|
|
297
|
+
};
|
|
298
|
+
const requireSession = (sessionId) => {
|
|
299
|
+
const record = sessions.get(sessionId);
|
|
300
|
+
if (record === void 0) throw invalidParams(`unknown session: ${sessionId}`);
|
|
301
|
+
return record;
|
|
302
|
+
};
|
|
303
|
+
/** Send a protocol update without letting a disconnected client fail an agent turn. */
|
|
304
|
+
const notify = (notification) => {
|
|
305
|
+
/* v8 ignore next 3 -- only a transport write failure reaches this guard. */
|
|
306
|
+
conn.sessionUpdate(notification).catch((error) => {
|
|
307
|
+
logger.warn(`acp: session/update failed: ${String(error)}`);
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const settlePrompt = (record, reason) => {
|
|
311
|
+
const inflight = record.inflight;
|
|
312
|
+
if (inflight === void 0) return;
|
|
313
|
+
record.inflight = void 0;
|
|
314
|
+
inflight.resolve(reason);
|
|
315
|
+
};
|
|
316
|
+
const rejectFromError = (inflight, reason) => {
|
|
317
|
+
inflight.reject(internalError(`turn failed: ${reason.error.message}`));
|
|
318
|
+
};
|
|
319
|
+
ctx.on("session/event", (session, event) => {
|
|
320
|
+
const record = sessions.get(session.header.id);
|
|
321
|
+
if (record === void 0 || record.agent.session !== session) return;
|
|
322
|
+
try {
|
|
323
|
+
if (event.type === "assistant/message") {
|
|
324
|
+
for (const block of event.data.message.content) if (block.type === "text" && block.text.length > 0) notify({
|
|
325
|
+
sessionId: record.agent.session.id,
|
|
326
|
+
update: {
|
|
327
|
+
sessionUpdate: "agent_message_chunk",
|
|
328
|
+
content: {
|
|
329
|
+
type: "text",
|
|
330
|
+
text: block.text
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
else if (block.type === "image") notify({
|
|
335
|
+
sessionId: record.agent.session.id,
|
|
336
|
+
update: {
|
|
337
|
+
sessionUpdate: "agent_message_chunk",
|
|
338
|
+
content: {
|
|
339
|
+
type: "text",
|
|
340
|
+
text: `[image attachment ${block.attachment.attachmentId}]`
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
} finally {
|
|
346
|
+
const inflight = record.inflight;
|
|
347
|
+
if (inflight !== void 0 && event.type === "turn/end" && inflight.turn === event.data.turn) if (event.data.reason.kind === "error") {
|
|
348
|
+
record.inflight = void 0;
|
|
349
|
+
rejectFromError(inflight, event.data.reason);
|
|
350
|
+
} else inflight.endReason = event.data.reason;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
ctx.on("agent/inbox/claimed", ({ agent, message, turn }) => {
|
|
354
|
+
const inflight = ownedRecord(agent)?.inflight;
|
|
355
|
+
if (inflight !== void 0 && inflight.messageId === message.id) inflight.turn = turn;
|
|
356
|
+
});
|
|
357
|
+
ctx.on("agent/error", ({ agent, turn, error }) => {
|
|
358
|
+
const record = ownedRecord(agent);
|
|
359
|
+
const inflight = record?.inflight;
|
|
360
|
+
if (record === void 0 || inflight === void 0 || inflight.turn === turn) return;
|
|
361
|
+
record.inflight = void 0;
|
|
362
|
+
inflight.reject(internalError(`turn failed: ${errorChain(error)}`));
|
|
363
|
+
});
|
|
364
|
+
ctx.on("approval/request", (request, next) => {
|
|
365
|
+
const record = ownedRecord(request.agent);
|
|
366
|
+
if (record === void 0 || request.callId === void 0) return next();
|
|
367
|
+
return conn.requestPermission({
|
|
368
|
+
sessionId: record.agent.session.id,
|
|
369
|
+
toolCall: { toolCallId: request.callId },
|
|
370
|
+
options: [{
|
|
371
|
+
optionId: "allow-once",
|
|
372
|
+
name: "Allow once",
|
|
373
|
+
kind: "allow_once"
|
|
374
|
+
}, {
|
|
375
|
+
optionId: "reject-once",
|
|
376
|
+
name: "Reject",
|
|
377
|
+
kind: "reject_once"
|
|
378
|
+
}]
|
|
379
|
+
}).then(({ outcome }) => {
|
|
380
|
+
if (outcome.outcome === "cancelled") return "cancelled";
|
|
381
|
+
return outcome.optionId === "allow-once" ? "allowed-once" : "rejected";
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
const makeAgent = (connection) => {
|
|
385
|
+
conn = connection;
|
|
386
|
+
return {
|
|
387
|
+
initialize(_params) {
|
|
388
|
+
return Promise.resolve({
|
|
389
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
390
|
+
agentInfo: {
|
|
391
|
+
name: "deepseek-harness-acp",
|
|
392
|
+
version: "0.0.1"
|
|
393
|
+
},
|
|
394
|
+
agentCapabilities: { promptCapabilities: {
|
|
395
|
+
image: false,
|
|
396
|
+
audio: false,
|
|
397
|
+
embeddedContext: false
|
|
398
|
+
} },
|
|
399
|
+
authMethods: []
|
|
400
|
+
});
|
|
401
|
+
},
|
|
402
|
+
authenticate(_params) {
|
|
403
|
+
return Promise.resolve();
|
|
404
|
+
},
|
|
405
|
+
async newSession(params) {
|
|
406
|
+
assertOpen();
|
|
407
|
+
validateSessionParams(params);
|
|
408
|
+
const sessionId = SessionId(randomUUID());
|
|
409
|
+
const handle = await agents.create({
|
|
410
|
+
sessionId,
|
|
411
|
+
meta: { cwd: params.cwd },
|
|
412
|
+
agentOptions: agentOptions(config)
|
|
413
|
+
});
|
|
414
|
+
/* v8 ignore next 4 -- a real stdio close can race an in-flight create. */
|
|
415
|
+
if (closed) {
|
|
416
|
+
await handle.dispose();
|
|
417
|
+
throw internalError("connection closed during session/new");
|
|
418
|
+
}
|
|
419
|
+
sessions.set(sessionId, {
|
|
420
|
+
agent: handle.agent,
|
|
421
|
+
dispose: () => handle.dispose(),
|
|
422
|
+
inflight: void 0
|
|
423
|
+
});
|
|
424
|
+
return { sessionId };
|
|
425
|
+
},
|
|
426
|
+
async prompt(params) {
|
|
427
|
+
assertOpen();
|
|
428
|
+
const record = requireSession(SessionId(params.sessionId));
|
|
429
|
+
if (record.inflight !== void 0) throw invalidParams("a prompt is already in flight for this session");
|
|
430
|
+
if (promptHasUnsupportedContent(params.prompt)) throw invalidParams("only text and resource_link prompt content is supported");
|
|
431
|
+
const text = acpPromptToText(params.prompt);
|
|
432
|
+
if (text.trim().length === 0) throw invalidParams("empty prompt");
|
|
433
|
+
if (ctx.agents.get(record.agent.id) !== record.agent) throw internalError("prompt was not queued: the agent was disposed outside the bridge");
|
|
434
|
+
const message = createUserMessage({
|
|
435
|
+
content: [{
|
|
436
|
+
type: "text",
|
|
437
|
+
text
|
|
438
|
+
}],
|
|
439
|
+
source: { kind: "user" }
|
|
440
|
+
});
|
|
441
|
+
return { stopReason: await new Promise((resolve, reject) => {
|
|
442
|
+
const inflight = {
|
|
443
|
+
resolve,
|
|
444
|
+
reject,
|
|
445
|
+
messageId: message.id,
|
|
446
|
+
turn: void 0,
|
|
447
|
+
endReason: void 0
|
|
448
|
+
};
|
|
449
|
+
record.inflight = inflight;
|
|
450
|
+
try {
|
|
451
|
+
record.agent.followup(message);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
record.inflight = void 0;
|
|
454
|
+
throw internalError(`prompt was not queued: ${error instanceof Error ? error.message : String(error)}`);
|
|
455
|
+
}
|
|
456
|
+
/* v8 ignore stop */
|
|
457
|
+
record.agent.whenIdle().then(() => {
|
|
458
|
+
if (record.inflight !== inflight) return;
|
|
459
|
+
record.inflight = void 0;
|
|
460
|
+
const end = inflight.endReason;
|
|
461
|
+
if (end === void 0) inflight.resolve("cancelled");
|
|
462
|
+
else inflight.resolve(end.kind === "max-tokens" ? "end_turn" : turnEndToStopReason(end));
|
|
463
|
+
});
|
|
464
|
+
}) };
|
|
465
|
+
},
|
|
466
|
+
cancel(params) {
|
|
467
|
+
const record = sessions.get(SessionId(params.sessionId));
|
|
468
|
+
if (record === void 0) return Promise.resolve();
|
|
469
|
+
record.agent.cancel({ kind: "user" });
|
|
470
|
+
settlePrompt(record, "cancelled");
|
|
471
|
+
return Promise.resolve();
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
conn = new AgentSideConnection(makeAgent, config.stream ?? ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin)));
|
|
476
|
+
let quiescing;
|
|
477
|
+
const quiesce = () => {
|
|
478
|
+
if (quiescing !== void 0) return quiescing;
|
|
479
|
+
closed = true;
|
|
480
|
+
const records = [...sessions.values()];
|
|
481
|
+
sessions.clear();
|
|
482
|
+
for (const record of records) {
|
|
483
|
+
record.agent.cancel({ kind: "user" });
|
|
484
|
+
settlePrompt(record, "cancelled");
|
|
485
|
+
}
|
|
486
|
+
quiescing = (async () => {
|
|
487
|
+
const subagents = ctx.get("subagents");
|
|
488
|
+
if (subagents !== void 0) try {
|
|
489
|
+
await subagents.drainContinuableDescendants(records.map((record) => record.agent));
|
|
490
|
+
} catch (error) {
|
|
491
|
+
logger.warn(`acp: continuable subagent teardown failed: ${String(error)}`);
|
|
492
|
+
}
|
|
493
|
+
const disposals = await Promise.allSettled(records.map((record) => record.dispose()));
|
|
494
|
+
const failures = [];
|
|
495
|
+
for (const result of disposals) if (result.status === "rejected") failures.push(result.reason);
|
|
496
|
+
if (failures.length > 0) {
|
|
497
|
+
const detail = failures.map((failure) => errorChain(failure)).join("; ");
|
|
498
|
+
throw new AggregateError(failures, `ACP agent teardown failed for ${failures.length} session(s): ${detail}`);
|
|
499
|
+
}
|
|
500
|
+
})();
|
|
501
|
+
return quiescing;
|
|
502
|
+
};
|
|
503
|
+
/* v8 ignore start -- production transport rejection and teardown failure. */
|
|
504
|
+
conn.closed.catch((error) => {
|
|
505
|
+
logger.warn(`acp: connection closed with an error: ${String(error)}`);
|
|
506
|
+
}).then(quiesce).catch((error) => {
|
|
507
|
+
logger.warn(`acp: connection-close teardown failed: ${String(error)}`);
|
|
508
|
+
});
|
|
509
|
+
/* v8 ignore stop */
|
|
510
|
+
ctx.effect(() => quiesce, "acp.connection");
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Build per-agent options from plugin config without assigning absent optional fields.
|
|
514
|
+
* @param config - ACP provider/model configuration.
|
|
515
|
+
* @returns the configured fields only.
|
|
516
|
+
*/
|
|
517
|
+
function agentOptions(config) {
|
|
518
|
+
return {
|
|
519
|
+
...config.provider !== void 0 ? { provider: config.provider } : {},
|
|
520
|
+
...config.model !== void 0 ? { model: config.model } : {}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
/** Reject session features outside the automation contract. */
|
|
524
|
+
function validateSessionParams(params) {
|
|
525
|
+
if (!isAbsolute(params.cwd)) throw invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
526
|
+
if (params.additionalDirectories !== void 0 && params.additionalDirectories.length > 0) throw invalidParams("additionalDirectories is not supported");
|
|
527
|
+
if (params.mcpServers.length > 0) throw invalidParams("mcpServers is not supported");
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
export { Config, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@monotykamary/dsh-acp`.
|
|
4
|
+
* @module @monotykamary/dsh-acp/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@monotykamary/dsh-acp";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "acp-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this transport owns no durable package-local event stream;
|
|
13
|
+
* protocol and lifecycle tests cover its mapping.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure translation between the harness lifecycle and the automation-only ACP wire.
|
|
3
|
+
* @module @monotykamary/dsh-acp/codec
|
|
4
|
+
*/
|
|
5
|
+
import type { ContentBlock as AcpContentBlock, StopReason } from '@agentclientprotocol/sdk';
|
|
6
|
+
import type { TurnEndReason } from '@monotykamary/dsh-session';
|
|
7
|
+
/**
|
|
8
|
+
* Map a harness turn ending to ACP's terminal reason vocabulary.
|
|
9
|
+
* @param reason - harness turn outcome.
|
|
10
|
+
* @returns the closest legal ACP stop reason.
|
|
11
|
+
*/
|
|
12
|
+
export declare function turnEndToStopReason(reason: TurnEndReason): StopReason;
|
|
13
|
+
/**
|
|
14
|
+
* Flatten an ACP prompt's baseline blocks to text. Text blocks concatenate
|
|
15
|
+
* verbatim; resource links become explicit textual references so a baseline
|
|
16
|
+
* client can point at files without the bridge silently dropping that context.
|
|
17
|
+
* @param prompt - supported ACP prompt blocks.
|
|
18
|
+
* @returns text in wire order, with resource links rendered as bracketed references.
|
|
19
|
+
*/
|
|
20
|
+
export declare function acpPromptToText(prompt: readonly AcpContentBlock[]): string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a prompt carries content beyond the ACP baseline. The spec requires
|
|
23
|
+
* every agent to accept `text` and `resource_link`; richer inline payloads
|
|
24
|
+
* (image, audio, embedded resource) are optional capabilities this bridge does
|
|
25
|
+
* not advertise, so they are rejected rather than silently dropped.
|
|
26
|
+
* @param prompt - ACP prompt blocks to inspect.
|
|
27
|
+
* @returns `true` when any block is neither `text` nor `resource_link`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function promptHasUnsupportedContent(prompt: readonly AcpContentBlock[]): boolean;
|
|
30
|
+
//# sourceMappingURL=codec.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automation-only Agent Client Protocol server over JSON-RPC stdio.
|
|
3
|
+
*
|
|
4
|
+
* The bridge exposes fresh harness sessions to trusted programmatic clients. It
|
|
5
|
+
* carries prompt text, committed assistant text, cancellation, and one-shot
|
|
6
|
+
* permission decisions; presentation and human-interaction features stay with
|
|
7
|
+
* the harness's UI modules.
|
|
8
|
+
*
|
|
9
|
+
* @module @monotykamary/dsh-acp
|
|
10
|
+
*/
|
|
11
|
+
import type { Context } from '@monotykamary/cordis';
|
|
12
|
+
import Schema from '@monotykamary/schemastery';
|
|
13
|
+
import { type Stream } from '@agentclientprotocol/sdk';
|
|
14
|
+
export declare const name = "acp";
|
|
15
|
+
/** The bridge creates and owns agents; every other concern is carried by the agent composition. */
|
|
16
|
+
export declare const inject: string[];
|
|
17
|
+
/** Plugin config: the provider/model selection used for each ACP-created agent. */
|
|
18
|
+
export interface AcpConfig {
|
|
19
|
+
/** Provider route for created agents. */
|
|
20
|
+
provider?: string;
|
|
21
|
+
/** Model name for created agents. */
|
|
22
|
+
model?: string;
|
|
23
|
+
/** Runtime-only transport override; production uses stdio. */
|
|
24
|
+
stream?: Stream;
|
|
25
|
+
}
|
|
26
|
+
export declare const Config: Schema<AcpConfig>;
|
|
27
|
+
/**
|
|
28
|
+
* Mount the automation-only ACP server.
|
|
29
|
+
* @param ctx - Cordis context carrying the agent factory and session events.
|
|
30
|
+
* @param config - Initial provider/model selection and optional test transport.
|
|
31
|
+
*/
|
|
32
|
+
export declare function apply(ctx: Context, config: AcpConfig): void;
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@monotykamary/dsh-acp`.
|
|
3
|
+
* @module @monotykamary/dsh-acp/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@monotykamary/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "acp-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@monotykamary/dsh-acp",
|
|
3
|
+
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
|
|
4
|
+
"version": "0.1.0-rc.5",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/acp/acp"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@agentclientprotocol/sdk": "0.25.1",
|
|
36
|
+
"@monotykamary/schemastery": "^3.18.1"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@monotykamary/dsh-agent": "^0.1.0-rc.5",
|
|
40
|
+
"@monotykamary/dsh-user-approval": "^0.1.0-rc.5",
|
|
41
|
+
"@monotykamary/dsh-invariants": "^0.1.0-rc.5",
|
|
42
|
+
"@monotykamary/dsh-session": "^0.1.0-rc.5",
|
|
43
|
+
"@monotykamary/cordis": "^4.0.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@monotykamary/dsh-invariants": "^0.1.0-rc.5",
|
|
47
|
+
"@monotykamary/dsh-llm": "^0.1.0-rc.5",
|
|
48
|
+
"@monotykamary/dsh-session": "^0.1.0-rc.5",
|
|
49
|
+
"@monotykamary/dsh-tools": "^0.1.0-rc.5",
|
|
50
|
+
"@monotykamary/dsh-agent": "^0.1.0-rc.5",
|
|
51
|
+
"@monotykamary/cordis": "^4.0.1",
|
|
52
|
+
"@monotykamary/dsh-user-approval": "^0.1.0-rc.5",
|
|
53
|
+
"@monotykamary/dsh-agent-loop-testkit": "^0.1.0-rc.5",
|
|
54
|
+
"@monotykamary/dsh-agent-loop": "^0.1.0-rc.5"
|
|
55
|
+
}
|
|
56
|
+
}
|