@flowy-agent-store/protocol 0.1.0-beta.10

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/dist/index.mjs ADDED
@@ -0,0 +1,481 @@
1
+ //#region src/protocol.ts
2
+ /**
3
+ * Agent Store App Server Protocol types.
4
+ *
5
+ * The chat surface uses only opaque public conversation/message IDs. Internal
6
+ * execution/session/step/attempt IDs never appear in this module.
7
+ */
8
+ /**
9
+ * A contract **fingerprint**, not a version number: it must differ from the
10
+ * previous value on any wire change at all, additive included. The shape is
11
+ * `fp-<n>` — a plain counter, so a bump just increments it and no value is ever
12
+ * reused by accident.
13
+ *
14
+ * So it is a **label, not a version**: neither a release number nor a date.
15
+ * Until `fp-1` the values were date stamps (kept below as history), and those
16
+ * dates were **not** the day of the change: consecutive changes advanced the
17
+ * stamp a day each, so they ran ahead of the calendar. The list below is keyed
18
+ * by *value* — read it as "which wire change is this?", never as "when did this
19
+ * ship".
20
+ *
21
+ * `2026-09-16` carried `StoreItem.published_at`; `2026-09-17` added the two MCP
22
+ * declaration write methods (`config/set-mcp`, `config/set-mcp-enabled`);
23
+ * `2026-09-18` adds `config/get-mcp`, the file editor's read of the same file;
24
+ * `2026-09-19` adds the `conversation/list-changed` notification; `2026-09-20`
25
+ * adds the Skill **file tree** read face (`skill/files`, `skill/file`) so a
26
+ * Skill's companion files are readable, not just its bounded manifest summary;
27
+ * `2026-09-21` adds the connector **call proxy** (`connector/call`), so a third
28
+ * party can run an installed MCP tool while the connection and its credentials
29
+ * stay on the host. **`fp-1` changes the shape only** (date stamp → counter): a
30
+ * `2026-…` value invites being read as a release date, and no wire behaviour
31
+ * changed with the rename. **`fp-2` carries the tools' parameters**
32
+ * (`ConnectorTool.input_schema`, plus `tools_truncated` on `ConnectorDetail`
33
+ * and `ConnectorProbeResult`): a caller that must *name* a tool to be granted it
34
+ * should be able to read what that tool takes. It is the counterpart of the
35
+ * host's `[connector_proxy]` grant moving from one tool at a time to the
36
+ * connector (doc `26`). **`fp-3` makes a Skill selectable per turn**:
37
+ * `conversation/send` gains an optional `mentions` list whose only honoured kind
38
+ * is `skill`, so one turn can mount a Skill's instructions without rewriting the
39
+ * conversation's create-time snapshot (doc `27` 阶段 1). **`fp-4` lets a
40
+ * conversation be created as an installed expert**: `conversation/create` gains
41
+ * an optional `agent_id`, whose Definition supplies the chat's preset identity
42
+ * plus its own Skill and Connector fences — frozen at creation, since none of
43
+ * those keys is mutable afterwards (doc `27` 阶段 2a). **`fp-5` opens a Team's
44
+ * Leader the same way**: an optional `team_id` runs the `team/run` orchestration
45
+ * (members, template, fences) but stops before the goal turn, so the client
46
+ * speaks first; `agent_id` and `team_id` are mutually exclusive (阶段 2b).
47
+ * **`fp-6` makes the model and the reasoning level selectable per call**:
48
+ * `conversation/send` and `agent/run` each gain an optional `model` and
49
+ * `reasoning_effort`. The scope is deliberately the **conversation** (send) and
50
+ * the **run** (agent/run), not "just this turn": the Nomi runtime is built from
51
+ * the persisted row, so a send-carried value is a sticky switch that takes
52
+ * effect on that very turn (doc `29` §4). `ConversationView` also gains
53
+ * `reasoning_effort`, because a setting that can be written by three methods but
54
+ * read back by none is not a setting a client can honour (doc `29` §5.5).
55
+ * **`fp-7` adds the `zip` marketplace source kind**: `market/add` accepts
56
+ * `source_kind: "zip"`, one archive whose root *is* the market root. The
57
+ * official bundles move to it because the old `url` form made a first fetch
58
+ * mirror 14,714 files (611 MiB) for `experts` alone (doc `30`).
59
+ * **`fp-8` adds expert definition export**: `agent/export` and `team/export`
60
+ * return an {@link ExpertPack} — the persona, model hints, skill references and
61
+ * (for a team) the roster with every member expanded. Nothing existing changes
62
+ * shape, but the response carries the Agent Markdown body, which the catalog
63
+ * faces deliberately never do; both methods are WebSocket-only, so the
64
+ * documented route split becomes `48 / 73` (doc `32`).
65
+ * **`fp-13` adds the market policy face** (doc `37`): `market/settings` reads the
66
+ * host's effective sweep cadence, auto-upgrade kind whitelist and last-sweep
67
+ * reading, and `market/settings-set` writes the named keys through to the host's
68
+ * `config.toml`. The same change makes the background sweep able to upgrade
69
+ * entries installed from an `auto_update` market (official sources only, and
70
+ * never a `connector` or a component the user switched off by hand). Both
71
+ * methods are mapped, so the documented route split becomes `55 / 80`.
72
+ * **`fp-14` gives the expert pack its plugin-level assets** (doc `38`):
73
+ * `import/assets` lists the `avatars/`, `bin/` and `scripts/` files of an
74
+ * immutable snapshot (sorted, sha256-digested) and `import/asset` serves one of
75
+ * them base64; `ExpertPack` gains an `assets` list and a declared `avatar` path
76
+ * (`pack_format 2`), so an exported team can materialize a directory the
77
+ * package's own scripts recognize as the plugin root. Both methods are
78
+ * WebSocket-only, so the documented route split becomes `55 / 82`.
79
+ */
80
+ const APP_SERVER_PROTOCOL_VERSION = "fp-14";
81
+ /**
82
+ * Format version of {@link ExpertPack}.
83
+ *
84
+ * **Independent of {@link APP_SERVER_PROTOCOL_VERSION}** on purpose: the
85
+ * fingerprint versions *our* wire compatibility, this versions the **artifact
86
+ * contract for third-party runtimes**. Gate on this one; the fingerprint is
87
+ * about whether your client can talk to the host at all.
88
+ */
89
+ const APP_SERVER_EXPERT_PACK_FORMAT = 2;
90
+ //#endregion
91
+ //#region src/errors.ts
92
+ var AppServerError = class extends Error {
93
+ code;
94
+ requestId;
95
+ retryable;
96
+ details;
97
+ constructor(wire) {
98
+ super(wire.message);
99
+ this.name = "AppServerError";
100
+ this.code = wire.code;
101
+ this.requestId = wire.request_id;
102
+ this.retryable = wire.retryable;
103
+ this.details = wire.details ?? {};
104
+ }
105
+ };
106
+ var TransportError = class extends Error {
107
+ phase;
108
+ retryable;
109
+ cause;
110
+ constructor(phase, message, options) {
111
+ super(message);
112
+ this.name = "TransportError";
113
+ this.phase = phase;
114
+ this.retryable = options?.retryable ?? false;
115
+ this.cause = options?.cause;
116
+ }
117
+ };
118
+ var ProtocolError = class extends Error {
119
+ kind;
120
+ constructor(kind, message) {
121
+ super(message);
122
+ this.name = "ProtocolError";
123
+ this.kind = kind;
124
+ }
125
+ };
126
+ var RequestTimeoutError = class extends Error {
127
+ method;
128
+ timeoutMs;
129
+ constructor(method, timeoutMs) {
130
+ super(`app-server request timed out after ${timeoutMs}ms: ${method}`);
131
+ this.name = "RequestTimeoutError";
132
+ this.method = method;
133
+ this.timeoutMs = timeoutMs;
134
+ }
135
+ };
136
+ function isAppServerError(error) {
137
+ return error instanceof AppServerError;
138
+ }
139
+ function isRetryableTransportError(error) {
140
+ return error instanceof TransportError && error.retryable;
141
+ }
142
+ /**
143
+ * Single human-readable rendering of a caught error.
144
+ *
145
+ * Every UI surface shares this one: a bare `Error` contributes only its
146
+ * message, while a protocol error contributes its stable `code` plus the
147
+ * server's retry hint. Components must not grow their own variants — a
148
+ * second spelling means the two drift apart on the retry hint.
149
+ */
150
+ function formatError(error) {
151
+ if (error instanceof AppServerError) return `${error.code}: ${error.message}`;
152
+ if (error instanceof Error) return error.message;
153
+ return String(error);
154
+ }
155
+ /** Whether the caught error is retryable — used by UI surfaces to append a
156
+ * localized "(可重试)" hint without baking wording into this lib. */
157
+ function isRetryableError(error) {
158
+ return (error instanceof AppServerError || error instanceof TransportError) && error.retryable;
159
+ }
160
+ //#endregion
161
+ //#region src/localized.ts
162
+ /** The other language, i.e. the second link of every fallback chain. */
163
+ function otherLocalizedLang(lang) {
164
+ return lang === "zh" ? "en" : "zh";
165
+ }
166
+ /**
167
+ * Map an i18n language tag (`zh-CN`, `en-US`, `en`) to a variant language.
168
+ * Anything that is not English resolves to `zh`, matching the app default.
169
+ */
170
+ function toLocalizedLang(language) {
171
+ return (language ?? "").toLowerCase().startsWith("en") ? "en" : "zh";
172
+ }
173
+ /** `{field}_{lang}` → `{field}_{other}` → `""`. */
174
+ function pickLocalized(text, lang) {
175
+ if (!text) return "";
176
+ return text[lang]?.trim() || text[otherLocalizedLang(lang)]?.trim() || "";
177
+ }
178
+ /** Resolve a list of localized values, dropping the ones that resolve empty. */
179
+ function pickLocalizedList(list, lang) {
180
+ return (list ?? []).map((item) => pickLocalized(item, lang)).filter((value) => value.length > 0);
181
+ }
182
+ /** Non-empty string variants in fallback order. */
183
+ function variantKeys(field, lang) {
184
+ return [`${field}_${lang}`, `${field}_${otherLocalizedLang(lang)}`];
185
+ }
186
+ /** `{field}_{lang}` → `{field}_{other}` as a string, when either carries text. */
187
+ function pickVariantText(variants, field, lang) {
188
+ for (const key of variantKeys(field, lang)) {
189
+ const value = variants?.[key];
190
+ if (typeof value === "string" && value.trim().length > 0) return value;
191
+ }
192
+ }
193
+ /** `{field}_{lang}` → `{field}_{other}` as a string list, when either is non-empty. */
194
+ function pickVariantList(variants, field, lang) {
195
+ for (const key of variantKeys(field, lang)) {
196
+ const value = variants?.[key];
197
+ if (Array.isArray(value)) {
198
+ const items = value.filter((item) => typeof item === "string" && item.length > 0);
199
+ if (items.length > 0) return items;
200
+ }
201
+ }
202
+ }
203
+ /**
204
+ * Market entry tags. Per D8=A `tags_{lang}` outranks `legacy_tags_{lang}` —
205
+ * the legacy field only fills in when the current one is absent, in either
206
+ * language.
207
+ */
208
+ function pickEntryTags(variants, lang) {
209
+ return pickVariantList(variants, "tags", lang) ?? pickVariantList(variants, "legacy_tags", lang);
210
+ }
211
+ /**
212
+ * A market entry text field: variant first, then the baseline field
213
+ * (`entry.description`, `entry.name`, …) as the last link of the chain.
214
+ */
215
+ function pickEntryText(variants, field, lang, baseline) {
216
+ return pickVariantText(variants, field, lang) ?? baseline ?? void 0;
217
+ }
218
+ //#endregion
219
+ //#region src/conversation-events.ts
220
+ function stringValue(value) {
221
+ return typeof value === "string" ? value : null;
222
+ }
223
+ function numberValue(value) {
224
+ return typeof value === "number" ? value : null;
225
+ }
226
+ function isRecord(value) {
227
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
228
+ }
229
+ /**
230
+ * Strict boolean reader: a wire value that is not a real boolean reads as
231
+ * `null` (unknown) rather than being coerced — "retryable" must never be
232
+ * guessed from a truthy string.
233
+ */
234
+ function booleanValue(value) {
235
+ return typeof value === "boolean" ? value : null;
236
+ }
237
+ /** Flatten a message/activity body to plain text, JSON-encoding anything else. */
238
+ function contentToText(value) {
239
+ if (typeof value === "string") return value;
240
+ if (isRecord(value) && typeof value.content === "string") return value.content;
241
+ return value == null ? "" : JSON.stringify(value);
242
+ }
243
+ /** Activity bodies may be nested one level down or JSON-encoded as a string. */
244
+ function unwrapContent(value) {
245
+ if (typeof value === "string") try {
246
+ return JSON.parse(value);
247
+ } catch {
248
+ return value;
249
+ }
250
+ if (isRecord(value)) {
251
+ const content = value.content;
252
+ if (typeof content === "string") try {
253
+ return JSON.parse(content);
254
+ } catch {
255
+ return value;
256
+ }
257
+ }
258
+ return value;
259
+ }
260
+ /** Shape a thinking body (`message.thinking`, or `message.activity` kind `thinking`). */
261
+ function thinkingData(value) {
262
+ if (isRecord(value) && typeof value.content === "string") return {
263
+ content: value.content,
264
+ subject: stringValue(value.subject),
265
+ status: stringValue(value.status),
266
+ duration: numberValue(value.duration) ?? numberValue(value.duration_ms)
267
+ };
268
+ const candidate = unwrapContent(value);
269
+ if (!isRecord(candidate)) return {
270
+ content: typeof candidate === "string" ? candidate : "",
271
+ subject: null,
272
+ status: null,
273
+ duration: null
274
+ };
275
+ return {
276
+ content: stringValue(candidate.content) ?? "",
277
+ subject: stringValue(candidate.subject),
278
+ status: stringValue(candidate.status),
279
+ duration: numberValue(candidate.duration) ?? numberValue(candidate.duration_ms)
280
+ };
281
+ }
282
+ /** Shape a tips body; `type` is accepted as an alias of `tip_type`. */
283
+ function tipsData(value) {
284
+ const readTip = (data) => {
285
+ const content = stringValue(data.content);
286
+ const tipType = stringValue(data.tip_type) ?? stringValue(data.type);
287
+ if (content === null && tipType === null) return null;
288
+ return {
289
+ content: content ?? "",
290
+ tipType: tipType ?? "info"
291
+ };
292
+ };
293
+ if (isRecord(value)) {
294
+ const direct = readTip(value);
295
+ if (direct) return direct;
296
+ }
297
+ const candidate = unwrapContent(value);
298
+ if (isRecord(candidate)) {
299
+ const parsed = readTip(candidate);
300
+ if (parsed) return parsed;
301
+ }
302
+ if (candidate != null && !isRecord(candidate)) return {
303
+ content: String(candidate),
304
+ tipType: "info"
305
+ };
306
+ return {
307
+ content: "",
308
+ tipType: "info"
309
+ };
310
+ }
311
+ /** Shape a tool-call body; `null` when the body carries nothing tool-shaped. */
312
+ function toolCallData(value) {
313
+ const candidate = unwrapContent(value);
314
+ if (!isRecord(candidate)) return null;
315
+ const name = stringValue(candidate.name) ?? stringValue(candidate.tool_name) ?? stringValue(candidate.tool);
316
+ if (!name && candidate.args === void 0 && candidate.arguments === void 0 && candidate.output === void 0 && candidate.result === void 0) return null;
317
+ return {
318
+ name,
319
+ args: candidate.args ?? candidate.arguments ?? candidate.input,
320
+ output: candidate.output ?? candidate.result,
321
+ status: stringValue(candidate.status)
322
+ };
323
+ }
324
+ /**
325
+ * Shape a plan body; `null` when there is no usable step list.
326
+ *
327
+ * `step` is accepted as an alias of `content`: the tool's own argument is named
328
+ * `step`, and the producer normalizes it to `content` for the plan row — a row
329
+ * written before that normalization (or by a producer that skipped it) would
330
+ * otherwise render as an empty plan.
331
+ */
332
+ function planData(value) {
333
+ const candidate = unwrapContent(value);
334
+ if (!isRecord(candidate) || !Array.isArray(candidate.entries)) return null;
335
+ const entries = candidate.entries.flatMap((raw) => {
336
+ if (!isRecord(raw)) return [];
337
+ const content = stringValue(raw.content) ?? stringValue(raw.step);
338
+ if (!content) return [];
339
+ return [{
340
+ content,
341
+ status: planStepStatus(raw.status)
342
+ }];
343
+ });
344
+ return entries.length > 0 ? { entries } : null;
345
+ }
346
+ /** Unknown/absent step statuses read as `pending` — never as "done". */
347
+ function planStepStatus(value) {
348
+ return value === "completed" || value === "in_progress" ? value : "pending";
349
+ }
350
+ /** Shape a `context.usage` payload; `null` when nothing was actually measured. */
351
+ function parseContextUsage(value) {
352
+ if (!isRecord(value)) return null;
353
+ const used = typeof value.used_tokens === "number" ? value.used_tokens : 0;
354
+ const window = typeof value.window_tokens === "number" ? value.window_tokens : 0;
355
+ if (used <= 0 || window <= 0) return null;
356
+ const rawPercent = typeof value.percent === "number" ? value.percent : used / window * 100;
357
+ return {
358
+ used_tokens: used,
359
+ window_tokens: window,
360
+ percent: Math.min(100, Math.max(0, rawPercent)),
361
+ updated_at: typeof value.updated_at === "number" ? value.updated_at : Date.now(),
362
+ source: typeof value.source === "string" ? value.source : "measured"
363
+ };
364
+ }
365
+ /**
366
+ * Shape a per-turn `usage` body (W9 / R14); `null` when the runtime reported
367
+ * nothing usable.
368
+ *
369
+ * A missing side is **unknown**, not zero: half a bill is not a bill, so a
370
+ * frame that only carries `input_tokens` decodes to `null` rather than to a
371
+ * total that silently drops the output side. Same rule for an all-zero report —
372
+ * "no tokens reported" must never render as "this turn was free".
373
+ */
374
+ function parseTurnUsage(value) {
375
+ if (!isRecord(value)) return null;
376
+ const input = typeof value.input_tokens === "number" ? value.input_tokens : null;
377
+ const output = typeof value.output_tokens === "number" ? value.output_tokens : null;
378
+ if (input === null || output === null) return null;
379
+ if (input <= 0 && output <= 0) return null;
380
+ return {
381
+ input_tokens: input,
382
+ output_tokens: output,
383
+ total_tokens: typeof value.total_tokens === "number" ? value.total_tokens : input + output
384
+ };
385
+ }
386
+ /**
387
+ * Decode one `conversation/event` frame into a typed body.
388
+ *
389
+ * An activity frame whose `kind` is `thinking` is normalized to
390
+ * `message.thinking` (the server has two spellings for the same thing).
391
+ */
392
+ function decodeConversationEvent(event) {
393
+ const payload = event.payload ?? {};
394
+ const messageId = () => stringValue(payload.message_id);
395
+ const createdAt = () => numberValue(payload.created_at);
396
+ if (event.event_type === "message.activity" && stringValue(payload.kind) === "thinking") return {
397
+ kind: "message.thinking",
398
+ event,
399
+ messageId: messageId(),
400
+ createdAt: createdAt(),
401
+ thinking: thinkingData(payload),
402
+ replace: payload.replace === true
403
+ };
404
+ switch (event.event_type) {
405
+ case "message.created": return {
406
+ kind: "message.created",
407
+ event,
408
+ messageId: messageId(),
409
+ createdAt: createdAt(),
410
+ content: payload.content ?? ""
411
+ };
412
+ case "message.delta": return {
413
+ kind: "message.delta",
414
+ event,
415
+ messageId: messageId(),
416
+ createdAt: createdAt(),
417
+ delta: stringValue(payload.content) ?? "",
418
+ replace: payload.replace === true
419
+ };
420
+ case "message.thinking": return {
421
+ kind: "message.thinking",
422
+ event,
423
+ messageId: messageId(),
424
+ createdAt: createdAt(),
425
+ thinking: thinkingData(payload),
426
+ replace: payload.replace === true
427
+ };
428
+ case "message.tool": return {
429
+ kind: "message.tool",
430
+ event,
431
+ messageId: messageId(),
432
+ createdAt: createdAt(),
433
+ tool: toolCallData(payload)
434
+ };
435
+ case "message.tips": return {
436
+ kind: "message.tips",
437
+ event,
438
+ messageId: messageId(),
439
+ createdAt: createdAt(),
440
+ tips: tipsData(payload)
441
+ };
442
+ case "message.error": return {
443
+ kind: "message.error",
444
+ event,
445
+ messageId: messageId(),
446
+ createdAt: createdAt(),
447
+ message: stringValue(payload.message),
448
+ code: stringValue(payload.code),
449
+ retryable: booleanValue(payload.retryable)
450
+ };
451
+ case "message.activity": {
452
+ const activityKind = stringValue(payload.kind) ?? "activity";
453
+ return {
454
+ kind: "message.activity",
455
+ event,
456
+ messageId: messageId(),
457
+ createdAt: createdAt(),
458
+ activityKind,
459
+ content: payload.content ?? "",
460
+ usage: activityKind === "turn_completed" ? parseTurnUsage(payload.usage) : null
461
+ };
462
+ }
463
+ case "turn.status": return {
464
+ kind: "turn.status",
465
+ event,
466
+ running: stringValue(payload.status) === "running"
467
+ };
468
+ case "context.usage": return {
469
+ kind: "context.usage",
470
+ event,
471
+ usage: parseContextUsage(payload.context_usage)
472
+ };
473
+ default: return {
474
+ kind: "unknown",
475
+ event,
476
+ eventType: event.event_type
477
+ };
478
+ }
479
+ }
480
+ //#endregion
481
+ export { APP_SERVER_EXPERT_PACK_FORMAT, APP_SERVER_PROTOCOL_VERSION, AppServerError, ProtocolError, RequestTimeoutError, TransportError, booleanValue, contentToText, decodeConversationEvent, formatError, isAppServerError, isRecord, isRetryableError, isRetryableTransportError, numberValue, otherLocalizedLang, parseContextUsage, parseTurnUsage, pickEntryTags, pickEntryText, pickLocalized, pickLocalizedList, pickVariantList, pickVariantText, planData, stringValue, thinkingData, tipsData, toLocalizedLang, toolCallData };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flowy-agent-store/protocol",
3
+ "version": "0.1.0-beta.10",
4
+ "description": "Versioned App Server Protocol types and wire errors (no runtime, no DOM/Node deps)",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Michael-Lfx/allo.git",
13
+ "directory": "web/packages/protocol"
14
+ },
15
+ "sideEffects": false,
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.mts",
19
+ "import": "./dist/index.mjs",
20
+ "require": "./dist/index.cjs"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsdown",
29
+ "prepack": "tsdown"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org/"
34
+ },
35
+ "main": "./dist/index.cjs",
36
+ "module": "./dist/index.mjs",
37
+ "types": "./dist/index.d.mts"
38
+ }