@flowy-agent-store/sdk 0.1.0-beta.6 → 0.1.0-beta.8
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 +52 -3
- package/dist/index.cjs +107 -1
- package/dist/index.d.cts +84 -2
- package/dist/index.d.mts +84 -2
- package/dist/index.mjs +106 -3
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -15,7 +15,17 @@ const items = await harness.store.search("software");
|
|
|
15
15
|
// 安装:导入 + 注册,并等到组件真的可用(默认 waitForReady: true)
|
|
16
16
|
const outcome = await harness.store.install(items[0]);
|
|
17
17
|
if (outcome.readyIssue === "authorization_required") {
|
|
18
|
-
|
|
18
|
+
const id = outcome.readyComponentId!;
|
|
19
|
+
// 失败有两条通道,别只看一条:浏览器**之前**的失败由 authStart 同步回
|
|
20
|
+
// state:"error";**之后**的失败(换 token 被拒、回调超时、被限流)只会出现在
|
|
21
|
+
// authStatus 的 error 里,state 一直停在 not_authenticated。
|
|
22
|
+
// waitForAuth 把第二条通道包成一次调用,并在结束时说清原因。
|
|
23
|
+
const started = await harness.connectors.authStart(id);
|
|
24
|
+
if (started.state === "error") throw new Error(started.error ?? "authorization failed to start");
|
|
25
|
+
const auth = await harness.connectors.waitForAuth(id);
|
|
26
|
+
if (auth.state !== "authenticated") {
|
|
27
|
+
throw new Error(auth.state === "error" ? auth.error : "timed out in the browser");
|
|
28
|
+
}
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
// 禁用 / 启用:连接器改 mcp_servers.enabled,专家/专家团改 Preset.enabled
|
|
@@ -29,14 +39,49 @@ if (!released.ok) console.warn(released.components.filter((c) => !c.ok));
|
|
|
29
39
|
await harness.close();
|
|
30
40
|
```
|
|
31
41
|
|
|
42
|
+
## 导出专家 / 专家团并写到目录(`exportAgent` / `exportTeam` / `materializePack`)
|
|
43
|
+
|
|
44
|
+
`agents.export` / `teams.export` 只给**定义**(`ExpertPack`),技能是**引用**(`{name, id}`,没有字节)。
|
|
45
|
+
下面三个函数把「定义 + 技能字节」一次**写成一个目录**(`docs/agent-store/35`;它把 doc `32` §6.5 的
|
|
46
|
+
11 行配方提升成了 API):
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { exportTeam } from "@flowy-agent-store/sdk";
|
|
50
|
+
|
|
51
|
+
// 多 agent 的专家团:递归展开 + 整包失败(成员缺一即失败并点名),leader 在首位
|
|
52
|
+
const result = await exportTeam(harness, teamId, "./frontend-backend-experts");
|
|
53
|
+
// 单 agent 专家用 exportAgent(harness, agentId, dir)
|
|
54
|
+
// 已有 pack(比如缓存/网络传来的)用 materializePack(harness, pack, dir) 只写目录
|
|
55
|
+
|
|
56
|
+
result.pack; // ExpertPack —— 内存里也拿得到,不落盘也能用
|
|
57
|
+
result.writtenSkills; // 实际写入的技能名(去重后)
|
|
58
|
+
result.danglingSkills; // 声明了但本机取不到的技能:{ id, error },如实上报,不静默跳过
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
写出的布局:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
<dir>/
|
|
65
|
+
expert-pack.json # 线上 pack 逐字节
|
|
66
|
+
persona.md # 仅 agent 形态(团没有自己的 persona)
|
|
67
|
+
members/<id>/persona.md # 仅 team 形态,每个成员一个
|
|
68
|
+
skills/<name>/… # 成员声明去重后的技能字节
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
错误语义:wire 导出失败(`agent_not_installed` / `agent_disabled` / `policy_denied` /
|
|
72
|
+
`version_mismatch` / `response_too_large`…)**先于任何写盘**,不留半成品目录;`skill/files`
|
|
73
|
+
失败进 `danglingSkills` 并继续;`skill/file` 在列文件成功后失败则**抛出**——那是宿主 I/O 错误,
|
|
74
|
+
吞掉会造出「列了文件却缺内容」的残目录。
|
|
75
|
+
|
|
32
76
|
**`launchHarness` 解析出的对象就是那个 client**(`docs/agent-store/31` §5 方案 B):`harness.store` / `harness.conversations` / `harness.models` 直接可用,没有 `.client` 一跳;子进程是 `harness.server`(`readiness` / `dataDir` / `exited`),握手响应是 `harness.handshake`。
|
|
33
77
|
|
|
34
78
|
五个动词都在 SDK 里可用,但**它们不是 `@flowy-agent-store/sdk` 自己实现的**:本包只负责进程与传输,返回的对象就是一个 `@flowy-agent-store/client` 的 `AppServerClient`(sdk 另挂 `server` / `handshake` / `close()`),其中 `store` 子客户端把 `install` / `uninstall` / `setEnabled` 编排成一条状态机(`search → install → … → uninstall`)。这样分层是刻意的——见 `docs/agent-store/12-sdk-packaging.md` §2 的职责划分。
|
|
35
79
|
|
|
36
80
|
几条使用上的事实(都是实测结论,不是约定):
|
|
37
81
|
|
|
38
|
-
- `store.install` 默认**等到可用**才 resolve。技能拷完即可用,连接器不然:它注册出来是 **disabled** 的(安装器的既定默认),所以就绪检查会先把它 enable,再跑一次探针——只轮询状态永远到不了 `connected
|
|
82
|
+
- `store.install` 默认**等到可用**才 resolve。技能拷完即可用,连接器不然:它注册出来是 **disabled** 的(安装器的既定默认),所以就绪检查会先把它 enable,再跑一次探针——只轮询状态永远到不了 `connected`,因为状态派生自上一次探针。探针**有节奏**:首轮(以及之后每 `readyProbeMs`,默认 5s)真连一次,其余轮次只读状态。这是刻意的:一次探针会解析已存 token(临近过期就刷新,401 再刷新一次),按 400ms 的轮询间隔去探针,30 秒预算里能对连接器及其授权服务器打出约 10 次连接 / 最多约 20 次 token 请求,而没有任何人点过按钮。
|
|
39
83
|
- 就绪超时**不会丢掉安装结果**:返回成功的安装 + `ready: false` / `readyIssue: "ready_timeout"`。需要 OAuth 的连接器立刻返回 `authorization_required`,不把超时预算烧光。
|
|
84
|
+
- `connectors.waitForAuth(id)` 是「等授权完成」的写法:它轮询 `authStatus`,一拿到 `error` 就立刻带原因返回(`{state:"error", error}`),成功返回 `{state:"authenticated"}`,预算用完返回 `{state:"timeout"}`;只有状态读本身失败才 reject。手写 `while (state !== "authenticated")` 循环的典型结局是「它就是没变成 authenticated」——而原因(换 token 被拒、回调超时、`slow_down` 限流)早就躺在它每次读到的 `error` 里。
|
|
40
85
|
- 没有「更新」动词。`store.checkUpdates()` 只报告哪些条目有新版,`store.updateHint(item)` 告诉你该怎么做——当前是 `"uninstall_reinstall"`:卸载后重装,安装路径会按版本漂移重新导入,取到市场当前版本。
|
|
41
86
|
- 卸载是**可重入**的:产物已不在算成功。部分失败时那些组件保持已安装、`ok: false` 且点名到组件,重试有意义。
|
|
42
87
|
- `store.setEnabled(item, false)` 对技能只翻一个目录层标记,返回 `code: "skill_disable_flag_only"`——技能语料没有启用状态,要让技能离开运行时只能 `uninstall`。这个 code 会原样透出,不会被吞。
|
|
@@ -64,4 +109,8 @@ await harness.close();
|
|
|
64
109
|
- 二进制定位:`bin` 参数 → `AGENT_STORE_BIN` → 平台 runtime 包的 `vendor/` → `PATH`;找不到直接报错(**不下载**,见 P2)。
|
|
65
110
|
- 就绪行 `protocol_version` 与 SDK 不一致时杀掉子进程并报错(含两端版本)。该值是**契约指纹**,不是版本号:任何 wire 变更都会 bump。
|
|
66
111
|
|
|
67
|
-
不在本包范围内(`07-typescript-sdk.md` §非目标):`config/*` 与 `skill/*`
|
|
112
|
+
不在本包范围内(`07-typescript-sdk.md` §非目标):`config/*` 与 `skill/*` **写**面。它们的**类型**在
|
|
113
|
+
`@flowy-agent-store/protocol`(wire 上存在的方法就有形状),但**没有** typed 方法——因为这些面跟着
|
|
114
|
+
宿主自己的设置文件与技能目录走,是最易变的部分。**技能的读面是例外**:`skill/files` / `skill/file`
|
|
115
|
+
有 typed 方法(`harness.skills.*`),且被上面的导出函数消费。这是可发现性边界,不是权限边界:
|
|
116
|
+
`harness.transport` 是公开的,服务端按磁盘归属判定可写性。要用就直连 `transport.request`,并自担变动。
|
package/dist/index.cjs
CHANGED
|
@@ -3,8 +3,8 @@ let _flowy_agent_store_client = require("@flowy-agent-store/client");
|
|
|
3
3
|
let node_fs = require("node:fs");
|
|
4
4
|
let node_module = require("node:module");
|
|
5
5
|
let node_path = require("node:path");
|
|
6
|
-
let node_child_process = require("node:child_process");
|
|
7
6
|
let node_fs_promises = require("node:fs/promises");
|
|
7
|
+
let node_child_process = require("node:child_process");
|
|
8
8
|
let node_os = require("node:os");
|
|
9
9
|
let node_readline = require("node:readline");
|
|
10
10
|
let _flowy_agent_store_protocol = require("@flowy-agent-store/protocol");
|
|
@@ -66,6 +66,109 @@ function findOnPath(name) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
//#endregion
|
|
69
|
+
//#region src/export.ts
|
|
70
|
+
/**
|
|
71
|
+
* Export an expert (single agent) or an expert **team** together with its
|
|
72
|
+
* skills, written out as a directory (doc `35`).
|
|
73
|
+
*
|
|
74
|
+
* The wire face (`agent/export` / `team/export`, docs/agent-store/32) carries
|
|
75
|
+
* skills **by reference** — `{name, id}`, never bytes — so "export the team's
|
|
76
|
+
* skills" means: walk the pack, fetch every referenced skill through
|
|
77
|
+
* `skill/files` + `skill/file`, and write the whole thing to a directory the
|
|
78
|
+
* *caller* owns. Doc `32` §6.5 originally shipped this as an 11-line recipe in
|
|
79
|
+
* prose; doc `35` promotes it into these three functions (and records why the
|
|
80
|
+
* earlier decision against an SDK helper was reversed).
|
|
81
|
+
*
|
|
82
|
+
* Everything here runs in the caller's process. The host never writes a byte:
|
|
83
|
+
* there is no server-side directory-writing endpoint by design (doc `32` §2
|
|
84
|
+
* item 7), which is why this file is allowed to import `node:fs` — the same
|
|
85
|
+
* privilege `spawn.ts` already has — while `@flowy-agent-store/client` must
|
|
86
|
+
* never gain it.
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* Write an in-hand pack to `dir` as a real directory (doc `35` §5 layout).
|
|
90
|
+
* "Materialize" here means exactly that: the in-memory pack object plus the
|
|
91
|
+
* skill bytes it references become files on disk; the pack data itself is
|
|
92
|
+
* unchanged.
|
|
93
|
+
*
|
|
94
|
+
* ```text
|
|
95
|
+
* <dir>/expert-pack.json # the wire pack, byte-for-byte (JSON.stringify(_, 2))
|
|
96
|
+
* <dir>/persona.md # kind === "agent" only: pack.persona.instructions
|
|
97
|
+
* <dir>/members/<id>/persona.md # kind === "team" only, one per member
|
|
98
|
+
* <dir>/skills/<name>/… # every referenced skill's files, deduplicated by id
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* Error semantics (doc `35` §6): the pack is fetched **before** anything is
|
|
102
|
+
* written, so a failed wire export leaves no directory behind; a `skill/files`
|
|
103
|
+
* rejection lands in {@link ExportResult.danglingSkills} and the loop
|
|
104
|
+
* continues; a `skill/file` rejection after a successful listing **throws** —
|
|
105
|
+
* that is a host I/O failure, and swallowing it would produce a directory that
|
|
106
|
+
* lists files it does not contain. No rollback on mid-write failure.
|
|
107
|
+
*/
|
|
108
|
+
async function materializePack(deps, pack, dir) {
|
|
109
|
+
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
110
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "expert-pack.json"), JSON.stringify(pack, null, 2));
|
|
111
|
+
if (pack.kind === "agent") await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "persona.md"), pack.persona.instructions);
|
|
112
|
+
for (const member of pack.team?.members ?? []) {
|
|
113
|
+
const target = (0, node_path.join)(dir, "members", member.id, "persona.md");
|
|
114
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(target), { recursive: true });
|
|
115
|
+
await (0, node_fs_promises.writeFile)(target, member.persona.instructions);
|
|
116
|
+
}
|
|
117
|
+
const refs = [...pack.skills, ...(pack.team?.members ?? []).flatMap((m) => m.skills)];
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
const writtenSkills = [];
|
|
120
|
+
const danglingSkills = [];
|
|
121
|
+
for (const ref of refs) {
|
|
122
|
+
if (seen.has(ref.id)) continue;
|
|
123
|
+
seen.add(ref.id);
|
|
124
|
+
let files;
|
|
125
|
+
try {
|
|
126
|
+
files = (await deps.skills.files(ref.id)).files;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
danglingSkills.push({
|
|
129
|
+
id: ref.id,
|
|
130
|
+
error: String(error)
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
const target = (0, node_path.join)(dir, "skills", ref.name, file.path);
|
|
136
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(target), { recursive: true });
|
|
137
|
+
await (0, node_fs_promises.writeFile)(target, await deps.skills.readFile(ref.id, file.path));
|
|
138
|
+
}
|
|
139
|
+
writtenSkills.push(ref.name);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
pack,
|
|
143
|
+
dir,
|
|
144
|
+
writtenSkills,
|
|
145
|
+
danglingSkills
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Export one single-agent expert and write it out as a directory.
|
|
150
|
+
*
|
|
151
|
+
* Same preconditions as the wire method: the expert must be installed and
|
|
152
|
+
* enabled, or the host answers `agent_not_installed` / `agent_disabled` /
|
|
153
|
+
* `preset_disabled`; a host whose `[expert_export]` table refuses the id
|
|
154
|
+
* answers `policy_denied` (the capability bit only reports the seam). Check
|
|
155
|
+
* `capabilities.expert_export` for the seam, not for this id.
|
|
156
|
+
*/
|
|
157
|
+
async function exportAgent(deps, agentId, dir) {
|
|
158
|
+
return materializePack(deps, await deps.agents.export(agentId), dir);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Export one expert team and write it out as a directory, leader first.
|
|
162
|
+
*
|
|
163
|
+
* All-or-nothing stays on the server: if any member is uninstalled, disabled,
|
|
164
|
+
* or refused by policy, the whole call fails and nothing is written. A pack
|
|
165
|
+
* over 1 MiB fails with `response_too_large` rather than being truncated.
|
|
166
|
+
* `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
|
|
167
|
+
*/
|
|
168
|
+
async function exportTeam(deps, teamId, dir, teamVersion) {
|
|
169
|
+
return materializePack(deps, await deps.teams.export(teamId, teamVersion), dir);
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
69
172
|
//#region src/readiness.ts
|
|
70
173
|
/** Parse one stdout line; `null` when it is not the readiness line. */
|
|
71
174
|
function parseReadinessLine(line) {
|
|
@@ -311,7 +414,10 @@ async function launchHarness(options) {
|
|
|
311
414
|
}
|
|
312
415
|
//#endregion
|
|
313
416
|
exports.assertProtocolCompatible = assertProtocolCompatible;
|
|
417
|
+
exports.exportAgent = exportAgent;
|
|
418
|
+
exports.exportTeam = exportTeam;
|
|
314
419
|
exports.launchHarness = launchHarness;
|
|
420
|
+
exports.materializePack = materializePack;
|
|
315
421
|
exports.parseReadinessLine = parseReadinessLine;
|
|
316
422
|
exports.resolveAppServerBin = resolveAppServerBin;
|
|
317
423
|
exports.spawnAppServer = spawnAppServer;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,8 +1,90 @@
|
|
|
1
1
|
import { AppServerClient } from "@flowy-agent-store/client";
|
|
2
|
-
import { ClientCapabilities, ClientInfo, InitializeResult } from "@flowy-agent-store/protocol";
|
|
2
|
+
import { ClientCapabilities, ClientInfo, ExpertPack, InitializeResult } from "@flowy-agent-store/protocol";
|
|
3
3
|
//#region src/bin.d.ts
|
|
4
4
|
export declare function resolveAppServerBin(explicit?: string): string;
|
|
5
5
|
//#endregion
|
|
6
|
+
//#region src/export.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* The three wire faces this module needs, narrowed from {@link Harness} on
|
|
9
|
+
* purpose: unit tests hand a fake, and the dependency surface stays explicit
|
|
10
|
+
* instead of quietly reaching `store` / `conversations`. A real harness
|
|
11
|
+
* satisfies this structurally (`Harness` extends `AppServerClient`, which owns
|
|
12
|
+
* `agents` / `teams` / `skills`).
|
|
13
|
+
*/
|
|
14
|
+
interface ExportDeps {
|
|
15
|
+
agents: {
|
|
16
|
+
export(agentId: string): Promise<ExpertPack>;
|
|
17
|
+
};
|
|
18
|
+
teams: {
|
|
19
|
+
export(teamId: string, teamVersion?: string): Promise<ExpertPack>;
|
|
20
|
+
};
|
|
21
|
+
skills: {
|
|
22
|
+
files(skillId: string): Promise<{
|
|
23
|
+
files: {
|
|
24
|
+
path: string;
|
|
25
|
+
}[];
|
|
26
|
+
}>;
|
|
27
|
+
readFile(skillId: string, path: string): Promise<Uint8Array>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
interface ExportResult {
|
|
31
|
+
/** The pack is also returned in memory: a consumer that never touches the disk needs no second call. */
|
|
32
|
+
pack: ExpertPack;
|
|
33
|
+
dir: string;
|
|
34
|
+
/** Skill names actually written (deduplicated). */
|
|
35
|
+
writtenSkills: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Skills the pack **declares** but this host cannot resolve (`skill/files`
|
|
38
|
+
* rejected them). Reported honestly, never skipped in silence: the pack
|
|
39
|
+
* states declarations, resolvability is a host fact the consumer has to
|
|
40
|
+
* judge (doc `32` §5 R8, live criterion EX-008).
|
|
41
|
+
*/
|
|
42
|
+
danglingSkills: {
|
|
43
|
+
id: string;
|
|
44
|
+
error: string;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Write an in-hand pack to `dir` as a real directory (doc `35` §5 layout).
|
|
49
|
+
* "Materialize" here means exactly that: the in-memory pack object plus the
|
|
50
|
+
* skill bytes it references become files on disk; the pack data itself is
|
|
51
|
+
* unchanged.
|
|
52
|
+
*
|
|
53
|
+
* ```text
|
|
54
|
+
* <dir>/expert-pack.json # the wire pack, byte-for-byte (JSON.stringify(_, 2))
|
|
55
|
+
* <dir>/persona.md # kind === "agent" only: pack.persona.instructions
|
|
56
|
+
* <dir>/members/<id>/persona.md # kind === "team" only, one per member
|
|
57
|
+
* <dir>/skills/<name>/… # every referenced skill's files, deduplicated by id
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* Error semantics (doc `35` §6): the pack is fetched **before** anything is
|
|
61
|
+
* written, so a failed wire export leaves no directory behind; a `skill/files`
|
|
62
|
+
* rejection lands in {@link ExportResult.danglingSkills} and the loop
|
|
63
|
+
* continues; a `skill/file` rejection after a successful listing **throws** —
|
|
64
|
+
* that is a host I/O failure, and swallowing it would produce a directory that
|
|
65
|
+
* lists files it does not contain. No rollback on mid-write failure.
|
|
66
|
+
*/
|
|
67
|
+
export declare function materializePack(deps: ExportDeps, pack: ExpertPack, dir: string): Promise<ExportResult>;
|
|
68
|
+
/**
|
|
69
|
+
* Export one single-agent expert and write it out as a directory.
|
|
70
|
+
*
|
|
71
|
+
* Same preconditions as the wire method: the expert must be installed and
|
|
72
|
+
* enabled, or the host answers `agent_not_installed` / `agent_disabled` /
|
|
73
|
+
* `preset_disabled`; a host whose `[expert_export]` table refuses the id
|
|
74
|
+
* answers `policy_denied` (the capability bit only reports the seam). Check
|
|
75
|
+
* `capabilities.expert_export` for the seam, not for this id.
|
|
76
|
+
*/
|
|
77
|
+
export declare function exportAgent(deps: ExportDeps, agentId: string, dir: string): Promise<ExportResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Export one expert team and write it out as a directory, leader first.
|
|
80
|
+
*
|
|
81
|
+
* All-or-nothing stays on the server: if any member is uninstalled, disabled,
|
|
82
|
+
* or refused by policy, the whole call fails and nothing is written. A pack
|
|
83
|
+
* over 1 MiB fails with `response_too_large` rather than being truncated.
|
|
84
|
+
* `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function exportTeam(deps: ExportDeps, teamId: string, dir: string, teamVersion?: string): Promise<ExportResult>;
|
|
87
|
+
//#endregion
|
|
6
88
|
//#region src/readiness.d.ts
|
|
7
89
|
/**
|
|
8
90
|
* Readiness protocol (docs/agent-store/12 P0-1).
|
|
@@ -114,4 +196,4 @@ export interface Harness extends AppServerClient {
|
|
|
114
196
|
*/
|
|
115
197
|
export declare function launchHarness(options: HarnessOptions): Promise<Harness>;
|
|
116
198
|
//#endregion
|
|
117
|
-
export type { ReadinessInfo, SpawnExitInfo, SpawnOptions, SpawnedServer };
|
|
199
|
+
export type { ExportDeps, ExportResult, ReadinessInfo, SpawnExitInfo, SpawnOptions, SpawnedServer };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,90 @@
|
|
|
1
1
|
import { AppServerClient } from "@flowy-agent-store/client";
|
|
2
|
-
import { ClientCapabilities, ClientInfo, InitializeResult } from "@flowy-agent-store/protocol";
|
|
2
|
+
import { ClientCapabilities, ClientInfo, ExpertPack, InitializeResult } from "@flowy-agent-store/protocol";
|
|
3
3
|
//#region src/bin.d.ts
|
|
4
4
|
export declare function resolveAppServerBin(explicit?: string): string;
|
|
5
5
|
//#endregion
|
|
6
|
+
//#region src/export.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* The three wire faces this module needs, narrowed from {@link Harness} on
|
|
9
|
+
* purpose: unit tests hand a fake, and the dependency surface stays explicit
|
|
10
|
+
* instead of quietly reaching `store` / `conversations`. A real harness
|
|
11
|
+
* satisfies this structurally (`Harness` extends `AppServerClient`, which owns
|
|
12
|
+
* `agents` / `teams` / `skills`).
|
|
13
|
+
*/
|
|
14
|
+
interface ExportDeps {
|
|
15
|
+
agents: {
|
|
16
|
+
export(agentId: string): Promise<ExpertPack>;
|
|
17
|
+
};
|
|
18
|
+
teams: {
|
|
19
|
+
export(teamId: string, teamVersion?: string): Promise<ExpertPack>;
|
|
20
|
+
};
|
|
21
|
+
skills: {
|
|
22
|
+
files(skillId: string): Promise<{
|
|
23
|
+
files: {
|
|
24
|
+
path: string;
|
|
25
|
+
}[];
|
|
26
|
+
}>;
|
|
27
|
+
readFile(skillId: string, path: string): Promise<Uint8Array>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
interface ExportResult {
|
|
31
|
+
/** The pack is also returned in memory: a consumer that never touches the disk needs no second call. */
|
|
32
|
+
pack: ExpertPack;
|
|
33
|
+
dir: string;
|
|
34
|
+
/** Skill names actually written (deduplicated). */
|
|
35
|
+
writtenSkills: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Skills the pack **declares** but this host cannot resolve (`skill/files`
|
|
38
|
+
* rejected them). Reported honestly, never skipped in silence: the pack
|
|
39
|
+
* states declarations, resolvability is a host fact the consumer has to
|
|
40
|
+
* judge (doc `32` §5 R8, live criterion EX-008).
|
|
41
|
+
*/
|
|
42
|
+
danglingSkills: {
|
|
43
|
+
id: string;
|
|
44
|
+
error: string;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Write an in-hand pack to `dir` as a real directory (doc `35` §5 layout).
|
|
49
|
+
* "Materialize" here means exactly that: the in-memory pack object plus the
|
|
50
|
+
* skill bytes it references become files on disk; the pack data itself is
|
|
51
|
+
* unchanged.
|
|
52
|
+
*
|
|
53
|
+
* ```text
|
|
54
|
+
* <dir>/expert-pack.json # the wire pack, byte-for-byte (JSON.stringify(_, 2))
|
|
55
|
+
* <dir>/persona.md # kind === "agent" only: pack.persona.instructions
|
|
56
|
+
* <dir>/members/<id>/persona.md # kind === "team" only, one per member
|
|
57
|
+
* <dir>/skills/<name>/… # every referenced skill's files, deduplicated by id
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* Error semantics (doc `35` §6): the pack is fetched **before** anything is
|
|
61
|
+
* written, so a failed wire export leaves no directory behind; a `skill/files`
|
|
62
|
+
* rejection lands in {@link ExportResult.danglingSkills} and the loop
|
|
63
|
+
* continues; a `skill/file` rejection after a successful listing **throws** —
|
|
64
|
+
* that is a host I/O failure, and swallowing it would produce a directory that
|
|
65
|
+
* lists files it does not contain. No rollback on mid-write failure.
|
|
66
|
+
*/
|
|
67
|
+
export declare function materializePack(deps: ExportDeps, pack: ExpertPack, dir: string): Promise<ExportResult>;
|
|
68
|
+
/**
|
|
69
|
+
* Export one single-agent expert and write it out as a directory.
|
|
70
|
+
*
|
|
71
|
+
* Same preconditions as the wire method: the expert must be installed and
|
|
72
|
+
* enabled, or the host answers `agent_not_installed` / `agent_disabled` /
|
|
73
|
+
* `preset_disabled`; a host whose `[expert_export]` table refuses the id
|
|
74
|
+
* answers `policy_denied` (the capability bit only reports the seam). Check
|
|
75
|
+
* `capabilities.expert_export` for the seam, not for this id.
|
|
76
|
+
*/
|
|
77
|
+
export declare function exportAgent(deps: ExportDeps, agentId: string, dir: string): Promise<ExportResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Export one expert team and write it out as a directory, leader first.
|
|
80
|
+
*
|
|
81
|
+
* All-or-nothing stays on the server: if any member is uninstalled, disabled,
|
|
82
|
+
* or refused by policy, the whole call fails and nothing is written. A pack
|
|
83
|
+
* over 1 MiB fails with `response_too_large` rather than being truncated.
|
|
84
|
+
* `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function exportTeam(deps: ExportDeps, teamId: string, dir: string, teamVersion?: string): Promise<ExportResult>;
|
|
87
|
+
//#endregion
|
|
6
88
|
//#region src/readiness.d.ts
|
|
7
89
|
/**
|
|
8
90
|
* Readiness protocol (docs/agent-store/12 P0-1).
|
|
@@ -114,4 +196,4 @@ export interface Harness extends AppServerClient {
|
|
|
114
196
|
*/
|
|
115
197
|
export declare function launchHarness(options: HarnessOptions): Promise<Harness>;
|
|
116
198
|
//#endregion
|
|
117
|
-
export type { ReadinessInfo, SpawnExitInfo, SpawnOptions, SpawnedServer };
|
|
199
|
+
export type { ExportDeps, ExportResult, ReadinessInfo, SpawnExitInfo, SpawnOptions, SpawnedServer };
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { AppServerClient, WebSocketTransport, isLoopbackUrl } from "@flowy-agent-store/client";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { delimiter, join } from "node:path";
|
|
4
|
+
import { delimiter, dirname, join } from "node:path";
|
|
5
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
5
6
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { APP_SERVER_PROTOCOL_VERSION } from "@flowy-agent-store/protocol";
|
|
@@ -65,6 +65,109 @@ function findOnPath(name) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
//#endregion
|
|
68
|
+
//#region src/export.ts
|
|
69
|
+
/**
|
|
70
|
+
* Export an expert (single agent) or an expert **team** together with its
|
|
71
|
+
* skills, written out as a directory (doc `35`).
|
|
72
|
+
*
|
|
73
|
+
* The wire face (`agent/export` / `team/export`, docs/agent-store/32) carries
|
|
74
|
+
* skills **by reference** — `{name, id}`, never bytes — so "export the team's
|
|
75
|
+
* skills" means: walk the pack, fetch every referenced skill through
|
|
76
|
+
* `skill/files` + `skill/file`, and write the whole thing to a directory the
|
|
77
|
+
* *caller* owns. Doc `32` §6.5 originally shipped this as an 11-line recipe in
|
|
78
|
+
* prose; doc `35` promotes it into these three functions (and records why the
|
|
79
|
+
* earlier decision against an SDK helper was reversed).
|
|
80
|
+
*
|
|
81
|
+
* Everything here runs in the caller's process. The host never writes a byte:
|
|
82
|
+
* there is no server-side directory-writing endpoint by design (doc `32` §2
|
|
83
|
+
* item 7), which is why this file is allowed to import `node:fs` — the same
|
|
84
|
+
* privilege `spawn.ts` already has — while `@flowy-agent-store/client` must
|
|
85
|
+
* never gain it.
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* Write an in-hand pack to `dir` as a real directory (doc `35` §5 layout).
|
|
89
|
+
* "Materialize" here means exactly that: the in-memory pack object plus the
|
|
90
|
+
* skill bytes it references become files on disk; the pack data itself is
|
|
91
|
+
* unchanged.
|
|
92
|
+
*
|
|
93
|
+
* ```text
|
|
94
|
+
* <dir>/expert-pack.json # the wire pack, byte-for-byte (JSON.stringify(_, 2))
|
|
95
|
+
* <dir>/persona.md # kind === "agent" only: pack.persona.instructions
|
|
96
|
+
* <dir>/members/<id>/persona.md # kind === "team" only, one per member
|
|
97
|
+
* <dir>/skills/<name>/… # every referenced skill's files, deduplicated by id
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* Error semantics (doc `35` §6): the pack is fetched **before** anything is
|
|
101
|
+
* written, so a failed wire export leaves no directory behind; a `skill/files`
|
|
102
|
+
* rejection lands in {@link ExportResult.danglingSkills} and the loop
|
|
103
|
+
* continues; a `skill/file` rejection after a successful listing **throws** —
|
|
104
|
+
* that is a host I/O failure, and swallowing it would produce a directory that
|
|
105
|
+
* lists files it does not contain. No rollback on mid-write failure.
|
|
106
|
+
*/
|
|
107
|
+
async function materializePack(deps, pack, dir) {
|
|
108
|
+
await mkdir(dir, { recursive: true });
|
|
109
|
+
await writeFile(join(dir, "expert-pack.json"), JSON.stringify(pack, null, 2));
|
|
110
|
+
if (pack.kind === "agent") await writeFile(join(dir, "persona.md"), pack.persona.instructions);
|
|
111
|
+
for (const member of pack.team?.members ?? []) {
|
|
112
|
+
const target = join(dir, "members", member.id, "persona.md");
|
|
113
|
+
await mkdir(dirname(target), { recursive: true });
|
|
114
|
+
await writeFile(target, member.persona.instructions);
|
|
115
|
+
}
|
|
116
|
+
const refs = [...pack.skills, ...(pack.team?.members ?? []).flatMap((m) => m.skills)];
|
|
117
|
+
const seen = /* @__PURE__ */ new Set();
|
|
118
|
+
const writtenSkills = [];
|
|
119
|
+
const danglingSkills = [];
|
|
120
|
+
for (const ref of refs) {
|
|
121
|
+
if (seen.has(ref.id)) continue;
|
|
122
|
+
seen.add(ref.id);
|
|
123
|
+
let files;
|
|
124
|
+
try {
|
|
125
|
+
files = (await deps.skills.files(ref.id)).files;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
danglingSkills.push({
|
|
128
|
+
id: ref.id,
|
|
129
|
+
error: String(error)
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
const target = join(dir, "skills", ref.name, file.path);
|
|
135
|
+
await mkdir(dirname(target), { recursive: true });
|
|
136
|
+
await writeFile(target, await deps.skills.readFile(ref.id, file.path));
|
|
137
|
+
}
|
|
138
|
+
writtenSkills.push(ref.name);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
pack,
|
|
142
|
+
dir,
|
|
143
|
+
writtenSkills,
|
|
144
|
+
danglingSkills
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Export one single-agent expert and write it out as a directory.
|
|
149
|
+
*
|
|
150
|
+
* Same preconditions as the wire method: the expert must be installed and
|
|
151
|
+
* enabled, or the host answers `agent_not_installed` / `agent_disabled` /
|
|
152
|
+
* `preset_disabled`; a host whose `[expert_export]` table refuses the id
|
|
153
|
+
* answers `policy_denied` (the capability bit only reports the seam). Check
|
|
154
|
+
* `capabilities.expert_export` for the seam, not for this id.
|
|
155
|
+
*/
|
|
156
|
+
async function exportAgent(deps, agentId, dir) {
|
|
157
|
+
return materializePack(deps, await deps.agents.export(agentId), dir);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Export one expert team and write it out as a directory, leader first.
|
|
161
|
+
*
|
|
162
|
+
* All-or-nothing stays on the server: if any member is uninstalled, disabled,
|
|
163
|
+
* or refused by policy, the whole call fails and nothing is written. A pack
|
|
164
|
+
* over 1 MiB fails with `response_too_large` rather than being truncated.
|
|
165
|
+
* `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
|
|
166
|
+
*/
|
|
167
|
+
async function exportTeam(deps, teamId, dir, teamVersion) {
|
|
168
|
+
return materializePack(deps, await deps.teams.export(teamId, teamVersion), dir);
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
68
171
|
//#region src/readiness.ts
|
|
69
172
|
/** Parse one stdout line; `null` when it is not the readiness line. */
|
|
70
173
|
function parseReadinessLine(line) {
|
|
@@ -309,4 +412,4 @@ async function launchHarness(options) {
|
|
|
309
412
|
}
|
|
310
413
|
}
|
|
311
414
|
//#endregion
|
|
312
|
-
export { assertProtocolCompatible, launchHarness, parseReadinessLine, resolveAppServerBin, spawnAppServer };
|
|
415
|
+
export { assertProtocolCompatible, exportAgent, exportTeam, launchHarness, materializePack, parseReadinessLine, resolveAppServerBin, spawnAppServer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowy-agent-store/sdk",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.8",
|
|
4
4
|
"description": "Node.js host for the Agent Store App Server: spawn the runtime binary and connect over loopback (docs/agent-store/12 P1-3)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@flowy-agent-store/client": "0.1.0-beta.
|
|
29
|
-
"@flowy-agent-store/protocol": "0.1.0-beta.
|
|
28
|
+
"@flowy-agent-store/client": "0.1.0-beta.8",
|
|
29
|
+
"@flowy-agent-store/protocol": "0.1.0-beta.8"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsdown",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"module": "./dist/index.mjs",
|
|
41
41
|
"types": "./dist/index.d.mts",
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"@flowy-agent-store/runtime-win32-x64": "0.1.0-beta.
|
|
44
|
-
"@flowy-agent-store/runtime-linux-x64": "0.1.0-beta.
|
|
45
|
-
"@flowy-agent-store/runtime-linux-arm64": "0.1.0-beta.
|
|
46
|
-
"@flowy-agent-store/runtime-darwin-x64": "0.1.0-beta.
|
|
47
|
-
"@flowy-agent-store/runtime-darwin-arm64": "0.1.0-beta.
|
|
43
|
+
"@flowy-agent-store/runtime-win32-x64": "0.1.0-beta.8",
|
|
44
|
+
"@flowy-agent-store/runtime-linux-x64": "0.1.0-beta.8",
|
|
45
|
+
"@flowy-agent-store/runtime-linux-arm64": "0.1.0-beta.8",
|
|
46
|
+
"@flowy-agent-store/runtime-darwin-x64": "0.1.0-beta.8",
|
|
47
|
+
"@flowy-agent-store/runtime-darwin-arm64": "0.1.0-beta.8"
|
|
48
48
|
}
|
|
49
49
|
}
|