pikiloom 0.4.26 → 0.4.28
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/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
- package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
- package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
- package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
- package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
- package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
- package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
- package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
- package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/acp-client.js +0 -59
- package/dist/agent/artifacts.js +1 -38
- package/dist/agent/auto-update.js +2 -78
- package/dist/agent/await-resume.js +0 -20
- package/dist/agent/cli/auth.js +3 -51
- package/dist/agent/cli/catalog.js +0 -3
- package/dist/agent/cli/detector.js +1 -19
- package/dist/agent/cli/index.js +0 -3
- package/dist/agent/cli/registry.js +0 -25
- package/dist/agent/driver.js +0 -14
- package/dist/agent/drivers/claude-tui.js +2 -769
- package/dist/agent/drivers/claude.js +12 -631
- package/dist/agent/drivers/codex.js +11 -162
- package/dist/agent/drivers/gemini.js +6 -114
- package/dist/agent/drivers/hermes.js +0 -128
- package/dist/agent/goal.js +0 -45
- package/dist/agent/handover.js +1 -46
- package/dist/agent/images.js +7 -133
- package/dist/agent/index.js +1 -29
- package/dist/agent/mcp/bridge.js +1 -197
- package/dist/agent/mcp/extensions.js +5 -84
- package/dist/agent/mcp/oauth.js +2 -69
- package/dist/agent/mcp/registry.js +2 -33
- package/dist/agent/mcp/session-server.js +2 -45
- package/dist/agent/mcp/tools/ask-user.js +0 -6
- package/dist/agent/mcp/tools/await-resume.js +0 -15
- package/dist/agent/mcp/tools/goal.js +0 -25
- package/dist/agent/mcp/tools/types.js +0 -5
- package/dist/agent/mcp/tools/workspace.js +0 -23
- package/dist/agent/npm.js +0 -14
- package/dist/agent/session.js +86 -196
- package/dist/agent/skill-installer.js +3 -70
- package/dist/agent/skills.js +0 -43
- package/dist/agent/stream.js +5 -148
- package/dist/agent/types.js +0 -12
- package/dist/agent/utils.js +2 -120
- package/dist/bot/bot.js +3 -468
- package/dist/bot/command-ui.js +0 -45
- package/dist/bot/commands.js +0 -51
- package/dist/bot/headless-bot.js +0 -21
- package/dist/bot/host.js +1 -5
- package/dist/bot/human-loop.js +0 -8
- package/dist/bot/menu.js +0 -3
- package/dist/bot/orchestration.js +0 -3
- package/dist/bot/render-shared.js +0 -63
- package/dist/bot/session-hub.js +14 -92
- package/dist/bot/session-status.js +1 -5
- package/dist/bot/streaming.js +0 -53
- package/dist/browser-profile.js +0 -51
- package/dist/browser-supervisor.js +0 -75
- package/dist/catalog/cli-tools.js +0 -53
- package/dist/catalog/index.js +0 -18
- package/dist/catalog/local-models.js +0 -18
- package/dist/catalog/mcp-servers.js +0 -35
- package/dist/catalog/skill-repos.js +0 -32
- package/dist/channels/base.js +0 -19
- package/dist/channels/dingtalk/bot.js +0 -3
- package/dist/channels/dingtalk/channel.js +0 -33
- package/dist/channels/discord/bot.js +0 -7
- package/dist/channels/discord/channel.js +0 -22
- package/dist/channels/feishu/bot.js +0 -71
- package/dist/channels/feishu/channel.js +0 -78
- package/dist/channels/feishu/markdown.js +0 -5
- package/dist/channels/feishu/render.js +0 -45
- package/dist/channels/health.js +0 -46
- package/dist/channels/slack/bot.js +0 -7
- package/dist/channels/slack/channel.js +1 -27
- package/dist/channels/states.js +0 -3
- package/dist/channels/telegram/bot.js +0 -60
- package/dist/channels/telegram/channel.js +2 -99
- package/dist/channels/telegram/directory.js +0 -3
- package/dist/channels/telegram/live-preview.js +0 -8
- package/dist/channels/telegram/render.js +0 -30
- package/dist/channels/wecom/bot.js +0 -3
- package/dist/channels/wecom/channel.js +0 -58
- package/dist/channels/weixin/api.js +0 -9
- package/dist/channels/weixin/bot.js +0 -88
- package/dist/channels/weixin/channel.js +0 -6
- package/dist/cli/autostart.js +1 -44
- package/dist/cli/channel-supervisor.js +1 -59
- package/dist/cli/channels.js +0 -6
- package/dist/cli/main.js +1 -145
- package/dist/cli/onboarding.js +0 -6
- package/dist/cli/run.js +0 -11
- package/dist/cli/setup-wizard.js +0 -3
- package/dist/core/config/runtime-config.js +0 -38
- package/dist/core/config/user-config.js +0 -63
- package/dist/core/config/validation.js +0 -9
- package/dist/core/constants.js +0 -268
- package/dist/core/git.js +0 -31
- package/dist/core/legacy-compat.js +1 -28
- package/dist/core/logging.js +0 -3
- package/dist/core/platform.js +0 -28
- package/dist/core/process-control.js +0 -8
- package/dist/core/secrets/index.js +0 -17
- package/dist/core/secrets/inline-seal.js +0 -7
- package/dist/core/secrets/ref.js +0 -19
- package/dist/core/secrets/resolver.js +0 -8
- package/dist/core/secrets/store.js +1 -12
- package/dist/core/utils.js +1 -37
- package/dist/core/version.js +0 -3
- package/dist/dashboard/platform.js +1 -35
- package/dist/dashboard/routes/agents.js +2 -71
- package/dist/dashboard/routes/cli.js +0 -27
- package/dist/dashboard/routes/config.js +0 -54
- package/dist/dashboard/routes/extensions.js +0 -132
- package/dist/dashboard/routes/local-models.js +0 -68
- package/dist/dashboard/routes/models.js +0 -46
- package/dist/dashboard/routes/sessions.js +0 -100
- package/dist/dashboard/runtime.js +1 -58
- package/dist/dashboard/server.js +0 -49
- package/dist/dashboard/session-control.js +2 -69
- package/dist/model/catalog.js +1 -17
- package/dist/model/index.js +0 -15
- package/dist/model/injector.js +1 -181
- package/dist/model/provider-models.js +1 -48
- package/dist/model/responses-bridge.js +3 -45
- package/dist/model/store.js +0 -17
- package/dist/model/types.js +0 -12
- package/dist/model/validation.js +1 -18
- package/dist/pikichannel/adapter-pikiloom.js +0 -31
- package/dist/pikichannel/code.js +0 -12
- package/dist/pikichannel/codec.js +0 -14
- package/dist/pikichannel/host.js +4 -33
- package/dist/pikichannel/protocol.js +0 -52
- package/dist/pikichannel/rendezvous-broker.js +5 -26
- package/dist/pikichannel/rendezvous-host.js +3 -16
- package/dist/pikichannel/server.js +6 -55
- package/dist/pikichannel/transport.js +0 -25
- package/dist/pikichannel/transports/webrtc-host.js +0 -18
- package/dist/pikichannel/transports/webrtc-shared.js +4 -34
- package/dist/pikichannel/transports/websocket-host.js +1 -14
- package/dist/pikichannel/turn.js +2 -69
- package/dist/pikichannel/web/sdk.js +14 -71
- package/package.json +1 -1
- package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
- package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
- package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
- package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
- package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
- package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
|
@@ -1,29 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider model-list cache.
|
|
3
|
-
*
|
|
4
|
-
* Backs the GET /api/models/providers/:id/models endpoint and the agent-status
|
|
5
|
-
* + IM /models surfaces. Each entry is a list of model ids the provider's
|
|
6
|
-
* /models endpoint reported, plus a fetch timestamp for TTL invalidation.
|
|
7
|
-
*
|
|
8
|
-
* Cache is in-memory only — providers' validation state already persists in
|
|
9
|
-
* setting.json; the model list itself can be re-fetched cheaply on demand.
|
|
10
|
-
*/
|
|
11
1
|
import { getProvider } from './store.js';
|
|
12
2
|
import { validateProvider } from './validation.js';
|
|
13
|
-
const TTL_MS = 30 * 60 * 1000;
|
|
14
|
-
// Cache is pinned to globalThis so module re-instantiation under tsx ESM
|
|
15
|
-
// (e.g. when the same file is imported via different specifier strings) can't
|
|
16
|
-
// fragment it into multiple disjoint maps. Without this, `getProviderModelList`
|
|
17
|
-
// from `dashboard/routes/models.ts` (barrel) and `peekProviderModelList` from
|
|
18
|
-
// `dashboard/routes/agents.ts` (also barrel) were writing/reading two
|
|
19
|
-
// different Map instances, making the peek perpetually miss.
|
|
20
|
-
// Cache pinned to globalThis so module re-instantiation under tsx ESM
|
|
21
|
-
// (where the same file imported via different specifier strings ends up as
|
|
22
|
-
// separate ESM module instances) can't fragment it into multiple disjoint
|
|
23
|
-
// maps. Without this, `getProviderModelList` from `dashboard/routes/models.ts`
|
|
24
|
-
// and `peekProviderModelList` from `dashboard/routes/agents.ts` would be
|
|
25
|
-
// writing/reading two different Map instances and the peek would
|
|
26
|
-
// perpetually miss.
|
|
3
|
+
const TTL_MS = 30 * 60 * 1000;
|
|
27
4
|
const GLOBAL_KEY = Symbol.for('pikiloom.providerModelsCache');
|
|
28
5
|
const _existing = globalThis[GLOBAL_KEY];
|
|
29
6
|
const cache = _existing || new Map();
|
|
@@ -34,10 +11,6 @@ function isFresh(entry, provider) {
|
|
|
34
11
|
return false;
|
|
35
12
|
return (Date.now() - entry.fetchedAt) < TTL_MS;
|
|
36
13
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Get the model list for a provider, fetching from /models on cache miss or
|
|
39
|
-
* when the provider config has been updated since the last fetch.
|
|
40
|
-
*/
|
|
41
14
|
export async function getProviderModelList(providerId, opts = {}) {
|
|
42
15
|
const provider = getProvider(providerId);
|
|
43
16
|
if (!provider)
|
|
@@ -66,19 +39,9 @@ export async function getProviderModelList(providerId, opts = {}) {
|
|
|
66
39
|
fromCache: false,
|
|
67
40
|
};
|
|
68
41
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Invalidate cached model list (e.g. after a provider edit/delete).
|
|
71
|
-
*/
|
|
72
42
|
export function invalidateProviderModels(providerId) {
|
|
73
43
|
cache.delete(providerId);
|
|
74
44
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Synchronous peek for a single model's cached metadata (context length,
|
|
77
|
-
* pricing). Returns `null` on cache miss or when the entry is stale relative
|
|
78
|
-
* to the provider's `updatedAt` — callers should treat that as "unknown" and
|
|
79
|
-
* fall back to whatever the agent CLI reports. Pair with
|
|
80
|
-
* `prefetchProviderModels` to populate the cache lazily for the next call.
|
|
81
|
-
*/
|
|
82
45
|
export function peekProviderModelInfo(providerId, modelId) {
|
|
83
46
|
const provider = getProvider(providerId);
|
|
84
47
|
if (!provider)
|
|
@@ -88,11 +51,6 @@ export function peekProviderModelInfo(providerId, modelId) {
|
|
|
88
51
|
return null;
|
|
89
52
|
return entry.modelInfos.find(info => info.id === modelId) ?? null;
|
|
90
53
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Synchronous peek for the full cached model list of a provider. Returns
|
|
93
|
-
* `null` on cache miss / stale entry — callers should fall back and let a
|
|
94
|
-
* background refresh populate it (`prefetchProviderModels`).
|
|
95
|
-
*/
|
|
96
54
|
export function peekProviderModelList(providerId) {
|
|
97
55
|
const provider = getProvider(providerId);
|
|
98
56
|
if (!provider)
|
|
@@ -102,11 +60,6 @@ export function peekProviderModelList(providerId) {
|
|
|
102
60
|
return null;
|
|
103
61
|
return entry.modelInfos;
|
|
104
62
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Fire-and-forget cache fill. Safe to call repeatedly: no-ops when the cache
|
|
107
|
-
* is already fresh, otherwise triggers a single in-flight fetch and discards
|
|
108
|
-
* the result (the next sync peek will see the populated cache).
|
|
109
|
-
*/
|
|
110
63
|
export function prefetchProviderModels(providerId) {
|
|
111
64
|
void getProviderModelList(providerId).catch(() => { });
|
|
112
65
|
}
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Responses↔Chat bridge.
|
|
3
|
-
*
|
|
4
|
-
* Codex 0.140+ speaks ONLY the OpenAI Responses API (`wire_api = "chat"` was
|
|
5
|
-
* removed). Many OpenAI-compatible providers — DeepSeek, Kimi/Moonshot,
|
|
6
|
-
* MiniMax, 豆包/Doubao, Qwen/DashScope, Zhipu, … — implement ONLY the Chat
|
|
7
|
-
* Completions API. This in-process HTTP server bridges the two so codex can
|
|
8
|
-
* drive any chat-only provider:
|
|
9
|
-
*
|
|
10
|
-
* codex ──(Responses API)──▶ bridge ──(Chat Completions)──▶ upstream provider
|
|
11
|
-
*
|
|
12
|
-
* One server instance routes every upstream: the upstream base URL is encoded
|
|
13
|
-
* (base64url) into the request path (`/u/<token>/responses`). The caller's
|
|
14
|
-
* Authorization header is forwarded verbatim, so the bridge never reads or
|
|
15
|
-
* stores credentials — codex injects `Authorization: Bearer <key>` from the
|
|
16
|
-
* provider's `env_key`, and we relay it upstream.
|
|
17
|
-
*
|
|
18
|
-
* Translation is intentionally NON-incremental: we call the upstream with
|
|
19
|
-
* `stream:false`, then synthesise a complete, spec-shaped Responses SSE stream.
|
|
20
|
-
* Codex rebuilds a turn from `response.output_item.done` items plus the final
|
|
21
|
-
* `response.completed`, so a fully-populated terminal payload is authoritative;
|
|
22
|
-
* this sidesteps fragile per-token delta bookkeeping while still surfacing
|
|
23
|
-
* assistant text AND tool/function calls (apply_patch, shell, MCP tools).
|
|
24
|
-
*/
|
|
25
1
|
import http from 'node:http';
|
|
26
2
|
import { writeScopedLog } from '../core/logging.js';
|
|
27
3
|
const SCOPE = 'model-bridge';
|
|
@@ -36,7 +12,6 @@ function genId(prefix) {
|
|
|
36
12
|
return `${prefix}_${Date.now().toString(36)}${idCounter.toString(36)}`;
|
|
37
13
|
}
|
|
38
14
|
function num(v) { return typeof v === 'number' && Number.isFinite(v) ? v : 0; }
|
|
39
|
-
/** base64url-encode an upstream base URL so it survives as a single path segment. */
|
|
40
15
|
export function upstreamToken(baseURL) {
|
|
41
16
|
return Buffer.from(baseURL, 'utf8').toString('base64url');
|
|
42
17
|
}
|
|
@@ -48,7 +23,6 @@ function decodeUpstream(token) {
|
|
|
48
23
|
return null;
|
|
49
24
|
}
|
|
50
25
|
}
|
|
51
|
-
/** Start (or reuse) the singleton bridge server; resolves to its localhost port. */
|
|
52
26
|
export async function ensureResponsesBridge() {
|
|
53
27
|
if (server && listenPort)
|
|
54
28
|
return listenPort;
|
|
@@ -76,13 +50,10 @@ export function shutdownResponsesBridge() {
|
|
|
76
50
|
try {
|
|
77
51
|
server?.close();
|
|
78
52
|
}
|
|
79
|
-
catch {
|
|
53
|
+
catch { }
|
|
80
54
|
server = null;
|
|
81
55
|
listenPort = 0;
|
|
82
56
|
}
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// HTTP handling
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
57
|
function handleRequest(req, res) {
|
|
87
58
|
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
88
59
|
const m = url.pathname.match(/^\/u\/([^/]+)\/(responses|models)$/);
|
|
@@ -96,8 +67,6 @@ function handleRequest(req, res) {
|
|
|
96
67
|
return;
|
|
97
68
|
}
|
|
98
69
|
if (m[2] === 'models') {
|
|
99
|
-
// Codex's model-catalog refresh is best-effort; an empty list keeps it quiet
|
|
100
|
-
// and never blocks the turn.
|
|
101
70
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
102
71
|
res.end(JSON.stringify({ object: 'list', data: [], models: [] }));
|
|
103
72
|
return;
|
|
@@ -148,11 +117,6 @@ async function handleResponses(req, res, upstreamBase, body) {
|
|
|
148
117
|
sendResponsesError(res, `upstream ${upstreamResp.status}: ${raw.slice(0, 500)}`);
|
|
149
118
|
return;
|
|
150
119
|
}
|
|
151
|
-
// Forward the upstream Chat Completions SSE incrementally, translating each
|
|
152
|
-
// delta into Responses events, so codex (and the dashboard) render the answer
|
|
153
|
-
// progressively — token-by-token — instead of popping it in at once. Codex
|
|
154
|
-
// still rebuilds the turn from the terminal `response.output_item.done` +
|
|
155
|
-
// `response.completed`, which we always emit.
|
|
156
120
|
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
157
121
|
res.flushHeaders?.();
|
|
158
122
|
let seq = 0;
|
|
@@ -285,7 +249,7 @@ function sendResponsesError(res, message) {
|
|
|
285
249
|
try {
|
|
286
250
|
res.end();
|
|
287
251
|
}
|
|
288
|
-
catch {
|
|
252
|
+
catch { }
|
|
289
253
|
return;
|
|
290
254
|
}
|
|
291
255
|
res.writeHead(200, {
|
|
@@ -300,9 +264,6 @@ function sendResponsesError(res, message) {
|
|
|
300
264
|
emit({ type: 'response.failed', response: { id, object: 'response', status: 'failed', error: { code: 'bridge_error', message }, output: [] } });
|
|
301
265
|
res.end();
|
|
302
266
|
}
|
|
303
|
-
// ---------------------------------------------------------------------------
|
|
304
|
-
// Request translation: Responses → Chat Completions
|
|
305
|
-
// ---------------------------------------------------------------------------
|
|
306
267
|
function asText(content) {
|
|
307
268
|
if (typeof content === 'string')
|
|
308
269
|
return content;
|
|
@@ -352,7 +313,6 @@ function toChatRequest(body) {
|
|
|
352
313
|
});
|
|
353
314
|
}
|
|
354
315
|
else if (type === 'reasoning') {
|
|
355
|
-
// Chat models cannot ingest prior reasoning items — drop.
|
|
356
316
|
}
|
|
357
317
|
}
|
|
358
318
|
const tools = Array.isArray(body.tools)
|
|
@@ -388,13 +348,11 @@ function toChatTool(t) {
|
|
|
388
348
|
},
|
|
389
349
|
};
|
|
390
350
|
}
|
|
391
|
-
// Codex built-in custom tools (e.g. local_shell) and web_search aren't
|
|
392
|
-
// expressible as chat functions — drop; codex falls back to its function tools.
|
|
393
351
|
return null;
|
|
394
352
|
}
|
|
395
353
|
function toChatToolChoice(tc) {
|
|
396
354
|
if (typeof tc === 'string')
|
|
397
|
-
return tc;
|
|
355
|
+
return tc;
|
|
398
356
|
if (tc?.type === 'function' && tc.name)
|
|
399
357
|
return { type: 'function', function: { name: tc.name } };
|
|
400
358
|
if (tc?.type === 'function' && tc.function)
|
package/dist/model/store.js
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persistence for Provider / Profile / activeProfileByAgent.
|
|
3
|
-
* Reads and writes the `models` section of ~/.pikiloom/setting.json.
|
|
4
|
-
*/
|
|
5
1
|
import { randomUUID } from 'node:crypto';
|
|
6
2
|
import { loadUserConfig, saveUserConfig } from '../core/config/user-config.js';
|
|
7
3
|
import { persistSecret, forgetSecret } from '../core/secrets/index.js';
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Read helpers
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
4
|
function getModelLayer() {
|
|
12
5
|
const config = loadUserConfig();
|
|
13
6
|
return config.models || {};
|
|
@@ -62,11 +55,9 @@ export async function updateProvider(id, patch) {
|
|
|
62
55
|
credential = patch.credentialRef;
|
|
63
56
|
else if (patch.apiKey !== undefined) {
|
|
64
57
|
if (existing.credential.source === 'keychain') {
|
|
65
|
-
// overwrite same keychain slot
|
|
66
58
|
credential = await persistSecret(existing.credential.account, patch.apiKey);
|
|
67
59
|
}
|
|
68
60
|
else {
|
|
69
|
-
// upgrade from inline/env/command to keychain
|
|
70
61
|
credential = await persistSecret(`provider/${id}`, patch.apiKey);
|
|
71
62
|
}
|
|
72
63
|
}
|
|
@@ -92,13 +83,11 @@ export async function removeProvider(id) {
|
|
|
92
83
|
if (!existing)
|
|
93
84
|
return false;
|
|
94
85
|
delete providers[id];
|
|
95
|
-
// also drop any profiles bound to this provider
|
|
96
86
|
const profiles = { ...(layer.profiles || {}) };
|
|
97
87
|
for (const [pid, prof] of Object.entries(profiles)) {
|
|
98
88
|
if (prof.providerId === id)
|
|
99
89
|
delete profiles[pid];
|
|
100
90
|
}
|
|
101
|
-
// and any active bindings
|
|
102
91
|
const bindings = { ...(layer.activeProfileByAgent || {}) };
|
|
103
92
|
for (const agentId of Object.keys(bindings)) {
|
|
104
93
|
const profileId = bindings[agentId];
|
|
@@ -127,9 +116,6 @@ export function getProfile(id) {
|
|
|
127
116
|
return layer.profiles?.[id] || null;
|
|
128
117
|
}
|
|
129
118
|
function defaultProfileName(_providerName, modelId, _effort) {
|
|
130
|
-
// Keep the auto-generated label short: the brand icon already carries the
|
|
131
|
-
// provider, and effort lives in its own pill on the card. Just the model id
|
|
132
|
-
// gives the cleanest tile + IM list, with the user free to override.
|
|
133
119
|
return modelId;
|
|
134
120
|
}
|
|
135
121
|
export function addProfile(input) {
|
|
@@ -188,9 +174,6 @@ export function removeProfile(id) {
|
|
|
188
174
|
writeModelLayer({ ...layer, profiles, activeProfileByAgent: bindings });
|
|
189
175
|
return true;
|
|
190
176
|
}
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
// Active binding
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
177
|
export function getActiveProfileId(agentId) {
|
|
195
178
|
const layer = getModelLayer();
|
|
196
179
|
return layer.activeProfileByAgent?.[agentId] || null;
|
package/dist/model/types.js
CHANGED
|
@@ -1,13 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pikiloom "Model" layer — Provider/Profile data model.
|
|
3
|
-
*
|
|
4
|
-
* Provider = a configured endpoint + credential reference (e.g. "OpenRouter
|
|
5
|
-
* personal", "Anthropic direct"). Holds baseURL + extra headers.
|
|
6
|
-
* Profile = a Provider + modelId + tuning params (effort, max output).
|
|
7
|
-
* This is the unit a user binds to an agent.
|
|
8
|
-
*
|
|
9
|
-
* Profiles are the smallest selectable unit because Hermes (and most coding
|
|
10
|
-
* agents) only support one model per session. Binding `activeProfileByAgent`
|
|
11
|
-
* tells the driver which Profile to use when spawning that agent.
|
|
12
|
-
*/
|
|
13
1
|
export {};
|
package/dist/model/validation.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider credential validation — Feishu-style explicit check.
|
|
3
|
-
*
|
|
4
|
-
* Validation strategy: GET ${baseURL}/models with the resolved API key.
|
|
5
|
-
* Status semantics mirror ChannelSetupState in core/config/validation.ts:
|
|
6
|
-
* missing | invalid | error | ready
|
|
7
|
-
*/
|
|
8
1
|
import { request } from 'undici';
|
|
9
2
|
import { resolveCredential } from '../core/secrets/index.js';
|
|
10
3
|
const VALIDATION_TIMEOUT_MS = 10_000;
|
|
@@ -12,13 +5,10 @@ function buildHeaders(provider, apiKey) {
|
|
|
12
5
|
const base = { 'Accept': 'application/json' };
|
|
13
6
|
switch (provider.kind) {
|
|
14
7
|
case 'anthropic':
|
|
15
|
-
// Anthropic models endpoint is /v1/models with x-api-key + anthropic-version
|
|
16
8
|
base['x-api-key'] = apiKey;
|
|
17
9
|
base['anthropic-version'] = '2023-06-01';
|
|
18
10
|
break;
|
|
19
11
|
case 'google':
|
|
20
|
-
// Google AI Studio uses ?key= query param, not Authorization header.
|
|
21
|
-
// Caller appends ?key= in URL builder below.
|
|
22
12
|
break;
|
|
23
13
|
case 'openai':
|
|
24
14
|
case 'openai-compatible':
|
|
@@ -33,14 +23,12 @@ function buildHeaders(provider, apiKey) {
|
|
|
33
23
|
function modelsUrl(provider, apiKey) {
|
|
34
24
|
const base = provider.baseURL.replace(/\/+$/, '');
|
|
35
25
|
if (provider.kind === 'google') {
|
|
36
|
-
// Google AI Studio: GET https://generativelanguage.googleapis.com/v1beta/models?key=API_KEY
|
|
37
26
|
const sep = base.includes('?') ? '&' : '?';
|
|
38
27
|
return `${base}/models${sep}key=${encodeURIComponent(apiKey)}`;
|
|
39
28
|
}
|
|
40
29
|
if (provider.kind === 'anthropic') {
|
|
41
30
|
return `${base}/v1/models`;
|
|
42
31
|
}
|
|
43
|
-
// OpenAI-compatible
|
|
44
32
|
return `${base}/models`;
|
|
45
33
|
}
|
|
46
34
|
function toNumber(value) {
|
|
@@ -53,11 +41,6 @@ function toNumber(value) {
|
|
|
53
41
|
}
|
|
54
42
|
return undefined;
|
|
55
43
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Convert a per-token USD price (the format OpenRouter & most OpenAI-compatible
|
|
58
|
-
* APIs use) to USD per 1M tokens. We preserve up to 4 decimals so micro-prices
|
|
59
|
-
* don't round to zero.
|
|
60
|
-
*/
|
|
61
44
|
function perTokenToPerMillion(value) {
|
|
62
45
|
const n = toNumber(value);
|
|
63
46
|
if (n === undefined)
|
|
@@ -177,7 +160,7 @@ export async function validateProvider(provider) {
|
|
|
177
160
|
try {
|
|
178
161
|
parsed = JSON.parse(bodyText);
|
|
179
162
|
}
|
|
180
|
-
catch {
|
|
163
|
+
catch { }
|
|
181
164
|
const modelInfos = extractModelInfos(parsed);
|
|
182
165
|
const models = modelInfos.map(info => info.id);
|
|
183
166
|
return {
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/adapter-pikiloom.ts — the reference {@link SessionSource}.
|
|
3
|
-
*
|
|
4
|
-
* Binds the universal protocol to pikiloom's live runtime *without touching the
|
|
5
|
-
* bot*. It is a peer consumer of the very same surfaces the Web Dashboard uses:
|
|
6
|
-
* - data plane: the `runtime.events` 'dashboard-event' bus (same bus the
|
|
7
|
-
* dashboard WebSocket layer listens to). We must NOT call
|
|
8
|
-
* `bot.onStreamSnapshot` — that is a single-slot callback already owned by
|
|
9
|
-
* the runtime; re-registering it would silently break the dashboard.
|
|
10
|
-
* - control plane: the exported session-control functions
|
|
11
|
-
* (`queueDashboardSessionTask`, `stopSessionTasks`, …) — the same entry
|
|
12
|
-
* points the dashboard REST routes call.
|
|
13
|
-
*
|
|
14
|
-
* The StreamSnapshot → UniversalSnapshot projection lives here, keeping the
|
|
15
|
-
* protocol package free of any pikiloom-internal type.
|
|
16
|
-
*/
|
|
17
1
|
import { EventEmitter } from 'node:events';
|
|
18
2
|
import { runtime } from '../dashboard/runtime.js';
|
|
19
3
|
import { queueDashboardSessionTask, stopSessionTasks, steerSessionTask, cancelSessionTask, interactionSelectOption, interactionSubmitText, interactionSkip, interactionCancel, } from '../dashboard/session-control.js';
|
|
@@ -45,8 +29,6 @@ function projectInteractions(snap) {
|
|
|
45
29
|
})),
|
|
46
30
|
}));
|
|
47
31
|
}
|
|
48
|
-
/** Strip empty optionals so the projection is lean AND deterministic (the diff
|
|
49
|
-
* baseline must be stable: a field is either consistently present or absent). */
|
|
50
32
|
function compact(obj) {
|
|
51
33
|
for (const k of Object.keys(obj)) {
|
|
52
34
|
const v = obj[k];
|
|
@@ -57,7 +39,6 @@ function compact(obj) {
|
|
|
57
39
|
}
|
|
58
40
|
return obj;
|
|
59
41
|
}
|
|
60
|
-
/** Project pikiloom's StreamSnapshot into the agent-agnostic wire snapshot. */
|
|
61
42
|
export function projectSnapshot(sessionKey, snap) {
|
|
62
43
|
const { agent } = splitKey(sessionKey);
|
|
63
44
|
const meta = snap.previewMeta;
|
|
@@ -105,34 +86,24 @@ function metaFromSnapshot(sessionKey, snap) {
|
|
|
105
86
|
const title = (snap.prompt || snap.text || '').slice(0, 80) || null;
|
|
106
87
|
return { sessionKey, agent: snap.agent, title, phase: snap.phase, updatedAt: snap.updatedAt };
|
|
107
88
|
}
|
|
108
|
-
/**
|
|
109
|
-
* The pikiloom SessionSource. One instance per process; it attaches to the
|
|
110
|
-
* runtime event bus lazily on first `onUpdate`.
|
|
111
|
-
*/
|
|
112
89
|
export class PikiloomSessionSource {
|
|
113
90
|
forwarder;
|
|
114
91
|
seqs = new Map();
|
|
115
92
|
known = new Map();
|
|
116
93
|
bus = new EventEmitter();
|
|
117
94
|
wired = false;
|
|
118
|
-
/** @param forwarder forwards tunneled `/api/*` requests to the HTTP router. */
|
|
119
95
|
constructor(forwarder) {
|
|
120
96
|
this.forwarder = forwarder;
|
|
121
97
|
}
|
|
122
98
|
hostInfo() {
|
|
123
99
|
return { name: 'pikiloom', version: VERSION, capabilities: CAPABILITIES, authRequired: true };
|
|
124
100
|
}
|
|
125
|
-
/** Control-plane HTTP tunnel — delegates to the embedder-supplied forwarder
|
|
126
|
-
* (the dashboard's Hono router). The host has already enforced auth + the
|
|
127
|
-
* `/api/*` allowlist before we get here. */
|
|
128
101
|
async handleRequest(req) {
|
|
129
102
|
if (!this.forwarder)
|
|
130
103
|
return { status: 503, body: 'tunnel forwarder not configured' };
|
|
131
104
|
return this.forwarder(req);
|
|
132
105
|
}
|
|
133
106
|
listSessions() {
|
|
134
|
-
// Most-recent first, bounded so a long-lived host never ships an unbounded
|
|
135
|
-
// list over the wire (remote clients want a recent picker, not all history).
|
|
136
107
|
return Array.from(this.known.values()).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).slice(0, 50);
|
|
137
108
|
}
|
|
138
109
|
getSnapshot(sessionKey) {
|
|
@@ -154,7 +125,6 @@ export class PikiloomSessionSource {
|
|
|
154
125
|
this.bus.on('sessions', handler);
|
|
155
126
|
return () => this.bus.off('sessions', handler);
|
|
156
127
|
}
|
|
157
|
-
/** Subscribe once to the runtime's dashboard-event bus and fan out internally. */
|
|
158
128
|
ensureWired() {
|
|
159
129
|
if (this.wired)
|
|
160
130
|
return;
|
|
@@ -172,7 +142,6 @@ export class PikiloomSessionSource {
|
|
|
172
142
|
}
|
|
173
143
|
});
|
|
174
144
|
}
|
|
175
|
-
// -- control plane (delegates to the shared session-control surface) -----
|
|
176
145
|
async prompt(cmd) {
|
|
177
146
|
const config = loadUserConfig();
|
|
178
147
|
const workdir = cmd.workdir || runtime.getRequestWorkdir(config);
|
package/dist/pikichannel/code.js
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/code.ts — the shareable connection code (server side).
|
|
3
|
-
*
|
|
4
|
-
* One short string a host shows and a client pastes. Same format the browser
|
|
5
|
-
* `endpoint.ts` decodes — keep the keys (h/r/n/t) in lockstep:
|
|
6
|
-
* h = direct host authority r = rendezvous URL n = nodeId t = token
|
|
7
|
-
*
|
|
8
|
-
* `buildServerCode` picks the best reachability for THIS host:
|
|
9
|
-
* - public address set → DIRECT code {h,t} (public-IP server; no NAT hop)
|
|
10
|
-
* - else rendezvous on → REMOTE code {r,n,t} (NAT traversal)
|
|
11
|
-
* - else → none (only reachable locally)
|
|
12
|
-
*/
|
|
13
1
|
export function encodeConnectionCode(d) {
|
|
14
2
|
const lean = {};
|
|
15
3
|
if (d.host)
|
|
@@ -1,22 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/codec.ts — frame (de)serialization for the L2 protocol.
|
|
3
|
-
*
|
|
4
|
-
* The transport layer (L1) moves opaque frames; this module is the only place
|
|
5
|
-
* that knows how a {@link ServerMessage} / {@link ClientMessage} becomes a frame
|
|
6
|
-
* and back. Today that is newline-free JSON text. The indirection is deliberate:
|
|
7
|
-
* swapping to a binary codec (CBOR / protobuf) later is a change here alone, with
|
|
8
|
-
* the wire-format tag negotiated in the handshake — no transport or host edits.
|
|
9
|
-
*/
|
|
10
1
|
export const DEFAULT_WIRE_FORMAT = 'json';
|
|
11
|
-
/** Encode a host→client message into a transport frame. */
|
|
12
2
|
export function encodeServer(msg, _fmt = DEFAULT_WIRE_FORMAT) {
|
|
13
3
|
return JSON.stringify(msg);
|
|
14
4
|
}
|
|
15
|
-
/** Encode a client→host message into a transport frame. */
|
|
16
5
|
export function encodeClient(msg, _fmt = DEFAULT_WIRE_FORMAT) {
|
|
17
6
|
return JSON.stringify(msg);
|
|
18
7
|
}
|
|
19
|
-
/** Coerce a transport frame (string / Buffer / ArrayBuffer) into text. */
|
|
20
8
|
function frameToText(raw) {
|
|
21
9
|
if (typeof raw === 'string')
|
|
22
10
|
return raw;
|
|
@@ -24,7 +12,6 @@ function frameToText(raw) {
|
|
|
24
12
|
return raw.toString('utf8');
|
|
25
13
|
return Buffer.from(new Uint8Array(raw)).toString('utf8');
|
|
26
14
|
}
|
|
27
|
-
/** Decode a client→host frame. Returns null on malformed input (never throws). */
|
|
28
15
|
export function decodeClient(raw, _fmt = DEFAULT_WIRE_FORMAT) {
|
|
29
16
|
try {
|
|
30
17
|
const value = JSON.parse(frameToText(raw));
|
|
@@ -36,7 +23,6 @@ export function decodeClient(raw, _fmt = DEFAULT_WIRE_FORMAT) {
|
|
|
36
23
|
return null;
|
|
37
24
|
}
|
|
38
25
|
}
|
|
39
|
-
/** Decode a host→client frame. Returns null on malformed input (never throws). */
|
|
40
26
|
export function decodeServer(raw, _fmt = DEFAULT_WIRE_FORMAT) {
|
|
41
27
|
try {
|
|
42
28
|
const value = JSON.parse(frameToText(raw));
|
package/dist/pikichannel/host.js
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/host.ts — the host SDK (transport- and agent-agnostic).
|
|
3
|
-
*
|
|
4
|
-
* `PikichannelHost` speaks the L2 protocol over any {@link ChannelConnection},
|
|
5
|
-
* driven by a {@link SessionSource} the embedder supplies. The host knows
|
|
6
|
-
* nothing about pikiloom: porting pikichannel to another agent runtime is a
|
|
7
|
-
* matter of implementing SessionSource (see adapter-pikiloom.ts).
|
|
8
|
-
*
|
|
9
|
-
* It owns three cross-cutting concerns the protocol requires:
|
|
10
|
-
* - Auth: a peer must pass `authenticate(token, remote)` (loopback exempt by
|
|
11
|
-
* policy) before ANY session data or control is processed.
|
|
12
|
-
* - Delta: it holds the latest full snapshot per session and emits a `full`
|
|
13
|
-
* patch on (re)subscribe / resync, deltas thereafter — O(n) per stream.
|
|
14
|
-
* - Fan-out: one delta per update is broadcast to every caught-up subscriber;
|
|
15
|
-
* a fresh subscriber gets a `full` so it shares the same baseline.
|
|
16
|
-
*/
|
|
17
1
|
import { PROTOCOL_VERSION, diffSnapshot, } from './protocol.js';
|
|
18
2
|
import { encodeServer, decodeClient } from './codec.js';
|
|
19
3
|
const SUBSCRIBE_ALL = '*';
|
|
@@ -24,7 +8,6 @@ export class PikichannelHost {
|
|
|
24
8
|
peers = new Set();
|
|
25
9
|
unsubscribers = [];
|
|
26
10
|
started = false;
|
|
27
|
-
/** Latest full snapshot per session — the delta baseline shared by all peers. */
|
|
28
11
|
lastFull = new Map();
|
|
29
12
|
lastSeq = new Map();
|
|
30
13
|
constructor(source, authenticate = () => true, log = () => { }) {
|
|
@@ -32,7 +15,6 @@ export class PikichannelHost {
|
|
|
32
15
|
this.authenticate = authenticate;
|
|
33
16
|
this.log = log;
|
|
34
17
|
}
|
|
35
|
-
/** Wire host-level subscriptions to the source (once). */
|
|
36
18
|
start() {
|
|
37
19
|
if (this.started)
|
|
38
20
|
return;
|
|
@@ -45,20 +27,19 @@ export class PikichannelHost {
|
|
|
45
27
|
try {
|
|
46
28
|
u();
|
|
47
29
|
}
|
|
48
|
-
catch {
|
|
30
|
+
catch { }
|
|
49
31
|
}
|
|
50
32
|
for (const peer of this.peers) {
|
|
51
33
|
try {
|
|
52
34
|
peer.conn.close();
|
|
53
35
|
}
|
|
54
|
-
catch {
|
|
36
|
+
catch { }
|
|
55
37
|
}
|
|
56
38
|
this.peers.clear();
|
|
57
39
|
this.lastFull.clear();
|
|
58
40
|
this.lastSeq.clear();
|
|
59
41
|
this.started = false;
|
|
60
42
|
}
|
|
61
|
-
/** Adopt a freshly-established connection from any transport binding. */
|
|
62
43
|
handleConnection(conn) {
|
|
63
44
|
if (!this.started)
|
|
64
45
|
this.start();
|
|
@@ -78,7 +59,6 @@ export class PikichannelHost {
|
|
|
78
59
|
this.log(`peer disconnected id=${conn.id} via=${conn.kind}`);
|
|
79
60
|
});
|
|
80
61
|
}
|
|
81
|
-
// -- delta baseline ------------------------------------------------------
|
|
82
62
|
onSourceUpdate(sessionKey, full, seq) {
|
|
83
63
|
const prev = this.lastFull.get(sessionKey);
|
|
84
64
|
const patch = prev ? diffSnapshot(prev, full) : { full };
|
|
@@ -90,7 +70,6 @@ export class PikichannelHost {
|
|
|
90
70
|
this.send(peer, frame);
|
|
91
71
|
}
|
|
92
72
|
}
|
|
93
|
-
/** Send the current full snapshot for a session to one peer (subscribe / resync). */
|
|
94
73
|
sendFull(peer, sessionKey) {
|
|
95
74
|
let full = this.lastFull.get(sessionKey);
|
|
96
75
|
let seq = this.lastSeq.get(sessionKey);
|
|
@@ -105,9 +84,7 @@ export class PikichannelHost {
|
|
|
105
84
|
}
|
|
106
85
|
this.send(peer, { type: 'session', sessionKey, seq: seq || 0, patch: { full } });
|
|
107
86
|
}
|
|
108
|
-
// -- inbound -------------------------------------------------------------
|
|
109
87
|
async handleClientMessage(peer, msg) {
|
|
110
|
-
// Ping is always allowed (RTT / keepalive). Everything else is gated on auth.
|
|
111
88
|
if (msg.type === 'ping') {
|
|
112
89
|
this.send(peer, { type: 'pong', t: msg.t });
|
|
113
90
|
return;
|
|
@@ -145,7 +122,7 @@ export class PikichannelHost {
|
|
|
145
122
|
}
|
|
146
123
|
switch (msg.type) {
|
|
147
124
|
case 'hello':
|
|
148
|
-
return;
|
|
125
|
+
return;
|
|
149
126
|
case 'subscribe': {
|
|
150
127
|
peer.subs.add(msg.sessionKey);
|
|
151
128
|
if (msg.sessionKey === SUBSCRIBE_ALL) {
|
|
@@ -207,9 +184,6 @@ export class PikichannelHost {
|
|
|
207
184
|
}
|
|
208
185
|
case 'request': {
|
|
209
186
|
const id = msg.id;
|
|
210
|
-
// Only the management API is tunnelable — never static, the SPA shell,
|
|
211
|
-
// or loopback-gated endpoints (e.g. /pikichannel/pair would otherwise
|
|
212
|
-
// leak the token to a remote peer via the host's own fetch).
|
|
213
187
|
if (!msg.path || !msg.path.startsWith('/api/')) {
|
|
214
188
|
this.send(peer, { type: 'response', id, status: 403, error: 'only /api/* is tunnelable' });
|
|
215
189
|
return;
|
|
@@ -229,7 +203,6 @@ export class PikichannelHost {
|
|
|
229
203
|
}
|
|
230
204
|
}
|
|
231
205
|
}
|
|
232
|
-
// -- outbound ------------------------------------------------------------
|
|
233
206
|
broadcast(frame, authedOnly = false) {
|
|
234
207
|
for (const peer of this.peers)
|
|
235
208
|
if (!authedOnly || peer.authed)
|
|
@@ -241,11 +214,9 @@ export class PikichannelHost {
|
|
|
241
214
|
try {
|
|
242
215
|
peer.conn.send(encodeServer(frame));
|
|
243
216
|
}
|
|
244
|
-
catch {
|
|
217
|
+
catch { }
|
|
245
218
|
}
|
|
246
|
-
/** Number of live peers (for status / metrics). */
|
|
247
219
|
get peerCount() { return this.peers.size; }
|
|
248
|
-
/** Number of authenticated peers. */
|
|
249
220
|
get authedPeerCount() { let n = 0; for (const p of this.peers)
|
|
250
221
|
if (p.authed)
|
|
251
222
|
n++; return n; }
|
|
@@ -1,58 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/protocol.ts — THE universal, agent-agnostic wire protocol (L2).
|
|
3
|
-
*
|
|
4
|
-
* This file is the single source of truth for the pikichannel contract. It is
|
|
5
|
-
* deliberately free of any pikiloom-internal or agent-specific type: a client
|
|
6
|
-
* SDK on any platform (Web / iOS / Android) only needs these shapes to speak to
|
|
7
|
-
* any pikichannel host. The browser SDK in `web/pikichannel-sdk.js` mirrors the
|
|
8
|
-
* string literals and shapes documented here — keep the two in lockstep.
|
|
9
|
-
*
|
|
10
|
-
* Layering:
|
|
11
|
-
* transport (L1) — moves opaque frames (a byte/string pipe): WebSocket, WebRTC
|
|
12
|
-
* datachannel, relay tunnel. Pluggable; see transport.ts.
|
|
13
|
-
* protocol (L2) — THIS file. The session/event semantics framed over L1.
|
|
14
|
-
*
|
|
15
|
-
* Design rules:
|
|
16
|
-
* - Every message is a flat discriminated union on `type`. No nested envelopes
|
|
17
|
-
* so clients can `switch (msg.type)` directly.
|
|
18
|
-
* - The host→client `session` event carries a {@link SnapshotPatch}: a `full`
|
|
19
|
-
* snapshot on first send / resync, then deltas (append-only suffixes for the
|
|
20
|
-
* unbounded text/reasoning fields + changed scalar/struct fields). The host
|
|
21
|
-
* remains the single source of truth — it holds the full snapshot and emits
|
|
22
|
-
* `full` on (re)subscribe or whenever a client reports a `seq` gap, so the
|
|
23
|
-
* wire is O(n) for a stream of total size n instead of O(n²).
|
|
24
|
-
* - `seq` is monotonic per session for ordering / gap detection.
|
|
25
|
-
* - Auth: a client authenticates in `hello` (token); loopback peers are exempt
|
|
26
|
-
* by host policy. No session data or control is processed before auth.
|
|
27
|
-
* - Versioned via PROTOCOL_VERSION; the handshake negotiates it.
|
|
28
|
-
*
|
|
29
|
-
* Extensibility (how this grows without forking the wire):
|
|
30
|
-
* - ADDITIVE by default: new optional fields on existing messages and brand-new
|
|
31
|
-
* `type`s are backward-compatible. Both host and client MUST ignore unknown
|
|
32
|
-
* message `type`s and unknown fields (the switch statements fall through, not
|
|
33
|
-
* throw) — so an old peer talks to a new peer safely.
|
|
34
|
-
* - Optional features are negotiated via `HostCapability` (advertised in
|
|
35
|
-
* `welcome`), not by version bumps — a client checks `host.capabilities`
|
|
36
|
-
* before using a non-core verb.
|
|
37
|
-
* - PROTOCOL_VERSION bumps only for a BREAKING change; `hello.v` lets either
|
|
38
|
-
* side detect a mismatch and degrade.
|
|
39
|
-
* - The transport (L1) is fully decoupled: a new binding (relay, WebTransport,
|
|
40
|
-
* QUIC) implements ChannelConnection and carries this protocol unchanged.
|
|
41
|
-
*/
|
|
42
1
|
export const PROTOCOL_VERSION = 1;
|
|
43
|
-
/** An empty baseline snapshot, used as the starting point before any patch. */
|
|
44
2
|
export function emptySnapshot() {
|
|
45
3
|
return { phase: 'idle', updatedAt: 0 };
|
|
46
4
|
}
|
|
47
5
|
const APPEND_FIELDS = ['text', 'reasoning'];
|
|
48
6
|
const STRUCT_FIELDS = ['plan', 'toolCalls', 'subAgents', 'usage', 'artifacts', 'interactions', 'queued'];
|
|
49
7
|
const SCALAR_FIELDS = ['phase', 'taskId', 'agent', 'model', 'effort', 'prompt', 'activity', 'error', 'incomplete', 'startedAt', 'updatedAt'];
|
|
50
|
-
/**
|
|
51
|
-
* Diff two full snapshots into a minimal patch. text/reasoning are encoded as
|
|
52
|
-
* append suffixes when the next value extends the prev (the streaming common
|
|
53
|
-
* case); any non-extension falls back to a `set`. Bounded fields are compared by
|
|
54
|
-
* value and sent whole when changed.
|
|
55
|
-
*/
|
|
56
8
|
export function diffSnapshot(prev, next) {
|
|
57
9
|
const patch = {};
|
|
58
10
|
let set;
|
|
@@ -84,7 +36,6 @@ export function diffSnapshot(prev, next) {
|
|
|
84
36
|
patch.set = set;
|
|
85
37
|
return patch;
|
|
86
38
|
}
|
|
87
|
-
/** Apply a patch onto a prior snapshot, returning the new cumulative snapshot. */
|
|
88
39
|
export function applySnapshotPatch(prev, patch) {
|
|
89
40
|
if (patch.full)
|
|
90
41
|
return patch.full;
|
|
@@ -97,9 +48,6 @@ export function applySnapshotPatch(prev, patch) {
|
|
|
97
48
|
Object.assign(next, patch.set);
|
|
98
49
|
return next;
|
|
99
50
|
}
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
// Type guards (host-side convenience)
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
51
|
export function isClientMessage(value) {
|
|
104
52
|
return !!value && typeof value === 'object' && typeof value.type === 'string';
|
|
105
53
|
}
|