@animalabs/connectome-host 0.7.3 → 0.8.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/.env.example +12 -5
- package/.github/PULL_REQUEST_TEMPLATE.md +3 -2
- package/.github/workflows/changelog.yml +9 -4
- package/.github/workflows/ci.yml +5 -3
- package/.github/workflows/publish.yml +12 -6
- package/CHANGELOG.md +401 -10
- package/CONTRIBUTING.md +47 -19
- package/HEADLESS-FLEET-PLAN.md +22 -0
- package/README.md +39 -1
- package/bun.lock +27 -31
- package/changelog.d/README.md +28 -0
- package/docs/AGENT-ONBOARDING.md +1 -1
- package/docs/debug-context-api.md +2 -2
- package/docs/retrieval-traces.md +173 -0
- package/docs/webui-deployment.md +2 -1
- package/package.json +6 -6
- package/recipes/SETUP.md +11 -5
- package/recipes/TRIUMVIRATE-SETUP.md +68 -14
- package/recipes/knowledge-miner.json +0 -30
- package/recipes/mock-test.json +19 -0
- package/recipes/triumvirate.json +6 -1
- package/scripts/audit-module-optins.ts +288 -0
- package/scripts/release-changelog.ts +210 -21
- package/src/cache-keepalive-log.ts +41 -0
- package/src/commands.ts +96 -0
- package/src/framework-strategy.ts +50 -4
- package/src/gate-telemetry.ts +106 -0
- package/src/headless.ts +24 -0
- package/src/index.ts +179 -64
- package/src/mcpl-config.ts +99 -1
- package/src/modules/fleet-module.ts +60 -1
- package/src/modules/fleet-types.ts +30 -1
- package/src/modules/identity-module.ts +310 -2
- package/src/modules/instructions-module.ts +265 -0
- package/src/modules/mcpl-admin-module.ts +89 -13
- package/src/modules/retrieval-module.ts +249 -51
- package/src/modules/retrieval-trace-page.ts +254 -0
- package/src/modules/retrieval-trace.ts +904 -0
- package/src/modules/subagent-module.ts +18 -0
- package/src/modules/tts-relay-module.ts +33 -18
- package/src/modules/web-ui-module.ts +445 -894
- package/src/recipe.ts +787 -29
- package/src/retrieval-config.ts +39 -0
- package/src/strategies/frontdesk-strategy.ts +34 -125
- package/src/tui.ts +325 -54
- package/src/web/panel-data.ts +1206 -0
- package/src/web/protocol.ts +75 -10
- package/src/workspace-mounts.ts +73 -0
- package/test/audit-module-optins.test.ts +174 -0
- package/test/cache-keepalive-log.test.ts +83 -0
- package/test/conversations-recipe.test.ts +142 -0
- package/test/fleet-panel-request.test.ts +90 -0
- package/test/framework-fkm-composition.test.ts +35 -3
- package/test/framework-strategy-defaults.test.ts +41 -0
- package/test/frontdesk-strategy.test.ts +25 -37
- package/test/gate-telemetry-adapter.test.ts +84 -0
- package/test/gate-telemetry.test.ts +91 -0
- package/test/headless-panel-request.test.ts +201 -0
- package/test/identity-and-surfaces.test.ts +212 -1
- package/test/instructions-module.test.ts +258 -0
- package/test/mcpl-admin-module.test.ts +64 -0
- package/test/mcpl-agent-overlay.test.ts +51 -3
- package/test/mcpl-child-env.test.ts +64 -0
- package/test/mock-headless-child.ts +14 -0
- package/test/nudge-command.test.ts +47 -0
- package/test/recipe-cache-keepalive.test.ts +59 -0
- package/test/recipe-compression-fallback.test.ts +19 -0
- package/test/recipe-hybrid-prose-routing.test.ts +12 -0
- package/test/recipe-instructions.test.ts +176 -0
- package/test/recipe-kv-unified.test.ts +87 -0
- package/test/recipe-mcp-source.test.ts +54 -0
- package/test/recipe-openai-compatible.test.ts +54 -0
- package/test/recipe-path-resolution.test.ts +19 -8
- package/test/recipe-provider.test.ts +14 -0
- package/test/recipe-save-unresolved.test.ts +244 -0
- package/test/recipe-source-only.test.ts +38 -0
- package/test/release-changelog.test.ts +202 -0
- package/test/retrieval-auth-loopback.test.ts +49 -0
- package/test/retrieval-config.test.ts +74 -0
- package/test/retrieval-module.test.ts +821 -0
- package/test/subagent-prose-routing.test.ts +109 -0
- package/test/tui-format.test.ts +106 -0
- package/test/web-ui-context-coverage.test.ts +1 -1
- package/test/web-ui-module.test.ts +189 -3
- package/test/web-ui-observers.test.ts +8 -5
- package/test/web-ui-protocol.test.ts +0 -0
- package/test/workspace-mounts.test.ts +68 -0
- package/web/src/App.tsx +160 -44
- package/web/src/Context.tsx +35 -8
- package/web/src/ContextDocument.tsx +20 -5
- package/web/src/Files.tsx +2 -8
- package/web/src/Health.tsx +61 -1
- package/web/src/Lessons.tsx +2 -38
- package/web/src/Mcpl.tsx +80 -14
- package/web/src/Pins.tsx +5 -0
- package/web/src/Settings.tsx +5 -0
- package/web/vite.config.ts +8 -2
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* bun src/index.ts --headless --exit-when-idle # One-shot: exit when agents go idle after first inference
|
|
11
11
|
*
|
|
12
12
|
* Environment variables:
|
|
13
|
-
* ANTHROPIC_API_KEY - Required
|
|
13
|
+
* ANTHROPIC_API_KEY - Required (not needed for recipe provider "mock")
|
|
14
14
|
* MODEL - Override model (default: from recipe or claude-opus-4-6)
|
|
15
15
|
* DATA_DIR - Data directory for sessions (default: ./data)
|
|
16
16
|
*/
|
|
@@ -19,25 +19,28 @@ import {
|
|
|
19
19
|
AnthropicXmlFormatter,
|
|
20
20
|
BedrockAdapter,
|
|
21
21
|
Membrane,
|
|
22
|
+
MockAdapter,
|
|
22
23
|
NativeFormatter,
|
|
23
24
|
OpenAIResponsesAPIAdapter,
|
|
24
25
|
OpenAIResponsesFormatter,
|
|
26
|
+
OpenAICompatibleAdapter,
|
|
25
27
|
OpenRouterAdapter,
|
|
26
28
|
} from '@animalabs/membrane';
|
|
27
29
|
import { LoggingAnthropicAdapter } from './logging-adapter.js';
|
|
28
30
|
import { LoggingProviderAdapter } from './logging-provider-wrapper.js';
|
|
31
|
+
import { gateTelemetryHeaders, type TurnTrigger } from './gate-telemetry.js';
|
|
29
32
|
import { LoggingBedrockAdapter } from './logging-bedrock-adapter.js';
|
|
30
33
|
import { CodexSubscriptionAdapter } from './codex-subscription-adapter.js';
|
|
31
34
|
import { CallLedger } from './call-ledger.js';
|
|
32
35
|
import { SettingsModule } from './modules/settings-module.js';
|
|
33
|
-
import { AgentFramework, WorkspaceModule, resolveTimeZone, type Module
|
|
36
|
+
import { AgentFramework, WorkspaceModule, resolveTimeZone, type Module } from '@animalabs/agent-framework';
|
|
34
37
|
import { resolve, join, basename } from 'node:path';
|
|
35
38
|
import { appendFile, mkdir, stat, rename } from 'node:fs/promises';
|
|
36
39
|
import { readFileSync, existsSync } from 'node:fs';
|
|
37
40
|
import { SubagentModule } from './modules/subagent-module.js';
|
|
38
41
|
import { LessonsModule } from './modules/lessons-module.js';
|
|
39
42
|
import { RetrievalModule } from './modules/retrieval-module.js';
|
|
40
|
-
import
|
|
43
|
+
import { buildRetrievalModuleConfig } from './retrieval-config.js';
|
|
41
44
|
import { TuiModule } from './modules/tui-module.js';
|
|
42
45
|
import { TimeModule } from './modules/time-module.js';
|
|
43
46
|
import { FleetModule, type FleetModuleConfig } from './modules/fleet-module.js';
|
|
@@ -49,14 +52,15 @@ import { ObserversModule } from './modules/observers-module.js';
|
|
|
49
52
|
import { IdentityModule } from './modules/identity-module.js';
|
|
50
53
|
import { McplAdminModule } from './modules/mcpl-admin-module.js';
|
|
51
54
|
import { TtsRelayModule } from './modules/tts-relay-module.js';
|
|
52
|
-
import {
|
|
55
|
+
import { InstructionsModule } from './modules/instructions-module.js';
|
|
56
|
+
import { loadMcplServers, applyAgentOverlay, composeMcplChildEnv, DEFAULT_CONFIG_PATH, DEFAULT_AGENT_OVERLAY_PATH } from './mcpl-config.js';
|
|
53
57
|
import { SessionManager } from './session-manager.js';
|
|
54
58
|
import { resolveAgentName } from './agent-name.js';
|
|
55
59
|
import { generateSessionName } from './synesthete.js';
|
|
56
60
|
import {
|
|
57
61
|
type Recipe,
|
|
58
62
|
DEFAULT_RECIPE,
|
|
59
|
-
|
|
63
|
+
loadRecipeDetailed,
|
|
60
64
|
saveRecipe,
|
|
61
65
|
loadSavedRecipe,
|
|
62
66
|
clearSavedRecipe,
|
|
@@ -64,7 +68,9 @@ import {
|
|
|
64
68
|
} from './recipe.js';
|
|
65
69
|
import { createBranchState, resetBranchState, handleExport, type BranchState } from './commands.js';
|
|
66
70
|
import { buildFrameworkAgentConfig, membraneCachingOverride } from './framework-agent-config.js';
|
|
67
|
-
import { buildFrameworkStrategy } from './framework-strategy.js';
|
|
71
|
+
import { buildFrameworkStrategy, buildConversationsConfig } from './framework-strategy.js';
|
|
72
|
+
import { buildWorkspaceMounts } from './workspace-mounts.js';
|
|
73
|
+
import { logKeepaliveEvent } from './cache-keepalive-log.js';
|
|
68
74
|
import { loadExtensions } from './extensions.js';
|
|
69
75
|
|
|
70
76
|
export type { AppContext };
|
|
@@ -79,6 +85,10 @@ const config = {
|
|
|
79
85
|
authToken: process.env.ANTHROPIC_AUTH_TOKEN,
|
|
80
86
|
openaiApiKey: process.env.OPENAI_API_KEY,
|
|
81
87
|
openrouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
88
|
+
// Deliberately NO fallback to OPENAI_API_KEY: agent.baseUrl is
|
|
89
|
+
// recipe-controlled, so a fallback would silently send a real OpenAI
|
|
90
|
+
// credential as a Bearer token to whatever endpoint a recipe names.
|
|
91
|
+
openaiCompatibleApiKey: process.env.OPENAI_COMPATIBLE_API_KEY,
|
|
82
92
|
codexBinary: process.env.CODEX_BINARY,
|
|
83
93
|
model: process.env.MODEL,
|
|
84
94
|
dataDir: process.env.DATA_DIR || './data',
|
|
@@ -103,6 +113,10 @@ interface AppContext {
|
|
|
103
113
|
branchState: BranchState;
|
|
104
114
|
userMessageCount: number;
|
|
105
115
|
codexAdapter?: CodexSubscriptionAdapter;
|
|
116
|
+
/** Content-free recent provider-call ledger. Consumed by the panel-data
|
|
117
|
+
* layer (health snapshots) in BOTH runtimes — WebUI host and headless
|
|
118
|
+
* fleet child. Null when the provider adapter exposes no ledger. */
|
|
119
|
+
callLedger: CallLedger | null;
|
|
106
120
|
|
|
107
121
|
/** Stop current framework, switch to a different session, start new framework. */
|
|
108
122
|
switchSession(id: string): Promise<void>;
|
|
@@ -123,8 +137,10 @@ async function resolveRecipe(): Promise<Recipe> {
|
|
|
123
137
|
|
|
124
138
|
if (source) {
|
|
125
139
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
// Save the unresolved (pre-substitution) form: $DATA_DIR is typically
|
|
141
|
+
// host-mounted and backed up, so resolved secrets must never land there.
|
|
142
|
+
const { recipe, persistable } = await loadRecipeDetailed(source);
|
|
143
|
+
saveRecipe(config.dataDir, persistable);
|
|
128
144
|
console.log(`Loaded recipe: ${recipe.name}${recipe.description ? ` — ${recipe.description}` : ''}`);
|
|
129
145
|
return recipe;
|
|
130
146
|
} catch (err) {
|
|
@@ -133,11 +149,21 @@ async function resolveRecipe(): Promise<Recipe> {
|
|
|
133
149
|
}
|
|
134
150
|
}
|
|
135
151
|
|
|
136
|
-
// Try saved recipe
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
152
|
+
// Try saved recipe. Re-resolves ${VAR} references against the CURRENT
|
|
153
|
+
// environment, so a missing secret fails loudly here rather than starting
|
|
154
|
+
// a misconfigured agent on the default recipe.
|
|
155
|
+
try {
|
|
156
|
+
const saved = await loadSavedRecipe(config.dataDir);
|
|
157
|
+
if (saved) {
|
|
158
|
+
console.log(`Resuming recipe: ${saved.name}`);
|
|
159
|
+
return saved;
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.error(
|
|
163
|
+
`Failed to resume saved recipe from ${config.dataDir}:`,
|
|
164
|
+
err instanceof Error ? err.message : err,
|
|
165
|
+
);
|
|
166
|
+
process.exit(1);
|
|
141
167
|
}
|
|
142
168
|
|
|
143
169
|
return DEFAULT_RECIPE;
|
|
@@ -227,16 +253,13 @@ async function createFramework(
|
|
|
227
253
|
}
|
|
228
254
|
|
|
229
255
|
// Retrieval (requires lessons). OPT-IN — not part of the standard recipe:
|
|
230
|
-
// it injects context-dependent content into every compile (plus two
|
|
231
|
-
//
|
|
232
|
-
//
|
|
256
|
+
// it injects context-dependent content into every compile (plus up to two
|
|
257
|
+
// configured retrieval-model calls), which adds per-turn context churn.
|
|
258
|
+
// Enable explicitly only when an agent actually curates a lesson library.
|
|
233
259
|
if (modules.retrieval && lessonsModule) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
retrievalModel: retrievalConfig.model,
|
|
238
|
-
maxInjectedLessons: retrievalConfig.maxInjected,
|
|
239
|
-
}));
|
|
260
|
+
moduleInstances.push(new RetrievalModule(
|
|
261
|
+
buildRetrievalModuleConfig(membrane, modules.retrieval, recipe.agent.provider),
|
|
262
|
+
));
|
|
240
263
|
}
|
|
241
264
|
|
|
242
265
|
// Gate config — core AF EventGate feature.
|
|
@@ -258,48 +281,30 @@ async function createFramework(
|
|
|
258
281
|
// Note: workspace: false disables ALL filesystem access (both read and write).
|
|
259
282
|
// Previously LocalFilesModule was always-on; this is an intentional change —
|
|
260
283
|
// recipes that need read-only access should keep workspace enabled (the default).
|
|
284
|
+
// Mount construction lives in workspace-mounts.ts so recipe validation
|
|
285
|
+
// reasons over the SAME mount flags the runtime builds (see that file's
|
|
286
|
+
// header for why).
|
|
261
287
|
let workspaceModule: WorkspaceModule | null = null;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// Only pass fields the recipe explicitly provides; let WorkspaceModule default the rest.
|
|
266
|
-
// We override watch to 'never' since FKM doesn't need chokidar filesystem watchers.
|
|
267
|
-
mounts = modules.workspace.mounts.map((m: RecipeWorkspaceMount) => {
|
|
268
|
-
const mount: MountConfig = {
|
|
269
|
-
name: m.name,
|
|
270
|
-
path: resolve(m.path),
|
|
271
|
-
mode: m.mode ?? 'read-write',
|
|
272
|
-
watch: m.watch ?? 'never', // FKM: no chokidar watchers by default
|
|
273
|
-
};
|
|
274
|
-
if (m.ignore) mount.ignore = m.ignore;
|
|
275
|
-
if (m.wakeOnChange !== undefined) mount.wakeOnChange = m.wakeOnChange;
|
|
276
|
-
if (m.autoMaterialize !== undefined) mount.autoMaterialize = m.autoMaterialize;
|
|
277
|
-
return mount;
|
|
278
|
-
});
|
|
279
|
-
} else {
|
|
280
|
-
// Default: read-only input mount + read-write products mount
|
|
281
|
-
mounts = [
|
|
282
|
-
{ name: 'input', path: resolve('./input'), mode: 'read-only', watch: 'never' },
|
|
283
|
-
{ name: 'products', path: resolve('./output'), mode: 'read-write', watch: 'never' },
|
|
284
|
-
];
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Config mount: version-controls gate.json (and future config files) via Chronicle.
|
|
288
|
-
// Opt-in via recipe: workspace.configMount = true
|
|
289
|
-
const wantConfigMount = typeof modules.workspace === 'object' && modules.workspace.configMount;
|
|
290
|
-
if (wantConfigMount) {
|
|
291
|
-
mounts.push({
|
|
292
|
-
name: '_config',
|
|
293
|
-
path: resolve(join(storePath, 'config')),
|
|
294
|
-
mode: 'read-write',
|
|
295
|
-
watch: 'always',
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
workspaceModule = new WorkspaceModule({ mounts });
|
|
288
|
+
const workspaceMounts = buildWorkspaceMounts(modules.workspace, storePath);
|
|
289
|
+
if (workspaceMounts) {
|
|
290
|
+
workspaceModule = new WorkspaceModule({ mounts: workspaceMounts });
|
|
300
291
|
moduleInstances.push(workspaceModule);
|
|
301
292
|
}
|
|
302
293
|
|
|
294
|
+
// Shared operating instructions — opt-in per recipe. Injects a living
|
|
295
|
+
// instructions document (read through a workspace mount) into EVERY
|
|
296
|
+
// agent's context on every turn — resident and ephemeral subagents alike —
|
|
297
|
+
// via gatherContext. Requires the workspace module; validateRecipe rejects
|
|
298
|
+
// the instructions+`workspace: false` pairing, so workspaceModule is
|
|
299
|
+
// non-null here (the guard keeps the module fail-open regardless).
|
|
300
|
+
if (modules.instructions) {
|
|
301
|
+
const instructionsConfig =
|
|
302
|
+
typeof modules.instructions === 'object' ? modules.instructions : {};
|
|
303
|
+
const instructionsModule = new InstructionsModule(instructionsConfig);
|
|
304
|
+
if (workspaceModule) instructionsModule.setWorkspace(workspaceModule);
|
|
305
|
+
moduleInstances.push(instructionsModule);
|
|
306
|
+
}
|
|
307
|
+
|
|
303
308
|
// Activity (typing indicators) — opt-in per recipe
|
|
304
309
|
let activityModule: ActivityModule | null = null;
|
|
305
310
|
if (modules.activity !== undefined && modules.activity !== false) {
|
|
@@ -459,9 +464,11 @@ async function createFramework(
|
|
|
459
464
|
const finalServers = applyAgentOverlay(allServers, DEFAULT_AGENT_OVERLAY_PATH).map((server) => {
|
|
460
465
|
const withEnv: { id: string; command?: string; url?: string; [k: string]: unknown } = {
|
|
461
466
|
...server,
|
|
462
|
-
// Stdio MCPL children inherit a single agent-facing wall clock
|
|
463
|
-
//
|
|
464
|
-
|
|
467
|
+
// Stdio MCPL children inherit a single agent-facing wall clock plus the
|
|
468
|
+
// framework's refusal-annotation suppression baseline (operator env
|
|
469
|
+
// supersedes the baseline; see composeMcplChildEnv). Protocol
|
|
470
|
+
// timestamps remain UTC; only their rendered text uses AGENT_TIMEZONE.
|
|
471
|
+
env: composeMcplChildEnv(server.env as Record<string, string> | undefined, timeZone),
|
|
465
472
|
};
|
|
466
473
|
// `access` is a declarative name (recipe/file/overlay); the credential
|
|
467
474
|
// provider it implies is attached HERE, at load time — fresh credential
|
|
@@ -487,6 +494,10 @@ async function createFramework(
|
|
|
487
494
|
const strategy = buildFrameworkStrategy(recipe, model, timeZone, extensionRegistry);
|
|
488
495
|
const agentConfig = buildFrameworkAgentConfig(recipe, agentName, model, strategy);
|
|
489
496
|
|
|
497
|
+
// Per-channel conversation routing: the recipe agent becomes the trunk
|
|
498
|
+
// template; forks get a fresh instance of the same recipe strategy.
|
|
499
|
+
const conversations = buildConversationsConfig(recipe, agentName, model, timeZone, extensionRegistry);
|
|
500
|
+
|
|
490
501
|
// -- Create framework --
|
|
491
502
|
const framework = await AgentFramework.create({
|
|
492
503
|
storePath,
|
|
@@ -498,6 +509,7 @@ agents: [agentConfig],
|
|
|
498
509
|
timeZone,
|
|
499
510
|
// Client-side programmatic tool calling (code_execution) — recipe opt-in.
|
|
500
511
|
...(recipe.codeExecution ? { codeExecution: recipe.codeExecution } : {}),
|
|
512
|
+
...(conversations ? { conversations } : {}),
|
|
501
513
|
});
|
|
502
514
|
|
|
503
515
|
// Wire post-creation hooks
|
|
@@ -870,6 +882,16 @@ async function main() {
|
|
|
870
882
|
fastMode: recipe.agent.codex?.fastMode ?? false,
|
|
871
883
|
})
|
|
872
884
|
: undefined;
|
|
885
|
+
// Generic OpenAI-compatible chat-completions endpoint (Ollama, vLLM, Together,
|
|
886
|
+
// Groq, NanoGPT, ...). The recipe carries the endpoint (agent.baseUrl,
|
|
887
|
+
// validated at load); the key is optional because local servers have none.
|
|
888
|
+
const openaiCompatibleAdapter = provider === 'openai-compatible'
|
|
889
|
+
? new OpenAICompatibleAdapter({
|
|
890
|
+
baseURL: recipe.agent.baseUrl!,
|
|
891
|
+
apiKey: config.openaiCompatibleApiKey || undefined,
|
|
892
|
+
providerName: 'openai-compatible',
|
|
893
|
+
})
|
|
894
|
+
: undefined;
|
|
873
895
|
const openrouterAdapter = provider === 'openrouter'
|
|
874
896
|
? new OpenRouterAdapter({
|
|
875
897
|
apiKey: config.openrouterApiKey!,
|
|
@@ -888,9 +910,74 @@ async function main() {
|
|
|
888
910
|
// live probe 2026-07-31. Still no CallLedger — it's
|
|
889
911
|
// anthropic-transport-only for now; cache metrics are visible in
|
|
890
912
|
// llm-calls.jsonl via the logging wrapper.
|
|
913
|
+
// BEDROCK_BASE_URL routes bedrock-runtime calls through an inference
|
|
914
|
+
// gateway (gate.animalabs.ai/bedrock/<credSet>) — mirrors the
|
|
915
|
+
// ANTHROPIC_BASE_URL hook below. The gate reads the agent token from the
|
|
916
|
+
// SigV4 Credential (AWS_ACCESS_KEY_ID slot), discards the client
|
|
917
|
+
// signature, and re-signs with real AWS creds held on the gate box.
|
|
891
918
|
const bedrockAdapter = provider === 'bedrock'
|
|
892
|
-
? new LoggingBedrockAdapter(
|
|
919
|
+
? new LoggingBedrockAdapter(
|
|
920
|
+
{ baseURL: process.env.BEDROCK_BASE_URL || undefined },
|
|
921
|
+
llmLogPath,
|
|
922
|
+
)
|
|
923
|
+
: undefined;
|
|
924
|
+
// Mock: membrane's canned/echo adapter — the full host loop with zero
|
|
925
|
+
// provider spend and no credentials (none of the key checks above are
|
|
926
|
+
// gated on it). Echo is the default because it's the informative shape
|
|
927
|
+
// for interactive smoke runs; recipe agent.mock.echoMode=false switches
|
|
928
|
+
// to defaultResponse for deterministic scripted output. It rides the
|
|
929
|
+
// generic logging decorator so even mock calls leave llm-calls.jsonl
|
|
930
|
+
// receipts — the observability path is part of what a mock run exercises.
|
|
931
|
+
const mockAdapter = provider === 'mock'
|
|
932
|
+
? new MockAdapter({
|
|
933
|
+
echoMode: recipe.agent.mock?.echoMode ?? true,
|
|
934
|
+
...(recipe.agent.mock?.defaultResponse !== undefined
|
|
935
|
+
? { defaultResponse: recipe.agent.mock.defaultResponse }
|
|
936
|
+
: {}),
|
|
937
|
+
})
|
|
893
938
|
: undefined;
|
|
939
|
+
// -- x-gate-debt-chunks stamp (membrane dynamicHeaders, antra-tess/membrane#65)
|
|
940
|
+
// The gate records compression-debt per ledger row; debt only changes at
|
|
941
|
+
// calls, so the per-call series is its full-resolution history ("spoke ->
|
|
942
|
+
// chunk appeared -> chewed" vs "solver repacked -> queue spiked"). The value
|
|
943
|
+
// is the SAME reduction /healthz reports (cm getCompressionDebt). Late-bound
|
|
944
|
+
// through appRef because the framework outlives adapter construction and is
|
|
945
|
+
// replaced on session switch. Whenever it is unreadable — no framework yet,
|
|
946
|
+
// multi-agent process (whose debt would we even claim?), no strategy — the
|
|
947
|
+
// header is simply not sent: an unstamped call is honest, a guessed one lies.
|
|
948
|
+
let appRefForDebt: AppContext | null = null;
|
|
949
|
+
const pendingDebtChunks = (): number | null => {
|
|
950
|
+
try {
|
|
951
|
+
const agents = appRefForDebt?.framework.getAllAgents() ?? [];
|
|
952
|
+
if (agents.length !== 1) return null;
|
|
953
|
+
const strategy = (agents[0] as unknown as {
|
|
954
|
+
getContextManager?: () => { getStrategy?: () => { getCompressionDebt?: () => unknown } };
|
|
955
|
+
}).getContextManager?.()?.getStrategy?.();
|
|
956
|
+
const d = strategy?.getCompressionDebt?.() as { pendingChunks?: unknown } | undefined;
|
|
957
|
+
const n = d?.pendingChunks;
|
|
958
|
+
return typeof n === 'number' && Number.isFinite(n) && n >= 0 ? Math.round(n) : null;
|
|
959
|
+
} catch {
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// Why the turn in progress fired (heartbeat / a channel message by whom /
|
|
965
|
+
// operator), read from the framework's active-turn trigger. Same guards as
|
|
966
|
+
// the debt getter: no framework or several agents
|
|
967
|
+
// -> null -> the origin trio is simply not sent.
|
|
968
|
+
const activeTurnTrigger = (): TurnTrigger | null => {
|
|
969
|
+
try {
|
|
970
|
+
const agents = appRefForDebt?.framework.getAllAgents() ?? [];
|
|
971
|
+
if (agents.length !== 1) return null;
|
|
972
|
+
const t = appRefForDebt?.framework.getActiveTurnTrigger(agents[0]!.name);
|
|
973
|
+
return t ? { reason: t.reason, source: t.source, channelId: t.channelId, counterparty: t.counterparty } : null;
|
|
974
|
+
} catch {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
const gateTelemetryDynamicHeaders = gateTelemetryHeaders(process.env, pendingDebtChunks, activeTurnTrigger);
|
|
980
|
+
|
|
894
981
|
const adapter = provider === 'openai-responses'
|
|
895
982
|
? new LoggingProviderAdapter(
|
|
896
983
|
new OpenAIResponsesAPIAdapter({
|
|
@@ -905,7 +992,9 @@ async function main() {
|
|
|
905
992
|
// `openrouterAdapter` instances stay un-wrapped for auth commands and
|
|
906
993
|
// dispose() — only the membrane sees the wrapper.
|
|
907
994
|
: bedrockAdapter
|
|
995
|
+
?? (mockAdapter ? new LoggingProviderAdapter(mockAdapter, llmLogPath) : undefined)
|
|
908
996
|
?? (openrouterAdapter ? new LoggingProviderAdapter(openrouterAdapter, llmLogPath) : undefined)
|
|
997
|
+
?? (openaiCompatibleAdapter ? new LoggingProviderAdapter(openaiCompatibleAdapter, llmLogPath) : undefined)
|
|
909
998
|
?? (codexAdapter ? new LoggingProviderAdapter(codexAdapter, llmLogPath) : undefined)
|
|
910
999
|
?? new LoggingAnthropicAdapter(
|
|
911
1000
|
{
|
|
@@ -927,6 +1016,29 @@ async function main() {
|
|
|
927
1016
|
: {}),
|
|
928
1017
|
}),
|
|
929
1018
|
baseURL: process.env.ANTHROPIC_BASE_URL || undefined,
|
|
1019
|
+
// Double-gated (see src/gate-telemetry.ts): GATE_TELEMETRY=1 AND a
|
|
1020
|
+
// configured base URL, so the stamp can only ever go to a
|
|
1021
|
+
// gateway the operator has declared — never to the vendor's
|
|
1022
|
+
// default endpoint (review finding on the first wiring).
|
|
1023
|
+
...(gateTelemetryDynamicHeaders ? { dynamicHeaders: gateTelemetryDynamicHeaders } : {}),
|
|
1024
|
+
// Hold this agent's cached prefix warm across idle gaps. Only fires
|
|
1025
|
+
// when the entry is actually near expiry, so a busy agent costs
|
|
1026
|
+
// nothing; only the 1h-TTL primary lane is eligible (the module
|
|
1027
|
+
// skips anything else). Off with cacheKeepalive.enabled: false.
|
|
1028
|
+
cacheKeepalive: {
|
|
1029
|
+
enabled: recipe.agent.cacheKeepalive?.enabled !== false,
|
|
1030
|
+
...(recipe.agent.cacheKeepalive?.maxIdleHours !== undefined
|
|
1031
|
+
? { maxIdleMs: recipe.agent.cacheKeepalive.maxIdleHours * 60 * 60_000 }
|
|
1032
|
+
: {}),
|
|
1033
|
+
...(recipe.agent.cacheKeepalive?.refreshAfterMinutes !== undefined
|
|
1034
|
+
? { refreshAfterMs: recipe.agent.cacheKeepalive.refreshAfterMinutes * 60_000 }
|
|
1035
|
+
: {}),
|
|
1036
|
+
// Every event — routine refreshes included — goes to stderr, so
|
|
1037
|
+
// all of them land in service-stderr.log next to
|
|
1038
|
+
// [inference-refusal]. See cache-keepalive-log.ts for why this is
|
|
1039
|
+
// not severity-routed.
|
|
1040
|
+
onEvent: logKeepaliveEvent,
|
|
1041
|
+
},
|
|
930
1042
|
},
|
|
931
1043
|
llmLogPath,
|
|
932
1044
|
() => settingsModule.getReasoning(),
|
|
@@ -996,6 +1108,7 @@ async function main() {
|
|
|
996
1108
|
branchState: createBranchState(),
|
|
997
1109
|
userMessageCount: 0,
|
|
998
1110
|
codexAdapter,
|
|
1111
|
+
callLedger,
|
|
999
1112
|
|
|
1000
1113
|
async switchSession(id: string) {
|
|
1001
1114
|
handleExport(this);
|
|
@@ -1016,6 +1129,8 @@ async function main() {
|
|
|
1016
1129
|
},
|
|
1017
1130
|
};
|
|
1018
1131
|
|
|
1132
|
+
appRefForDebt = app;
|
|
1133
|
+
|
|
1019
1134
|
// Off-path refusal dragnet → ops alerts (observability M3): refusals on
|
|
1020
1135
|
// non-streamed calls (compression/summarizer drains, maintenance) never
|
|
1021
1136
|
// reach the framework's own noteRefusal — the 2026-07-15 mythos cascade
|
package/src/mcpl-config.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
8
8
|
import { resolve, dirname } from 'node:path';
|
|
9
|
+
import { REFUSAL_REACTION_BASELINE } from '@animalabs/agent-framework';
|
|
9
10
|
|
|
10
11
|
/** Default config file path, resolved from cwd. */
|
|
11
12
|
export const DEFAULT_CONFIG_PATH = resolve(process.cwd(), 'mcpl-servers.json');
|
|
@@ -134,10 +135,57 @@ export function saveAgentOverlay(
|
|
|
134
135
|
writeFileSync(overlayPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Capabilities an agent-deployed server is never granted by default: the
|
|
140
|
+
* consequential surfaces — context hooks (observation of and injection into
|
|
141
|
+
* the agent's own inference), server-initiated inference, and inference
|
|
142
|
+
* lifecycle. Bare parents on purpose: the config mask's subtree matching
|
|
143
|
+
* denies everything beneath them, present and future (afterInference, undo,
|
|
144
|
+
* state land under these the day the vocabulary grows them). A world/chat
|
|
145
|
+
* server needs none of this — channels + tools is the whole job. An operator
|
|
146
|
+
* who wants a self-deployed server to hold one of these moves the server
|
|
147
|
+
* into the recipe, where `enabledCapabilities` is theirs to write.
|
|
148
|
+
*/
|
|
149
|
+
export const AGENT_DEPLOY_DENIED_CAPABILITIES: readonly string[] = [
|
|
150
|
+
'contextHooks',
|
|
151
|
+
'inferenceRequest',
|
|
152
|
+
'inferenceLifecycle',
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
/** The allow/deny list fields where an EMPTY array carries no intent (see
|
|
156
|
+
* resolveOverlayEntry — OpenAI strict function calling forces every schema
|
|
157
|
+
* property, so agent tool calls arrive with `[]` meaning "unspecified"). */
|
|
158
|
+
const OVERLAY_LIST_FIELDS = [
|
|
159
|
+
'enabledFeatureSets',
|
|
160
|
+
'disabledFeatureSets',
|
|
161
|
+
'enabledTools',
|
|
162
|
+
'disabledTools',
|
|
163
|
+
] as const;
|
|
164
|
+
|
|
137
165
|
/**
|
|
138
166
|
* Resolve an overlay entry into a server config object (id + fields, relative
|
|
139
167
|
* `./`/`../` args resolved against the overlay file's directory). Returns
|
|
140
168
|
* null for tombstones and entries with neither command nor url.
|
|
169
|
+
*
|
|
170
|
+
* The overlay is the AGENT's file, so resolution is also where host policy
|
|
171
|
+
* for self-deployed servers lives (applied at boot AND at deploy — existing
|
|
172
|
+
* files heal without a re-deploy):
|
|
173
|
+
*
|
|
174
|
+
* - Empty allow/deny lists are treated as absent. OpenAI-style strict
|
|
175
|
+
* function calling forces every schema property, so GPT-family residents
|
|
176
|
+
* calling mcpl_deploy emit `[]` where they meant "unspecified" — and for
|
|
177
|
+
* the allowlists PRESENT-empty is deny-all under the §5.3 pin (Mica's
|
|
178
|
+
* silently eventless eidoverse, 2026-08-04). An agent that truly wants
|
|
179
|
+
* deny-all says `disabledTools: ["*"]` / `disabledFeatureSets: ["*"]`.
|
|
180
|
+
*
|
|
181
|
+
* - `enabledCapabilities` is dropped: the agent's file can narrow, never
|
|
182
|
+
* widen — a hand-written entry here could re-grant §13.4 deny-by-default
|
|
183
|
+
* paths.
|
|
184
|
+
*
|
|
185
|
+
* - `disabledCapabilities` always carries at least
|
|
186
|
+
* AGENT_DEPLOY_DENIED_CAPABILITIES (unioned with anything the entry
|
|
187
|
+
* already denies): self-deployed servers get channels + tools and
|
|
188
|
+
* nothing consequential by default.
|
|
141
189
|
*/
|
|
142
190
|
export function resolveOverlayEntry(
|
|
143
191
|
id: string,
|
|
@@ -148,9 +196,29 @@ export function resolveOverlayEntry(
|
|
|
148
196
|
if (!entry.command && !entry.url) return null;
|
|
149
197
|
const overlayDir = dirname(resolve(overlayPath));
|
|
150
198
|
const { disabled: _d, ...fields } = entry;
|
|
199
|
+
const rec = fields as Record<string, unknown>;
|
|
200
|
+
for (const k of OVERLAY_LIST_FIELDS) {
|
|
201
|
+
if (Array.isArray(rec[k]) && (rec[k] as unknown[]).length === 0) delete rec[k];
|
|
202
|
+
}
|
|
203
|
+
delete rec.enabledCapabilities;
|
|
204
|
+
// A network server the agent deployed should come back when it bounces.
|
|
205
|
+
// reconnect defaulted to false, so an entry that never said `reconnect:
|
|
206
|
+
// true` was severed PERMANENTLY by any server restart — with no signal to
|
|
207
|
+
// anyone — until the agent's own next restart, which for a long-lived
|
|
208
|
+
// resident is days away (Mythos, eventless in eidoverse after the
|
|
209
|
+
// 2026-08-04 door deploy). Websocket entries now default to reconnect
|
|
210
|
+
// unless the entry explicitly says false. Stdio entries keep the old
|
|
211
|
+
// default: reconnect does not respawn a dead child anyway (mcpl_restart
|
|
212
|
+
// is that path), so `true` there would promise something it can't do.
|
|
213
|
+
if (entry.url && rec.reconnect === undefined) rec.reconnect = true;
|
|
214
|
+
const denied = new Set<string>([
|
|
215
|
+
...AGENT_DEPLOY_DENIED_CAPABILITIES,
|
|
216
|
+
...(Array.isArray(rec.disabledCapabilities) ? (rec.disabledCapabilities as unknown[]).map(String) : []),
|
|
217
|
+
]);
|
|
151
218
|
return {
|
|
152
219
|
id,
|
|
153
|
-
...
|
|
220
|
+
...rec,
|
|
221
|
+
disabledCapabilities: [...denied].sort(),
|
|
154
222
|
...(entry.args
|
|
155
223
|
? {
|
|
156
224
|
args: entry.args.map(arg =>
|
|
@@ -206,3 +274,33 @@ export function saveMcplServers(configPath: string, servers: Record<string, Serv
|
|
|
206
274
|
const data: McplServersFile = { mcplServers: servers };
|
|
207
275
|
writeFileSync(configPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
208
276
|
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Compose the environment for a stdio MCPL child.
|
|
280
|
+
*
|
|
281
|
+
* Two host-owned values ride along with whatever the server entry declares:
|
|
282
|
+
*
|
|
283
|
+
* - `DISCORD_SUPPRESSED_REACTIONS_BASELINE` — the framework's exported
|
|
284
|
+
* refusal-annotation set (REFUSAL_REACTION_BASELINE, comma-joined), so a
|
|
285
|
+
* never-configured Discord adapter defaults to suppressing exactly the
|
|
286
|
+
* markers this host's framework stamps. Placed BEFORE the spread: an
|
|
287
|
+
* operator who sets the var on the server entry supersedes the house
|
|
288
|
+
* baseline — the host injects a default, never overrides a decision. The
|
|
289
|
+
* adapter's own precedence (file key incl. [] → legacy operator env →
|
|
290
|
+
* baseline) then decides what is actually enforced; house markers are
|
|
291
|
+
* Host semantics, and a standalone adapter without this composition stays
|
|
292
|
+
* honestly unprotected.
|
|
293
|
+
* - `AGENT_TIMEZONE` — after the spread, deliberately: the agent-facing
|
|
294
|
+
* wall clock is resolved per-recipe by the host and is not a per-server
|
|
295
|
+
* operator knob.
|
|
296
|
+
*/
|
|
297
|
+
export function composeMcplChildEnv(
|
|
298
|
+
serverEnv: Record<string, string> | undefined,
|
|
299
|
+
timeZone: string,
|
|
300
|
+
): Record<string, string> {
|
|
301
|
+
return {
|
|
302
|
+
DISCORD_SUPPRESSED_REACTIONS_BASELINE: REFUSAL_REACTION_BASELINE.join(','),
|
|
303
|
+
...(serverEnv ?? {}),
|
|
304
|
+
AGENT_TIMEZONE: timeZone,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -34,7 +34,7 @@ import { spawn as spawnProcess, type ChildProcess } from 'node:child_process';
|
|
|
34
34
|
import { connect as netConnect, type Socket } from 'node:net';
|
|
35
35
|
import { existsSync, mkdirSync, unlinkSync, openSync, closeSync, appendFileSync, realpathSync } from 'node:fs';
|
|
36
36
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
37
|
-
import { type IncomingCommand, type WireEvent, matchesSubscription } from './fleet-types.js';
|
|
37
|
+
import { type IncomingCommand, type WireEvent, type PanelResponseEvent, matchesSubscription } from './fleet-types.js';
|
|
38
38
|
import { loadRecipe } from '../recipe.js';
|
|
39
39
|
import { REDUCER_REQUIRED_EVENTS } from '../state/agent-tree-reducer.js';
|
|
40
40
|
|
|
@@ -1621,6 +1621,65 @@ export class FleetModule implements Module {
|
|
|
1621
1621
|
catch { return false; }
|
|
1622
1622
|
}
|
|
1623
1623
|
|
|
1624
|
+
/** Monotonic corrId source for requestPanel. */
|
|
1625
|
+
private panelSeq = 0;
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Run one operator-panel op (see src/web/panel-data.ts) in a fleet child
|
|
1629
|
+
* and await its `panel-response`. Promise-based counterpart to the
|
|
1630
|
+
* fire-and-forget request* verbs above: HTTP proxy routes need to await a
|
|
1631
|
+
* body, and the WS handlers are simpler for it too.
|
|
1632
|
+
*
|
|
1633
|
+
* Never rejects — a dead child, send failure, or timeout resolves as
|
|
1634
|
+
* `{ok:false, error, status}` (502 unreachable, 504 timeout), so callers
|
|
1635
|
+
* translate straight into a response without try/catch.
|
|
1636
|
+
*/
|
|
1637
|
+
requestPanel(
|
|
1638
|
+
childName: string,
|
|
1639
|
+
op: string,
|
|
1640
|
+
params?: Record<string, unknown>,
|
|
1641
|
+
timeoutMs = 30_000,
|
|
1642
|
+
): Promise<{ ok: boolean; data?: unknown; error?: string; status?: number }> {
|
|
1643
|
+
const child = this.children.get(childName);
|
|
1644
|
+
if (!child || !child.socket) {
|
|
1645
|
+
return Promise.resolve({
|
|
1646
|
+
ok: false,
|
|
1647
|
+
error: child ? `child '${childName}' is ${child.status}, not running` : `unknown child: ${childName}`,
|
|
1648
|
+
status: child ? 502 : 404,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
const corrId = `panel-${op}-${++this.panelSeq}-${Date.now().toString(36)}`;
|
|
1652
|
+
return new Promise((resolvePanel) => {
|
|
1653
|
+
let settled = false;
|
|
1654
|
+
const finish = (result: { ok: boolean; data?: unknown; error?: string; status?: number }): void => {
|
|
1655
|
+
if (settled) return;
|
|
1656
|
+
settled = true;
|
|
1657
|
+
unsub();
|
|
1658
|
+
clearTimeout(timer);
|
|
1659
|
+
resolvePanel(result);
|
|
1660
|
+
};
|
|
1661
|
+
const unsub = this.onChildEvent(childName, (_name, evt) => {
|
|
1662
|
+
if (evt.type !== 'panel-response') return;
|
|
1663
|
+
const e = evt as unknown as PanelResponseEvent;
|
|
1664
|
+
if (e.corrId !== corrId) return;
|
|
1665
|
+
finish({
|
|
1666
|
+
ok: e.ok === true,
|
|
1667
|
+
...(e.data !== undefined ? { data: e.data } : {}),
|
|
1668
|
+
...(typeof e.error === 'string' ? { error: e.error } : {}),
|
|
1669
|
+
...(typeof e.status === 'number' ? { status: e.status } : {}),
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
const timer = setTimeout(() => {
|
|
1673
|
+
finish({ ok: false, error: `panel op '${op}' timed out after ${timeoutMs}ms (child '${childName}' unresponsive)`, status: 504 });
|
|
1674
|
+
}, timeoutMs);
|
|
1675
|
+
try {
|
|
1676
|
+
this.sendToChild(child, { type: 'panel-request', op, ...(params ? { params } : {}), corrId });
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
finish({ ok: false, error: `send to child failed: ${err instanceof Error ? err.message : String(err)}`, status: 502 });
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1624
1683
|
private async killChild(child: FleetChild): Promise<void> {
|
|
1625
1684
|
if (child.status === 'exited' || child.status === 'crashed') return;
|
|
1626
1685
|
const proc = child.process;
|
|
@@ -38,7 +38,18 @@ export type IncomingCommand =
|
|
|
38
38
|
* `cancel-subagent-result`. The child looks the agent up in its own
|
|
39
39
|
* SubagentModule, so this is the only way to stop a subagent that lives
|
|
40
40
|
* in a fleet child rather than the conductor. */
|
|
41
|
-
| { type: 'cancel-subagent'; name: string; corrId?: string }
|
|
41
|
+
| { type: 'cancel-subagent'; name: string; corrId?: string }
|
|
42
|
+
/**
|
|
43
|
+
* Run one operator-panel operation in the child (see PANEL_OPS in
|
|
44
|
+
* src/web/panel-data.ts: mcpl / settings(-update|-reset|-cancel-transition)
|
|
45
|
+
* / pins / pin-add / pin-remove / health / context-makeup /
|
|
46
|
+
* context-coverage / context-curve / context-preview / debug-context).
|
|
47
|
+
* Response is a single `panel-response` with the same corrId. One generic
|
|
48
|
+
* verb rather than a verb per panel: both ends dispatch through the SAME
|
|
49
|
+
* shared handler (`runPanelOp`), so a new panel surface needs no protocol
|
|
50
|
+
* change to work across the fleet.
|
|
51
|
+
*/
|
|
52
|
+
| { type: 'panel-request'; op: string; params?: Record<string, unknown>; corrId?: string };
|
|
42
53
|
|
|
43
54
|
// ---------------------------------------------------------------------------
|
|
44
55
|
// Child → Parent: events
|
|
@@ -134,6 +145,23 @@ export interface CancelSubagentResultEvent {
|
|
|
134
145
|
ts?: number;
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
/** Response to a {type:'panel-request'} request. `data` is the same
|
|
149
|
+
* wire-shaped JSON the WebUI host serves locally for the given op;
|
|
150
|
+
* `ok:false` carries the error plus an HTTP-ish `status` so the parent's
|
|
151
|
+
* proxy routes can answer faithfully (404 unknown agent, 429 preview
|
|
152
|
+
* cooldown, 501 unsupported build). */
|
|
153
|
+
export interface PanelResponseEvent {
|
|
154
|
+
type: 'panel-response';
|
|
155
|
+
corrId?: string;
|
|
156
|
+
/** Echo of the requested op. */
|
|
157
|
+
op: string;
|
|
158
|
+
ok: boolean;
|
|
159
|
+
data?: unknown;
|
|
160
|
+
error?: string;
|
|
161
|
+
status?: number;
|
|
162
|
+
ts?: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
137
165
|
/** Response to a {type:'request-workspace-file'} request. */
|
|
138
166
|
export interface WorkspaceFileSnapshotEvent {
|
|
139
167
|
type: 'workspace-file-snapshot';
|
|
@@ -163,6 +191,7 @@ export type WireEvent =
|
|
|
163
191
|
| WorkspaceTreeSnapshotEvent
|
|
164
192
|
| WorkspaceFileSnapshotEvent
|
|
165
193
|
| CancelSubagentResultEvent
|
|
194
|
+
| PanelResponseEvent
|
|
166
195
|
// Arbitrary framework TraceEvent passthrough. The child stamps every emitted
|
|
167
196
|
// event with `ts: Date.now()` in `emit()` (see headless.ts), so ts is always
|
|
168
197
|
// present on the wire even when the underlying TraceEvent doesn't declare it.
|