@coda-console/plugin-check 0.4.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -17
- package/bin/check.mjs +4 -3
- package/check/agent.mjs +29 -19
- package/check/coda/builtin.json +1273 -0
- package/check/coda/components/plugins/agent-plugins.mjs +88 -0
- package/check/coda/components/plugins/plugin-extensions.mjs +171 -0
- package/check/coda/components/plugins/plugin-rules.mjs +819 -0
- package/check/coda/components/plugins/plugin-source-rules.mjs +362 -0
- package/check/coda/components/plugins/skill-document.mjs +35 -0
- package/check/coda/components/prompt-recovery/prompt-recovery.mjs +56 -0
- package/check/coda/components/session-config/commands.mjs +111 -0
- package/check/coda/components/workflows/executor-labels.mjs +12 -0
- package/check/coda/contract.json +29 -0
- package/check/coda/domain/agent-protocol.mjs +45 -0
- package/check/coda/domain/duration.mjs +11 -0
- package/check/coda/domain/errors.mjs +11 -0
- package/check/coda/domain/values.mjs +39 -0
- package/check/coda/kernel/agent-failures.mjs +16 -0
- package/check/coda/resources/plugins/resources/panels/command.mjs +46 -0
- package/check/coda/resources/plugins/resources/panels/panels.mjs +225 -0
- package/check/coda/resources/plugins/resources/panels/rules.mjs +242 -0
- package/check/coda/resources/plugins/resources/panels/sign-in.mjs +131 -0
- package/check/index.mjs +3 -4
- package/check/leftovers.mjs +9 -3
- package/check/manifest.mjs +198 -434
- package/check/panel.mjs +49 -100
- package/check/waits.mjs +5 -4
- package/package.json +4 -2
- package/check/coda.mjs +0 -165
package/check/panel.mjs
CHANGED
|
@@ -1,114 +1,63 @@
|
|
|
1
|
-
// A panel at the handshake level:
|
|
2
|
-
//
|
|
1
|
+
// A panel at the handshake level: read once by Coda's own resources module (check/coda/resources/plugins/resources/panels/,
|
|
2
|
+
// Panels), given what Coda's plugin host gives it (components/plugins/plugin-host.mjs): the installed plugins as it watches
|
|
3
|
+
// them, here the one checked, and each one's environment, as Coda gives every process of the plugin's.
|
|
3
4
|
// Its actions are not run: a button may change things (switch an account, spend a reset), and only a person presses it.
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
5
|
import { dirname } from 'node:path';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { cleared } from './coda/components/plugins/plugin-rules.mjs';
|
|
7
|
+
import { ended, runCommand } from './coda/resources/plugins/resources/panels/command.mjs';
|
|
8
|
+
import { Panels } from './coda/resources/plugins/resources/panels/panels.mjs';
|
|
9
|
+
import { OUTPUT_LIMIT, panelItems } from './coda/resources/plugins/resources/panels/rules.mjs';
|
|
10
|
+
import { closing, temporary } from './leftovers.mjs';
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const object = (value, where) => check(value && typeof value === 'object' && !Array.isArray(value), `${where} 须为对象`);
|
|
13
|
-
const keys = (value, allowed, where) => {
|
|
14
|
-
const extra = Object.keys(value).filter(k => !allowed.includes(k));
|
|
15
|
-
check(!extra.length, `${where} 里有 Coda 不支持的键:${extra.join('、')}`);
|
|
16
|
-
};
|
|
17
|
-
const text = (value, max, where, { need = true, line = true } = {}) => {
|
|
18
|
-
if (value === undefined && !need) return;
|
|
19
|
-
check(typeof value === 'string' && value.trim() && [...value].length <= max && !value.includes('\0') && (!line || !/[\r\n]/.test(value)),
|
|
20
|
-
`${where} 须为不超过 ${max} 字的${line ? '一行' : ''}文本`);
|
|
21
|
-
};
|
|
12
|
+
// The session a process would belong to: the host's belong to none.
|
|
13
|
+
const THREAD_ENV = 'CODA_THREAD_ID';
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
switch (raw.type) {
|
|
29
|
-
case 'card':
|
|
30
|
-
keys(raw, ['type', 'title', 'subtitle', 'identity', 'items'], where);
|
|
31
|
-
check(Array.isArray(raw.items) && raw.items.length <= L.cardItems, `${where} 的 items 须为不超过 ${L.cardItems} 项的数组`);
|
|
32
|
-
text(raw.title, L.label, `${where} 的 title`); text(raw.subtitle, L.label, `${where} 的 subtitle`, { need: false });
|
|
33
|
-
text(raw.identity, L.identity, `${where} 的 identity`, { need: false });
|
|
34
|
-
return [raw.type, ...raw.items.flatMap((r, i) => item(r, `${where}.items[${i}]`, actions, true))];
|
|
35
|
-
case 'meter':
|
|
36
|
-
keys(raw, ['type', 'label', 'value', 'fraction', 'note', 'time', 'timeLabel'], where);
|
|
37
|
-
check(raw.fraction === undefined || raw.fraction === null || (typeof raw.fraction === 'number' && raw.fraction >= 0 && raw.fraction <= 1),
|
|
38
|
-
`${where} 的 fraction 须在 0–1 之间(还剩多少),不知道时不写`);
|
|
39
|
-
check(raw.time === undefined || (typeof raw.time === 'string' && Number.isFinite(Date.parse(raw.time))), `${where} 的 time 须为 ISO 8601 时间`);
|
|
40
|
-
text(raw.label, L.label, `${where} 的 label`);
|
|
41
|
-
for (const key of ['value', 'note', 'timeLabel']) text(raw[key], L.label, `${where} 的 ${key}`, { need: false });
|
|
42
|
-
return [raw.type];
|
|
43
|
-
case 'stat':
|
|
44
|
-
keys(raw, ['type', 'label', 'value'], where);
|
|
45
|
-
text(raw.label, L.label, `${where} 的 label`); text(raw.value, L.label, `${where} 的 value`);
|
|
46
|
-
return [raw.type];
|
|
47
|
-
case 'markdown':
|
|
48
|
-
keys(raw, ['type', 'text'], where);
|
|
49
|
-
text(raw.text, L.markdown, `${where} 的 text`, { line: false });
|
|
50
|
-
return [raw.type];
|
|
51
|
-
case 'error':
|
|
52
|
-
keys(raw, ['type', 'text'], where);
|
|
53
|
-
text(raw.text, L.error, `${where} 的 text`, { line: false });
|
|
54
|
-
return [raw.type];
|
|
55
|
-
case 'button':
|
|
56
|
-
keys(raw, ['type', 'label', 'action', 'args', 'disabled', 'confirm'], where);
|
|
57
|
-
check(typeof raw.action === 'string' && Object.hasOwn(actions, raw.action), `${where} 的 action 须是面板声明的动作:${raw.action}`);
|
|
58
|
-
check(raw.args === undefined || (raw.args && typeof raw.args === 'object' && !Array.isArray(raw.args) && JSON.stringify(raw.args).length <= ARGS_LIMIT),
|
|
59
|
-
`${where} 的 args 须为不超过 ${ARGS_LIMIT} 字节的对象`);
|
|
60
|
-
check(raw.disabled === undefined || typeof raw.disabled === 'boolean', `${where} 的 disabled 须为开或关`);
|
|
61
|
-
text(raw.label, L.label, `${where} 的 label`); text(raw.confirm, L.confirm, `${where} 的 confirm`, { need: false, line: false });
|
|
62
|
-
return [raw.type];
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// The item types a reading holds, cards' items included; throws Invalid with where it went wrong.
|
|
66
|
-
export function panelItems(output, panel) {
|
|
67
|
-
check(Buffer.byteLength(output) <= OUTPUT_LIMIT, `输出超过 ${OUTPUT_LIMIT / 1024} KiB`);
|
|
68
|
-
let value;
|
|
69
|
-
try { value = JSON.parse(output); } catch { check(false, '输出不是 JSON'); }
|
|
70
|
-
object(value, '输出');
|
|
71
|
-
keys(value, ['items'], '输出');
|
|
72
|
-
check(Array.isArray(value.items) && value.items.length <= ITEM_LIMITS.items, `输出的 items 须为不超过 ${ITEM_LIMITS.items} 项的数组`);
|
|
73
|
-
return value.items.flatMap((r, i) => item(r, `items[${i}]`, panel.actions, false));
|
|
74
|
-
}
|
|
15
|
+
// The item types a reading holds, cards' items included.
|
|
16
|
+
const typesOf = items => items.flatMap(item => item.type === 'card' ? ['card', ...item.items.map(i => i.type)] : [item.type]);
|
|
17
|
+
// What a panel's command printed as Coda takes it (panelItems), by type; throws PanelError with where it went wrong. For the
|
|
18
|
+
// tests of a plugin's panel.
|
|
19
|
+
export const panelTypes = (output, panel, views = []) => typesOf(panelItems(output, panel, views));
|
|
75
20
|
|
|
76
|
-
// `
|
|
77
|
-
//
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
const child = spawn('/bin/sh', ['-c', command], { cwd: tmpdir(), env, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
81
|
-
const ended = group(child.pid), out = [], err = [];
|
|
82
|
-
let size = 0, overflow = false, timedOut = false;
|
|
83
|
-
const kill = () => { try { process.kill(-child.pid, 'SIGKILL'); } catch {} ended(); };
|
|
84
|
-
const timer = setTimeout(() => { timedOut = true; kill(); }, limit);
|
|
85
|
-
child.stdout.on('data', chunk => { size += chunk.length; if (size > OUTPUT_LIMIT) { overflow = true; kill(); } else out.push(chunk); });
|
|
86
|
-
child.stderr.on('data', chunk => err.push(chunk));
|
|
87
|
-
child.on('error', error => { clearTimeout(timer); resolve({ code: null, error: error.message, stdout: '', stderr: '' }); });
|
|
88
|
-
child.on('close', code => { clearTimeout(timer); kill();
|
|
89
|
-
resolve({ code, stdout: Buffer.concat(out).toString('utf8'), stderr: Buffer.concat(err).toString('utf8'), overflow, timedOut }); });
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
const lastLine = text => text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').split('\n').map(l => l.trim()).filter(Boolean).at(-1)?.slice(0, 200) ?? '';
|
|
93
|
-
|
|
94
|
-
export async function checkPanel({ panel, pluginDir, settings, dependencies = {}, say, limit = PANEL_READ_LIMIT }) {
|
|
21
|
+
// `checked`: the plugin as the manifest level read it (manifest.mjs checkManifest). `limit`: how long a reading may take, as
|
|
22
|
+
// Panels has it unless given.
|
|
23
|
+
export async function checkPanel({ panel, checked, say, limit }) {
|
|
24
|
+
const { plugin } = checked;
|
|
95
25
|
// The plugin's state directory, which Coda keeps for it; here a temporary one, gone however the checker ends.
|
|
96
26
|
const state = temporary('coda-plugin-check-state-');
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
27
|
+
// The host's environment of a plugin (env): its processes', the backend's `node` first on the PATH, and the concurrency
|
|
28
|
+
// groups in use as it is asked for (none here).
|
|
29
|
+
const environment = async () => {
|
|
30
|
+
const env = { ...cleared(process.env), ...checked.env(state.dir) };
|
|
100
31
|
delete env[THREAD_ENV];
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
32
|
+
env.PATH = [dirname(process.execPath), env.PATH].filter(Boolean).join(':');
|
|
33
|
+
env.CODA_GROUPS_STATE = JSON.stringify({ now: Date.now(), groups: [] });
|
|
34
|
+
return env;
|
|
35
|
+
};
|
|
36
|
+
// The installed plugins as a module watches them (components/plugins/plugins.mjs offered).
|
|
37
|
+
const offered = { id: plugin.id, name: plugin.displayName, version: plugin.version, source: plugin.source, views: plugin.views.map(v => v.id), contributes: plugin.offers ?? {} };
|
|
38
|
+
const coda = { plugin: { state: null }, environment,
|
|
39
|
+
watch: async (name, told) => { told({ type: 'ADDED', id: offered.id, object: offered }); return { dispose() {} }; } };
|
|
40
|
+
// The command's own result, beside what the page shows of it: whether it ended well and what it printed was not a panel's.
|
|
41
|
+
let result = null;
|
|
42
|
+
const panels = new Panels(coda, { run: async (command, options) => (result = await runCommand(command, options)), ...(limit ? { readTimeoutMs: limit } : {}) });
|
|
43
|
+
const done = closing(() => panels.close());
|
|
44
|
+
try {
|
|
45
|
+
await panels.start();
|
|
46
|
+
const started = Date.now(), reading = await panels.read(`${plugin.id}/${panel.id}`), took = ((Date.now() - started) / 1000).toFixed(1);
|
|
47
|
+
if (reading.error && result && ended(result)) return say.fail('panel.output', `输出不合面板格式:资源页显示「${reading.error}」`);
|
|
48
|
+
if (reading.error) {
|
|
49
|
+
const why = !result ? '没有运行' : result.timedOut ? `${panels.readTimeoutMs / 1000} 秒没读完,Coda 停下这次读取` : result.overflow ? `标准输出超过 ${OUTPUT_LIMIT / 1024} KiB,Coda 停下这次读取`
|
|
50
|
+
: result.error ? '命令起不来' : `退出码 ${result.code}`;
|
|
51
|
+
return say.fail('panel.read', `读取没成功(${why}):资源页显示「${reading.error}」`);
|
|
52
|
+
}
|
|
53
|
+
const types = typesOf(reading.items);
|
|
107
54
|
const counts = Object.entries(Object.groupBy(types, t => t)).map(([t, list]) => `${t} ×${list.length}`).join(',');
|
|
108
|
-
if (!types.length) say.info('panel.read', `读出空的 items(${
|
|
109
|
-
else say.pass('panel.read', `读取 ${
|
|
55
|
+
if (!types.length) say.info('panel.read', `读出空的 items(${took} 秒):资源页不显示它${panel.signIn ? ',它出现在「添加资源」里' : ''}`);
|
|
56
|
+
else say.pass('panel.read', `读取 ${took} 秒,输出合面板格式:${counts}`);
|
|
110
57
|
if (Object.keys(panel.actions).length) say.skip('panel.actions', `动作 ${Object.keys(panel.actions).join('、')} 不运行:按钮可能改动东西,只由人点`);
|
|
111
58
|
} finally {
|
|
59
|
+
panels.close();
|
|
60
|
+
done();
|
|
112
61
|
state.remove();
|
|
113
62
|
}
|
|
114
63
|
}
|
package/check/waits.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// `_coda/wait` as Coda takes it: validWait first (check/coda.mjs
|
|
2
|
-
// 「Agent 活动」. Where Coda drops a report without a word, the checker fails it: an agent that relies
|
|
3
|
-
|
|
1
|
+
// `_coda/wait` as Coda takes it: Coda's validWait first (check/coda/domain/agent-protocol.mjs), then the rules of
|
|
2
|
+
// docs/agent-protocol.md「Agent 活动」. Where Coda drops a report without a word, the checker fails it: an agent that relies
|
|
3
|
+
// on it is not heard.
|
|
4
|
+
import { validWait, WAIT_STATES } from './coda/domain/agent-protocol.mjs';
|
|
4
5
|
|
|
5
6
|
export const TERMINAL = new Set(['done', 'failed', 'cancelled']);
|
|
6
7
|
const LIMITS = { sessionId: 200, waitId: 200, kind: 50, title: 500, detail: 4000, childSessionId: 200, model: 200, outputFile: 4096, toolCallId: 200 };
|
|
7
8
|
|
|
8
|
-
// The first part of validWait a report fails, in words.
|
|
9
|
+
// The first part of validWait a report fails, in words (validWait decides; this only says why).
|
|
9
10
|
export function whyInvalid(p) {
|
|
10
11
|
if (!p || typeof p !== 'object' || Array.isArray(p)) return 'params 须为对象';
|
|
11
12
|
for (const key of ['sessionId', 'waitId']) if (typeof p[key] !== 'string' || !p[key]) return `缺 ${key}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coda-console/plugin-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Coda 插件的契约检查:扮演 Coda 读清单、启动 Agent 讲 ACP 与 _coda/wait、读面板。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"test": "node --test 'test/**/*.test.mjs'"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@
|
|
25
|
+
"@0dep/piso": "5.2.0",
|
|
26
|
+
"@agentclientprotocol/sdk": "1.4.0",
|
|
27
|
+
"yaml": "2.9.1"
|
|
26
28
|
},
|
|
27
29
|
"publishConfig": {
|
|
28
30
|
"access": "public"
|
package/check/coda.mjs
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
// What the checker copies from Coda, in one place. Coda is the authority: its「读取来源」(PluginSources.readSource) reads a
|
|
2
|
-
// plugin with the same rules, and wins where the two differ. Each rule names where it came from in the Coda repository at
|
|
3
|
-
// CODA_COMMIT; a Coda PR that changes one of them changes this file too (docs/agent-protocol.md here is synced from the same
|
|
4
|
-
// commit, and test/protocol-copy.test.mjs keeps the two together).
|
|
5
|
-
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
|
|
6
|
-
|
|
7
|
-
// The Coda commit these rules were copied from (an empty current mode is none chosen).
|
|
8
|
-
export const CODA_COMMIT = 'a52459b1f950957923a6e187075ba981263279f4';
|
|
9
|
-
// The ACP version Coda speaks: the SDK's (backend/package.json pins @agentclientprotocol/sdk 1.4.0, as package.json here).
|
|
10
|
-
export const ACP_PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
11
|
-
|
|
12
|
-
// ---- ACP (backend/src/adapters/acp.mjs) ----
|
|
13
|
-
// `initialize` as Coda sends it (AcpSession.initialize): no clientInfo, these capabilities.
|
|
14
|
-
export const clientCapabilities = () => ({
|
|
15
|
-
elicitation: { form: {}, url: {} },
|
|
16
|
-
session: { compaction: {}, configOptions: { boolean: {} } },
|
|
17
|
-
_meta: { coda: { waits: {} } },
|
|
18
|
-
});
|
|
19
|
-
// Session instructions (AcpSession.takesInstructions, initialize): ACP's unstable `systemPrompt`, sent only to an agent that
|
|
20
|
-
// declares it, the same on `session/new` and `session/load`, in `append` mode, one text block (docs/agent-protocol.md「会话指令」).
|
|
21
|
-
export const takesInstructions = init => init?.agentCapabilities?.sessionCapabilities?.systemPrompt != null;
|
|
22
|
-
export const systemPrompt = text => ({ mode: 'append', content: [{ type: 'text', text }] });
|
|
23
|
-
// Steering (AcpSession.supportsSteering, steer; docs/agent-protocol.md「插话」): declared beside the capabilities; sent only
|
|
24
|
-
// while a turn of Coda's own is under way. `injected` joined it; `promptRequired` was not taken, the turn having ended;
|
|
25
|
-
// anything else Coda records as unknown.
|
|
26
|
-
export const STEERING = '_session/steering';
|
|
27
|
-
export const supportsSteering = init => init?._meta?.steering?.supported === true;
|
|
28
|
-
export const steeringParams = (sessionId, text) => ({ sessionId, prompt: [{ type: 'text', text }], _meta: { steering: { idleBehavior: 'promptRequired' } } });
|
|
29
|
-
export const STEERING_OUTCOMES = ['injected', 'promptRequired'];
|
|
30
|
-
// Failure categories and presentation (backend/src/domain/agent-failures.mjs).
|
|
31
|
-
export function agentFailure(error) {
|
|
32
|
-
const failure = error?.data?._meta?.coda?.failure;
|
|
33
|
-
if (!record(failure)) return null;
|
|
34
|
-
return failure.kind === 'quota_exhausted' ||
|
|
35
|
-
(failure.kind === 'service_unavailable' && ['accepted', 'not_accepted'].includes(failure.input)) ? failure : null;
|
|
36
|
-
}
|
|
37
|
-
export function agentErrorMessage(error) {
|
|
38
|
-
const message = typeof error?.message === 'string' && error.message ? error.message : 'Agent 返回错误';
|
|
39
|
-
const detail = typeof error?.data?.message === 'string' ? error.data.message.trim() : '';
|
|
40
|
-
const text = detail && !message.includes(detail) ? `${message}\n\n${detail}` : message;
|
|
41
|
-
return agentFailure(error)?.kind === 'quota_exhausted' && !text.startsWith('额度不足') ? `额度不足\n\n${text}` : text;
|
|
42
|
-
}
|
|
43
|
-
// Service retry policy (backend/src/application/prompt-recovery.mjs).
|
|
44
|
-
export function serviceFailure(error) {
|
|
45
|
-
const failure = agentFailure(error);
|
|
46
|
-
return failure?.kind === 'service_unavailable' ? failure : null;
|
|
47
|
-
}
|
|
48
|
-
// The former capacity/model-unavailable policy, shared by every agent that reports a service failure.
|
|
49
|
-
export const RETRY_DELAYS = [15_000, 30_000, 60_000];
|
|
50
|
-
export const continuationPrompt = '继续完成当前任务,沿用已有上下文和已完成的工作,从尚未完成的部分接着执行。';
|
|
51
|
-
|
|
52
|
-
// The agent refuses until it is signed in: ACP's auth_required (signedOut).
|
|
53
|
-
export const SIGNED_OUT = -32000;
|
|
54
|
-
// What Coda answers an agent request it has no handler for (the SDK's): Coda handles only approvals and forms.
|
|
55
|
-
export const METHOD_NOT_FOUND = -32601;
|
|
56
|
-
// What an agent may say of a wait of its own (validWait, verbatim); anything else Coda drops unread.
|
|
57
|
-
const record = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
58
|
-
export const WAIT_STATES = new Set(['waiting', 'handling', 'done', 'failed', 'cancelled']);
|
|
59
|
-
const fits = (value, limit) => value === undefined || (typeof value === 'string' && value.length <= limit);
|
|
60
|
-
export const validWait = p => record(p) && typeof p.sessionId === 'string' && p.sessionId.length > 0 && p.sessionId.length <= 200
|
|
61
|
-
&& typeof p.waitId === 'string' && p.waitId.length > 0 && p.waitId.length <= 200 && WAIT_STATES.has(p.state)
|
|
62
|
-
&& fits(p.kind, 50) && fits(p.title, 500) && fits(p.detail, 4000) && fits(p.childSessionId, 200) && fits(p.model, 200) && fits(p.outputFile, 4096)
|
|
63
|
-
&& fits(p.toolCallId, 200)
|
|
64
|
-
&& [p.stoppable, p.recurring].every(flag => flag === undefined || typeof flag === 'boolean')
|
|
65
|
-
&& (p.at === undefined || (typeof p.at === 'string' && Number.isFinite(Date.parse(p.at))));
|
|
66
|
-
// The model a session runs with, as Coda reads it (catalog().current.model): every turn Coda configures first, and refuses
|
|
67
|
-
// a session whose agent names none (configure: 「适配器未报告实际模型」).
|
|
68
|
-
export function currentModel(session) {
|
|
69
|
-
const model = session?.configOptions?.find?.(x => x?.category === 'model');
|
|
70
|
-
return model?.currentValue ?? session?.models?.currentModelId;
|
|
71
|
-
}
|
|
72
|
-
// docs/agent-protocol.md「Agent 活动」规则 3: a turn the user stops ends within this, or Coda closes the agent's process group.
|
|
73
|
-
export const CANCEL_LIMIT = 30_000;
|
|
74
|
-
// adapters/session-host.mjs `close`: Coda stops an agent with SIGTERM to its process group, and SIGKILL this long after.
|
|
75
|
-
export const TERM_GRACE = 800;
|
|
76
|
-
// adapters/acp.mjs `open` and application/option-cache.mjs: opening a session (initialize, session/new or session/load) may take this.
|
|
77
|
-
export const OPEN_LIMIT = 60_000;
|
|
78
|
-
|
|
79
|
-
// ---- What every process Coda runs for a plugin is given (backend/src/components/plugins/plugin-rules.mjs pluginEnv, cleared) ----
|
|
80
|
-
// Its agents', its panels' and the commands it brings alike: Coda's own Node, the plugin's directory, its state, its settings,
|
|
81
|
-
// what is plugged into its points (none here: the checker installs nothing beside it), where the plugins it depends on are
|
|
82
|
-
// (`dependencies`, by ID), and CODA_ARBITER only for an instance's own backend, which the checker is not. What a process
|
|
83
|
-
// inherits is cleared of them first, those it is given only sometimes too (its secrets' files, the model services, the PATH
|
|
84
|
-
// without Coda's directories): it never takes another plugin's for its own.
|
|
85
|
-
export const PLUGIN_VARIABLES = ['CODA_NODE', 'CODA_ARBITER', 'CODA_PLUGIN_PATH', 'CODA_PLUGIN_STATE', 'CODA_PLUGIN_SETTINGS', 'CODA_PLUGIN_SECRETS',
|
|
86
|
-
'CODA_PLUGIN_EXTENSIONS', 'CODA_PLUGIN_DEPENDENCIES', 'CODA_MODEL_PROVIDERS', 'CODA_USER_PATH'];
|
|
87
|
-
export const pluginEnv = ({ pluginDir, stateDir, settings = {}, dependencies = {} }) => ({ CODA_NODE: process.execPath, CODA_PLUGIN_PATH: pluginDir,
|
|
88
|
-
CODA_PLUGIN_STATE: stateDir, CODA_PLUGIN_SETTINGS: JSON.stringify(settings),
|
|
89
|
-
...(Object.keys(dependencies).length ? { CODA_PLUGIN_DEPENDENCIES: JSON.stringify(dependencies) } : {}) });
|
|
90
|
-
export const cleared = base => Object.fromEntries(Object.entries(base).filter(([name]) => !PLUGIN_VARIABLES.includes(name)));
|
|
91
|
-
// How Coda starts an agent (domain/agents.mjs launch, adapters/session-host.mjs): `sh -c` in its own process group, standard
|
|
92
|
-
// error unread, the session's environment plus its plugin's.
|
|
93
|
-
// A panel's command (application/panels.mjs env): no session's ID, the backend's `node` first on the PATH, and the
|
|
94
|
-
// concurrency groups in use as it starts (`{ now, groups }`; none here).
|
|
95
|
-
export const THREAD_ENV = 'CODA_THREAD_ID';
|
|
96
|
-
export const groupsState = () => JSON.stringify({ now: Date.now(), groups: [] });
|
|
97
|
-
|
|
98
|
-
// ---- Manifests (backend/src/domain/plugins.mjs, agent-plugins.mjs, panels.mjs, plugin-extensions.mjs) ----
|
|
99
|
-
export const PLUGIN_ID = /^[a-z][a-z0-9-]{0,39}$/;
|
|
100
|
-
export const AGENT_ID = /^[a-z][a-z0-9-]{0,39}$/;
|
|
101
|
-
export const PANEL_ID = /^[a-z][a-z0-9-]{0,63}$/;
|
|
102
|
-
export const POINT = /^[a-z][a-z0-9-]{0,39}$/;
|
|
103
|
-
export const SETTING_KEY = /^[a-z][a-z0-9-]*(\.[a-z][A-Za-z0-9]*)+$/;
|
|
104
|
-
// The string formats a setting may have, beside `coda-agent` and `coda-agent-match` (backend/src/components/plugins/plugin-rules.mjs
|
|
105
|
-
// STRING_FORMATS): a duration, a repository, a directory of the machine, a model service another plugin provides
|
|
106
|
-
// (`<插件>/<服务>`, MODEL_PROVIDER), and the address of one.
|
|
107
|
-
export const STRING_FORMATS = ['duration', 'coda-repo', 'coda-directory', 'coda-model-provider', 'coda-model-url'];
|
|
108
|
-
export const MODEL_PROVIDER = /^[a-z][a-z0-9-]{0,39}\/[a-z][a-z0-9-]{0,39}$/;
|
|
109
|
-
export const CAPABILITY = /^[a-z][a-z0-9-]{0,63}$/;
|
|
110
|
-
// What a plugin may contribute to Coda itself (backend/src/components/plugins/plugin-rules.mjs CONTRIBUTES).
|
|
111
|
-
export const CONTRIBUTES = ['configuration', 'capabilities', 'instructions', 'agents', 'extensionPoints', 'extensions', 'groups', 'events', 'commands', 'gitCredentials',
|
|
112
|
-
'repositories', 'services', 'setup', 'skills', 'environment', 'path', 'endpoints', 'mcpServers', 'views', 'modelProviders', 'executors'];
|
|
113
|
-
// The keys of `contributes` points of the plugins Coda ships take (Coda's docs/plugins.md「写在 contributes 里的扩展点」): the
|
|
114
|
-
// resources plugin's `panels`. Anything else is said, not refused: Coda may have added it since this copy.
|
|
115
|
-
export const POINT_KEYS = ['panels'];
|
|
116
|
-
// What only a plugin Coda ships may contribute, one installed from a repository or npm being refused it
|
|
117
|
-
// (backend/src/components/plugins/plugin-source-rules.mjs catalogProblem): the clients do not show these in the confirmation yet.
|
|
118
|
-
export const BUILTIN_ONLY = { gitCredentials: '只有 Coda 自带的插件能提供 Git 凭据', repositories: '只有 Coda 自带的插件能提供仓库列表' };
|
|
119
|
-
// The bundled services a plugin turns on (`contributes.services`, docs/supervisor.md「独立服务进程」): their IDs.
|
|
120
|
-
export const SERVICE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
121
|
-
// A command a plugin puts on every session's and step's PATH (`contributes.commands`): its name, as a shell finds it, which
|
|
122
|
-
// may not be one of Coda's own; what runs for it, one line.
|
|
123
|
-
export const COMMAND = /^[a-z][a-z0-9-]{0,39}$/;
|
|
124
|
-
export const CODA_COMMANDS = ['coda-run', 'coda-event'];
|
|
125
|
-
// An event a plugin publishes (`contributes.events`, Coda's docs/plugins.md「事件」): `<域>.<名>`, in no domain of Coda's own;
|
|
126
|
-
// whether it starts the workflows subscribed to it (`starts`), and whether it ends its subject (`ends`: a run re-run after it
|
|
127
|
-
// is told it again).
|
|
128
|
-
export const EVENT = /^(?=.{3,100}$)[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
|
|
129
|
-
export const CODA_EVENT_DOMAINS = ['workflow', 'session', 'user'];
|
|
130
|
-
export const EVENT_LIMIT = 50;
|
|
131
|
-
export const COMMAND_LIMIT = 4000;
|
|
132
|
-
// A plugin's setup action (`contributes.setup`): `{ command }`, one line as a panel's command is; it may install software, so
|
|
133
|
-
// the checker reads it and does not run it.
|
|
134
|
-
export const SETUP_KEYS = ['command'];
|
|
135
|
-
export const INSTRUCTIONS_LIMIT = 200;
|
|
136
|
-
export const ICON_LIMIT = 32 * 1024;
|
|
137
|
-
export const AGENT_LIMITS = { displayName: 40, modeTitle: 10, modeUnset: 20, modeNote: 100, modeId: 200, setupHint: 200, command: 4000, configFiles: 16, path: 1000 };
|
|
138
|
-
export const PANEL_LIMITS = { title: 40, command: 4000, confirm: 200, note: 200 };
|
|
139
|
-
|
|
140
|
-
// ---- From a repository (backend/src/domain/plugin-sources.mjs) ----
|
|
141
|
-
export const SIZE_LIMIT = 10 * 1024 * 1024;
|
|
142
|
-
export const SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
143
|
-
export const DEPTH = 2;
|
|
144
|
-
// A plugin's npm dependencies come with its package-lock.json, naming the same (manifestProblem): Coda installs them with
|
|
145
|
-
// `npm ci` in the plugin's version directory.
|
|
146
|
-
export const NPM = ['dependencies', 'optionalDependencies'];
|
|
147
|
-
export const NPM_LOCK = 'package-lock.json';
|
|
148
|
-
// Those kept in `plugins/<id>` here that Coda ships as npm dependencies, `@coda-console/<id>` (docs/plugins.md「内置插件的来源」):
|
|
149
|
-
// a version of the same package may be installed over Coda's. Other sources may use the same ID once the built-in plugin is
|
|
150
|
-
// uninstalled; only the target backend can check its installed state (builtinProblem).
|
|
151
|
-
export const BUILTIN_HERE = ['claude', 'codex', 'codex-accounts', 'grok', 'kimi', 'opencode', 'review'];
|
|
152
|
-
export const BUILTIN_SCOPE = '@coda-console/';
|
|
153
|
-
// An agent, a concurrency group and a command are one plugin's (catalogProblem, claims). Those the built-in plugins declare, by
|
|
154
|
-
// plugin; and `claude`, the agent ID sessions from before #1069 still store (docs/plugins.md「Agent」).
|
|
155
|
-
export const BUILTIN_AGENTS = { 'claude-sdk': 'claude', codex: 'codex', grok: 'grok', kimi: 'kimi', opencode: 'opencode', claude: 'claude' };
|
|
156
|
-
export const BUILTIN_GROUPS = { host: 'host' };
|
|
157
|
-
export const BUILTIN_COMMANDS = { 'host-sim': 'host' };
|
|
158
|
-
|
|
159
|
-
// ---- Panels (backend/src/domain/panels.mjs, application/panels.mjs) ----
|
|
160
|
-
export const ITEM_TYPES = ['card', 'meter', 'stat', 'markdown', 'button', 'error'];
|
|
161
|
-
export const OUTPUT_LIMIT = 64 * 1024;
|
|
162
|
-
export const ARGS_LIMIT = 4 * 1024;
|
|
163
|
-
export const ITEM_LIMITS = { items: 50, cardItems: 20, label: 80, identity: 200, markdown: 4000, error: 500, confirm: 200 };
|
|
164
|
-
// A reading is stopped past this (docs/plugins.md「面板」).
|
|
165
|
-
export const PANEL_READ_LIMIT = 30_000;
|