@canonmsg/claude-code-plugin 0.12.1 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/host.js +38 -98
- package/package.json +3 -3
- package/dist/host-runtime.d.ts +0 -134
- package/dist/host-runtime.js +0 -264
package/dist/host.js
CHANGED
|
@@ -26,8 +26,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
26
26
|
import { existsSync } from 'node:fs';
|
|
27
27
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
28
28
|
import { isAnthropicImageAttachment, materializeMessageMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
|
|
29
|
-
import { CLAUDE_PERMISSION_MODE_OPTIONS, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream,
|
|
30
|
-
import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
|
|
29
|
+
import { CLAUDE_PERMISSION_MODE_OPTIONS, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
31
30
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
32
31
|
import { createStreamingBuffer } from './streaming-buffer.js';
|
|
33
32
|
import { runCli } from './cli-entry.js';
|
|
@@ -47,76 +46,16 @@ let runtimeMetadata = {
|
|
|
47
46
|
};
|
|
48
47
|
let runtimeMetadataRefresh = null;
|
|
49
48
|
function buildClaudeRuntimeDescriptor(input) {
|
|
50
|
-
return {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
id: 'workspace',
|
|
63
|
-
label: 'Project',
|
|
64
|
-
options: input.workspaces.map((workspace) => ({
|
|
65
|
-
value: workspace.id,
|
|
66
|
-
label: workspace.label,
|
|
67
|
-
...(workspace.description ? { description: workspace.description } : {}),
|
|
68
|
-
...(workspace.workspaceRootId ? { workspaceRootId: workspace.workspaceRootId } : {}),
|
|
69
|
-
...(workspace.workspaceRelativePath ? { workspaceRelativePath: workspace.workspaceRelativePath } : {}),
|
|
70
|
-
...(workspace.source ? { source: workspace.source } : {}),
|
|
71
|
-
})),
|
|
72
|
-
defaultValue: input.workspaces[0]?.id ?? null,
|
|
73
|
-
availability: 'setup',
|
|
74
|
-
liveBehavior: 'none',
|
|
75
|
-
selectionPolicy: 'inherit',
|
|
76
|
-
description: input.workspaceRoots?.length
|
|
77
|
-
? 'Choose one of the projects discovered inside the approved local roots for this host.'
|
|
78
|
-
: 'Choose one of the local projects advertised by this host.',
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: 'executionMode',
|
|
82
|
-
label: 'Execution mode',
|
|
83
|
-
options: input.executionModes.map((mode) => ({
|
|
84
|
-
value: mode,
|
|
85
|
-
label: mode === 'worktree' ? 'Isolated worktree' : 'Use shared project',
|
|
86
|
-
description: mode === 'worktree'
|
|
87
|
-
? 'Creates or reuses a per-conversation git worktree under ~/.canon/conversation-worktrees when the selected project is a git repo.'
|
|
88
|
-
: 'Runs directly in the selected project folder. Changes happen there.',
|
|
89
|
-
})),
|
|
90
|
-
defaultValue: null,
|
|
91
|
-
availability: 'setup',
|
|
92
|
-
liveBehavior: 'none',
|
|
93
|
-
selectionPolicy: 'required_explicit',
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
runtimeControls: [
|
|
97
|
-
{
|
|
98
|
-
id: 'permissionMode',
|
|
99
|
-
label: 'Permission mode',
|
|
100
|
-
options: [...CLAUDE_PERMISSION_MODE_OPTIONS],
|
|
101
|
-
defaultValue: 'default',
|
|
102
|
-
availability: 'setup_and_live',
|
|
103
|
-
liveBehavior: 'immediate',
|
|
104
|
-
selectionPolicy: 'inherit',
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
id: 'effort',
|
|
108
|
-
label: 'Thinking level',
|
|
109
|
-
options: [
|
|
110
|
-
{ value: 'low', label: 'Low' },
|
|
111
|
-
{ value: 'medium', label: 'Medium' },
|
|
112
|
-
{ value: 'high', label: 'High' },
|
|
113
|
-
],
|
|
114
|
-
defaultValue: 'medium',
|
|
115
|
-
availability: 'setup_and_live',
|
|
116
|
-
liveBehavior: 'immediate',
|
|
117
|
-
selectionPolicy: 'inherit',
|
|
118
|
-
},
|
|
119
|
-
],
|
|
49
|
+
return buildFirstPartyCodingRuntimeDescriptor({
|
|
50
|
+
clientType: 'claude-code',
|
|
51
|
+
models: input.models,
|
|
52
|
+
workspaces: input.workspaces,
|
|
53
|
+
workspaceRoots: input.workspaceRoots,
|
|
54
|
+
executionModes: input.executionModes,
|
|
55
|
+
permissionModes: CLAUDE_PERMISSION_MODE_OPTIONS,
|
|
56
|
+
defaultPermissionMode: 'default',
|
|
57
|
+
streamingTextMode: 'delta',
|
|
58
|
+
admissionActions: HOST_ADMISSION_ACTIONS_DISABLED,
|
|
120
59
|
actions: [
|
|
121
60
|
{
|
|
122
61
|
id: 'plan',
|
|
@@ -204,11 +143,7 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
204
143
|
dispatch: { kind: 'signal', signal: 'interrupt' },
|
|
205
144
|
},
|
|
206
145
|
],
|
|
207
|
-
|
|
208
|
-
supportsInterrupt: true,
|
|
209
|
-
streamingTextMode: 'delta',
|
|
210
|
-
admissionActions: HOST_ADMISSION_ACTIONS_DISABLED,
|
|
211
|
-
};
|
|
146
|
+
});
|
|
212
147
|
}
|
|
213
148
|
function normalizeClaudeEffortLevel(value) {
|
|
214
149
|
return value === 'low' || value === 'medium' || value === 'high'
|
|
@@ -550,7 +485,7 @@ async function buildCanonUserContent(input) {
|
|
|
550
485
|
];
|
|
551
486
|
}
|
|
552
487
|
// ── Session factory ─────────────────────────────────────────────────
|
|
553
|
-
function createSession(conversationId, environment, agentId, client, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
|
|
488
|
+
function createSession(conversationId, environment, agentId, client, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
|
|
554
489
|
const { cwd } = environment;
|
|
555
490
|
// Per-session input stream with message queue
|
|
556
491
|
let resolveInput = null;
|
|
@@ -757,10 +692,9 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
757
692
|
session.streamingTimer = timer;
|
|
758
693
|
},
|
|
759
694
|
onWrite: (text) => {
|
|
760
|
-
|
|
695
|
+
runtimeState.writeStreaming(conversationId, {
|
|
761
696
|
text,
|
|
762
697
|
status: 'streaming',
|
|
763
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
764
698
|
}).catch(() => { });
|
|
765
699
|
},
|
|
766
700
|
});
|
|
@@ -773,7 +707,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
773
707
|
}
|
|
774
708
|
function clearStreaming() {
|
|
775
709
|
streamingBuffer.clear();
|
|
776
|
-
|
|
710
|
+
runtimeState.clearStreaming(conversationId).catch(() => { });
|
|
777
711
|
}
|
|
778
712
|
function clearIdleResetTimer() {
|
|
779
713
|
if (!session.idleResetTimer)
|
|
@@ -862,7 +796,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
862
796
|
}, FINAL_DELIVERY_RETRY_MS);
|
|
863
797
|
}
|
|
864
798
|
function writeState() {
|
|
865
|
-
writeSessionState(conversationId,
|
|
799
|
+
runtimeState.writeSessionState(conversationId, {
|
|
866
800
|
...session.state,
|
|
867
801
|
...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
|
|
868
802
|
cwd,
|
|
@@ -877,7 +811,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
877
811
|
}).catch(() => { });
|
|
878
812
|
}
|
|
879
813
|
function writeTurn() {
|
|
880
|
-
writeTurnState(conversationId,
|
|
814
|
+
runtimeState.writeTurnState(conversationId, {
|
|
881
815
|
turnId: session.currentTurnId,
|
|
882
816
|
state: session.turnState,
|
|
883
817
|
queueDepth: session.pendingInputs.length,
|
|
@@ -1138,7 +1072,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
1138
1072
|
executionModes: config.availableExecutionModes,
|
|
1139
1073
|
}),
|
|
1140
1074
|
});
|
|
1141
|
-
writeSessionState(conversationId,
|
|
1075
|
+
runtimeState.writeSessionState(conversationId, {
|
|
1142
1076
|
...session.state,
|
|
1143
1077
|
availableModels: modelList,
|
|
1144
1078
|
cwd,
|
|
@@ -1244,6 +1178,11 @@ export async function main() {
|
|
|
1244
1178
|
lastStartedAt: new Date().toISOString(),
|
|
1245
1179
|
lastHeartbeatAt: new Date().toISOString(),
|
|
1246
1180
|
});
|
|
1181
|
+
const runtimeState = createRuntimeStatePublisher({
|
|
1182
|
+
agentId,
|
|
1183
|
+
clientType: 'claude-code',
|
|
1184
|
+
hostMode: true,
|
|
1185
|
+
});
|
|
1247
1186
|
let streamConnected = false;
|
|
1248
1187
|
const hostAvailableExecutionModes = [
|
|
1249
1188
|
...EXECUTION_ENVIRONMENT_MODES,
|
|
@@ -1294,6 +1233,7 @@ export async function main() {
|
|
|
1294
1233
|
runtime: runtimeDescriptor,
|
|
1295
1234
|
workspaceOptions,
|
|
1296
1235
|
defaultCwd: workingDir,
|
|
1236
|
+
extraSessionConfigFields: ['permissionMode', 'effort'],
|
|
1297
1237
|
liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
|
|
1298
1238
|
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
1299
1239
|
return [
|
|
@@ -1334,7 +1274,7 @@ export async function main() {
|
|
|
1334
1274
|
worktreePath: session?.environment.worktreePath ?? null,
|
|
1335
1275
|
fallbackReason: resolveExecutionFallbackReason(session?.environment),
|
|
1336
1276
|
});
|
|
1337
|
-
await writeRuntimeInfo(conversationId,
|
|
1277
|
+
await runtimeState.writeRuntimeInfo(conversationId, payload);
|
|
1338
1278
|
})).catch((error) => {
|
|
1339
1279
|
console.error('[canon-host] Failed to publish runtime info:', error);
|
|
1340
1280
|
});
|
|
@@ -1446,7 +1386,7 @@ export async function main() {
|
|
|
1446
1386
|
if (session.streamingTimer)
|
|
1447
1387
|
clearTimeout(session.streamingTimer);
|
|
1448
1388
|
session.streamingText = '';
|
|
1449
|
-
|
|
1389
|
+
runtimeState.clearStreaming(conversationId).catch(() => { });
|
|
1450
1390
|
if (session.idleResetTimer) {
|
|
1451
1391
|
clearTimeout(session.idleResetTimer);
|
|
1452
1392
|
session.idleResetTimer = null;
|
|
@@ -1455,8 +1395,8 @@ export async function main() {
|
|
|
1455
1395
|
clearTimeout(session.finalDeliveryTimer);
|
|
1456
1396
|
session.finalDeliveryTimer = null;
|
|
1457
1397
|
}
|
|
1458
|
-
clearSessionState(conversationId
|
|
1459
|
-
clearTurnState(conversationId
|
|
1398
|
+
runtimeState.clearSessionState(conversationId).catch(() => { });
|
|
1399
|
+
runtimeState.clearTurnState(conversationId).catch(() => { });
|
|
1460
1400
|
client.setTyping(conversationId, false).catch(() => { });
|
|
1461
1401
|
try {
|
|
1462
1402
|
session.query.close();
|
|
@@ -1510,7 +1450,7 @@ export async function main() {
|
|
|
1510
1450
|
executionMode: environment.mode,
|
|
1511
1451
|
});
|
|
1512
1452
|
const resumeId = savedSessionIds.get(resumeKey) ?? persisted?.claudeSessionId;
|
|
1513
|
-
const session = createSession(conversationId, environment, agentId, client, (convoId, sdkSessionId) => {
|
|
1453
|
+
const session = createSession(conversationId, environment, agentId, client, runtimeState, (convoId, sdkSessionId) => {
|
|
1514
1454
|
// Called when session processing loop ends — save session ID for resume
|
|
1515
1455
|
const existing = sessions.get(convoId);
|
|
1516
1456
|
if (existing) {
|
|
@@ -1533,8 +1473,8 @@ export async function main() {
|
|
|
1533
1473
|
}
|
|
1534
1474
|
}
|
|
1535
1475
|
sessions.delete(convoId);
|
|
1536
|
-
clearSessionState(convoId
|
|
1537
|
-
clearTurnState(convoId
|
|
1476
|
+
runtimeState.clearSessionState(convoId).catch(() => { });
|
|
1477
|
+
runtimeState.clearTurnState(convoId).catch(() => { });
|
|
1538
1478
|
}, (descriptor) => {
|
|
1539
1479
|
runtimeDescriptor = descriptor;
|
|
1540
1480
|
void publishRuntimeHeartbeat();
|
|
@@ -1794,7 +1734,7 @@ export async function main() {
|
|
|
1794
1734
|
},
|
|
1795
1735
|
onDisconnected: () => {
|
|
1796
1736
|
streamConnected = false;
|
|
1797
|
-
|
|
1737
|
+
runtimeState.clearAgentRuntime().catch(() => { });
|
|
1798
1738
|
console.error('[canon-host] SSE disconnected');
|
|
1799
1739
|
},
|
|
1800
1740
|
onError: (err) => console.error(`[canon-host] SSE error: ${err.message}`),
|
|
@@ -1852,7 +1792,7 @@ export async function main() {
|
|
|
1852
1792
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set effort:`, err);
|
|
1853
1793
|
}
|
|
1854
1794
|
}
|
|
1855
|
-
writeSessionState(convoId,
|
|
1795
|
+
runtimeState.writeSessionState(convoId, {
|
|
1856
1796
|
...session.state,
|
|
1857
1797
|
...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
|
|
1858
1798
|
cwd: session.cwd,
|
|
@@ -1880,7 +1820,7 @@ export async function main() {
|
|
|
1880
1820
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Interrupt signal`);
|
|
1881
1821
|
try {
|
|
1882
1822
|
session.turnState = 'interrupted';
|
|
1883
|
-
writeTurnState(convoId,
|
|
1823
|
+
runtimeState.writeTurnState(convoId, {
|
|
1884
1824
|
turnId: session.currentTurnId,
|
|
1885
1825
|
state: 'interrupted',
|
|
1886
1826
|
queueDepth: session.pendingInputs.length,
|
|
@@ -1896,7 +1836,7 @@ export async function main() {
|
|
|
1896
1836
|
session.streamingTimer = null;
|
|
1897
1837
|
}
|
|
1898
1838
|
session.streamingText = '';
|
|
1899
|
-
|
|
1839
|
+
runtimeState.clearStreaming(convoId).catch(() => { });
|
|
1900
1840
|
client.setTyping(convoId, false).catch(() => { });
|
|
1901
1841
|
}
|
|
1902
1842
|
catch (err) {
|
|
@@ -1917,7 +1857,7 @@ export async function main() {
|
|
|
1917
1857
|
// ── Heartbeat (per-session) ──
|
|
1918
1858
|
const heartbeat = setInterval(() => {
|
|
1919
1859
|
for (const session of sessions.values()) {
|
|
1920
|
-
writeSessionState(session.conversationId,
|
|
1860
|
+
runtimeState.writeSessionState(session.conversationId, {
|
|
1921
1861
|
...session.state,
|
|
1922
1862
|
...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
|
|
1923
1863
|
cwd: session.cwd,
|
|
@@ -1927,7 +1867,7 @@ export async function main() {
|
|
|
1927
1867
|
hostMode: true,
|
|
1928
1868
|
isActive: true,
|
|
1929
1869
|
}).catch(() => { });
|
|
1930
|
-
writeTurnState(session.conversationId,
|
|
1870
|
+
runtimeState.writeTurnState(session.conversationId, {
|
|
1931
1871
|
turnId: session.currentTurnId,
|
|
1932
1872
|
state: session.turnState,
|
|
1933
1873
|
queueDepth: session.pendingInputs.length,
|
|
@@ -1962,7 +1902,7 @@ export async function main() {
|
|
|
1962
1902
|
clearInterval(heartbeat);
|
|
1963
1903
|
clearInterval(idleCheck);
|
|
1964
1904
|
stream.stop();
|
|
1965
|
-
await
|
|
1905
|
+
await runtimeState.clearAgentRuntime().catch(() => { });
|
|
1966
1906
|
for (const convoId of [...sessions.keys()]) {
|
|
1967
1907
|
closeSession(convoId);
|
|
1968
1908
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "^0.2.101",
|
|
34
|
-
"@canonmsg/agent-sdk": "^0.10.
|
|
35
|
-
"@canonmsg/core": "^0.
|
|
34
|
+
"@canonmsg/agent-sdk": "^0.10.2",
|
|
35
|
+
"@canonmsg/core": "^0.14.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
package/dist/host-runtime.d.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host-runtime helpers, inlined from @canonmsg/core.
|
|
3
|
-
*
|
|
4
|
-
* These helpers glue Canon host wrappers (Codex, Claude Code) to the Canon
|
|
5
|
-
* RTDB session/runtime surface. They used to live in
|
|
6
|
-
* `@canonmsg/core/src/host-runtime/` but were moved into each plugin in
|
|
7
|
-
* media-parity PR C so core does not have to carry host-specific concerns.
|
|
8
|
-
*
|
|
9
|
-
* Keep this file in lockstep with the equivalent file in
|
|
10
|
-
* `packages/codex-plugin/src/host-runtime.ts`. If you change the
|
|
11
|
-
* behavior here, update that copy too and adjust the shared golden
|
|
12
|
-
* fixture test (`packages/claude-code-plugin/src/host-runtime.test.ts`).
|
|
13
|
-
*/
|
|
14
|
-
import { type AgentClientType, type AgentRuntime, type CanonClient, type CanonConversation, type CanonMessage, type CanonMessagesPage, type MessageCreatedPayload, type ResolvedAgentBehaviorPolicy, type SessionWorkspaceConfig } from '@canonmsg/core';
|
|
15
|
-
export interface HostInboundParticipantContext {
|
|
16
|
-
conversationType: CanonConversation['type'] | 'unknown';
|
|
17
|
-
memberCount: number | null;
|
|
18
|
-
senderType: 'human' | 'ai_agent';
|
|
19
|
-
senderName: string;
|
|
20
|
-
isOwner: boolean;
|
|
21
|
-
mentionedAgent: boolean;
|
|
22
|
-
recentSenderTypes: Array<'human' | 'ai_agent'>;
|
|
23
|
-
recentHumanCount: number;
|
|
24
|
-
recentAgentCount: number;
|
|
25
|
-
consecutiveAgentTurns: number;
|
|
26
|
-
currentAgentStreakStartedByHuman: boolean;
|
|
27
|
-
}
|
|
28
|
-
type HostInboundMessage = {
|
|
29
|
-
text?: string | null;
|
|
30
|
-
contentType?: CanonMessage['contentType'] | null;
|
|
31
|
-
attachments?: CanonMessage['attachments'];
|
|
32
|
-
senderType?: CanonMessage['senderType'];
|
|
33
|
-
mentions?: string[] | null;
|
|
34
|
-
contactCard?: CanonMessage['contactCard'];
|
|
35
|
-
};
|
|
36
|
-
interface HostWorkspaceResolverOption {
|
|
37
|
-
id: string;
|
|
38
|
-
cwd: string;
|
|
39
|
-
}
|
|
40
|
-
export declare const HOST_ADMISSION_ACTION_CAPABILITIES: Readonly<{
|
|
41
|
-
canStartDirectConversation: false;
|
|
42
|
-
canSendContactRequest: false;
|
|
43
|
-
canApprovePendingContactRequests: false;
|
|
44
|
-
canRejectPendingContactRequests: false;
|
|
45
|
-
}>;
|
|
46
|
-
export declare function buildCanonHostPrompt(input: {
|
|
47
|
-
hostLabel: string;
|
|
48
|
-
content: string;
|
|
49
|
-
conversationId: string;
|
|
50
|
-
participantContext: HostInboundParticipantContext;
|
|
51
|
-
behavior?: ResolvedAgentBehaviorPolicy | null;
|
|
52
|
-
workSession?: MessageCreatedPayload['message']['workSession'];
|
|
53
|
-
workSessions?: MessageCreatedPayload['workSessions'];
|
|
54
|
-
buildInboundContextLines: (context: HostInboundParticipantContext) => string[];
|
|
55
|
-
}): string;
|
|
56
|
-
/**
|
|
57
|
-
* Render the **text portion** of an inbound Canon message. Images and other
|
|
58
|
-
* binary attachments are referenced by short placeholders — their actual
|
|
59
|
-
* bytes are delivered to the host as native vision/media inputs (Anthropic
|
|
60
|
-
* image blocks, Codex `-i` file args). URLs are intentionally *not* inlined,
|
|
61
|
-
* since the harness never needs to refetch and earlier `[Image: <url>]`
|
|
62
|
-
* inlining caused vision models to see a string about an image instead of
|
|
63
|
-
* the image itself.
|
|
64
|
-
*
|
|
65
|
-
* `materialized` may be passed so non-image attachments can reference a
|
|
66
|
-
* local path the agent can Read. Without it we fall back to an unadorned
|
|
67
|
-
* placeholder; the vision path still works because image blocks carry the
|
|
68
|
-
* bytes directly.
|
|
69
|
-
*/
|
|
70
|
-
export declare function renderCanonHostInboundContent(message: HostInboundMessage, materialized?: ReadonlyArray<{
|
|
71
|
-
kind: 'image' | 'audio' | 'file';
|
|
72
|
-
path: string;
|
|
73
|
-
fileName?: string;
|
|
74
|
-
durationMs?: number;
|
|
75
|
-
index: number;
|
|
76
|
-
}>): string;
|
|
77
|
-
export declare function buildHydratedInboundContext(input: {
|
|
78
|
-
agentId: string;
|
|
79
|
-
conversation: CanonConversation | null;
|
|
80
|
-
page?: CanonMessagesPage | null;
|
|
81
|
-
message: HostInboundMessage;
|
|
82
|
-
senderName: string;
|
|
83
|
-
isOwner: boolean;
|
|
84
|
-
}): {
|
|
85
|
-
participantContext: HostInboundParticipantContext;
|
|
86
|
-
behavior?: ResolvedAgentBehaviorPolicy | null;
|
|
87
|
-
workSessions: NonNullable<MessageCreatedPayload['workSessions']>;
|
|
88
|
-
hydratedFromPage: boolean;
|
|
89
|
-
};
|
|
90
|
-
export declare function publishHostAgentRuntime(agentId: string, clientType: AgentClientType, runtime: AgentRuntime): Promise<void>;
|
|
91
|
-
export declare function publishHostSessionSnapshots(input: {
|
|
92
|
-
conversationIds: string[];
|
|
93
|
-
agentId: string;
|
|
94
|
-
clientType: AgentClientType;
|
|
95
|
-
runtime: AgentRuntime;
|
|
96
|
-
workspaceOptions: HostWorkspaceResolverOption[];
|
|
97
|
-
defaultCwd: string;
|
|
98
|
-
liveSessionConfigByConversation?: ReadonlyMap<string, {
|
|
99
|
-
model?: string;
|
|
100
|
-
permissionMode?: string;
|
|
101
|
-
effort?: string;
|
|
102
|
-
runtimeControlValues?: Record<string, string>;
|
|
103
|
-
workspaceId?: string;
|
|
104
|
-
executionMode?: SessionWorkspaceConfig['executionMode'];
|
|
105
|
-
executionBranch?: string | null;
|
|
106
|
-
}>;
|
|
107
|
-
}): Promise<void>;
|
|
108
|
-
export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>> & {
|
|
109
|
-
runtimeControlValues?: Record<string, string>;
|
|
110
|
-
}) | null;
|
|
111
|
-
export declare function loadHostSessionConfig<TExtra extends string = never>(input: {
|
|
112
|
-
conversationId: string;
|
|
113
|
-
agentId: string;
|
|
114
|
-
extraStringFields?: readonly TExtra[];
|
|
115
|
-
}): Promise<(SessionWorkspaceConfig & Partial<Record<TExtra, string>> & {
|
|
116
|
-
runtimeControlValues?: Record<string, string>;
|
|
117
|
-
}) | null>;
|
|
118
|
-
export declare function resolveHostWorkspaceCwd(input: {
|
|
119
|
-
workspaceOptions: HostWorkspaceResolverOption[];
|
|
120
|
-
config: {
|
|
121
|
-
workspaceId?: string;
|
|
122
|
-
retiredWorkspaceConfig?: boolean;
|
|
123
|
-
} | null;
|
|
124
|
-
defaultCwd: string;
|
|
125
|
-
}): string;
|
|
126
|
-
export declare function createConversationMetadataLoader(input: {
|
|
127
|
-
client: CanonClient;
|
|
128
|
-
conversationCache: Map<string, CanonConversation>;
|
|
129
|
-
cacheTtlMs?: number;
|
|
130
|
-
}): {
|
|
131
|
-
refreshConversationCache(force?: boolean): Promise<void>;
|
|
132
|
-
getConversationMeta(conversationId: string): Promise<CanonConversation | null>;
|
|
133
|
-
};
|
|
134
|
-
export {};
|
package/dist/host-runtime.js
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host-runtime helpers, inlined from @canonmsg/core.
|
|
3
|
-
*
|
|
4
|
-
* These helpers glue Canon host wrappers (Codex, Claude Code) to the Canon
|
|
5
|
-
* RTDB session/runtime surface. They used to live in
|
|
6
|
-
* `@canonmsg/core/src/host-runtime/` but were moved into each plugin in
|
|
7
|
-
* media-parity PR C so core does not have to carry host-specific concerns.
|
|
8
|
-
*
|
|
9
|
-
* Keep this file in lockstep with the equivalent file in
|
|
10
|
-
* `packages/codex-plugin/src/host-runtime.ts`. If you change the
|
|
11
|
-
* behavior here, update that copy too and adjust the shared golden
|
|
12
|
-
* fixture test (`packages/claude-code-plugin/src/host-runtime.test.ts`).
|
|
13
|
-
*/
|
|
14
|
-
import { buildAgentSessionSnapshot, buildConversationWorktreeSpec, buildBehaviorPolicyLines, buildParticipationHistorySnapshot, buildWorkSessionsPromptLines, mergeWorkSessionContexts, normalizeOptionalString, patchAgentSessionSnapshot, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, rtdbRead, rtdbWrite, } from '@canonmsg/core';
|
|
15
|
-
export const HOST_ADMISSION_ACTION_CAPABILITIES = Object.freeze({
|
|
16
|
-
canStartDirectConversation: false,
|
|
17
|
-
canSendContactRequest: false,
|
|
18
|
-
canApprovePendingContactRequests: false,
|
|
19
|
-
canRejectPendingContactRequests: false,
|
|
20
|
-
});
|
|
21
|
-
export function buildCanonHostPrompt(input) {
|
|
22
|
-
const resolvedWorkSessions = mergeWorkSessionContexts(input.workSession, input.workSessions);
|
|
23
|
-
return [
|
|
24
|
-
`You are connected to Canon messaging through a ${input.hostLabel} host wrapper.`,
|
|
25
|
-
'Only the last assistant message from this turn will be delivered as the permanent Canon reply.',
|
|
26
|
-
'Short intermediate assistant messages may be shown as ephemeral status while you work.',
|
|
27
|
-
...input.buildInboundContextLines(input.participantContext),
|
|
28
|
-
...buildBehaviorPolicyLines(input.behavior),
|
|
29
|
-
...buildWorkSessionsPromptLines(resolvedWorkSessions),
|
|
30
|
-
'Canon participants may be humans or AI agents.',
|
|
31
|
-
'Honor the Canon behavior policy above when deciding how proactively to participate.',
|
|
32
|
-
...(resolvedWorkSessions.length > 0
|
|
33
|
-
? ['Honor the Canon work-session context above within its stated disclosure limits.']
|
|
34
|
-
: []),
|
|
35
|
-
`Conversation ID: ${input.conversationId}`,
|
|
36
|
-
'',
|
|
37
|
-
'New Canon message:',
|
|
38
|
-
input.content,
|
|
39
|
-
].join('\n');
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Render the **text portion** of an inbound Canon message. Images and other
|
|
43
|
-
* binary attachments are referenced by short placeholders — their actual
|
|
44
|
-
* bytes are delivered to the host as native vision/media inputs (Anthropic
|
|
45
|
-
* image blocks, Codex `-i` file args). URLs are intentionally *not* inlined,
|
|
46
|
-
* since the harness never needs to refetch and earlier `[Image: <url>]`
|
|
47
|
-
* inlining caused vision models to see a string about an image instead of
|
|
48
|
-
* the image itself.
|
|
49
|
-
*
|
|
50
|
-
* `materialized` may be passed so non-image attachments can reference a
|
|
51
|
-
* local path the agent can Read. Without it we fall back to an unadorned
|
|
52
|
-
* placeholder; the vision path still works because image blocks carry the
|
|
53
|
-
* bytes directly.
|
|
54
|
-
*/
|
|
55
|
-
export function renderCanonHostInboundContent(message, materialized) {
|
|
56
|
-
const body = message.text || '';
|
|
57
|
-
const placeholders = [];
|
|
58
|
-
const attachments = message.attachments ?? [];
|
|
59
|
-
for (let i = 0; i < attachments.length; i += 1) {
|
|
60
|
-
const att = attachments[i];
|
|
61
|
-
const mat = materialized?.find((m) => m.index === i) ?? null;
|
|
62
|
-
placeholders.push(describeAttachment(att, mat));
|
|
63
|
-
}
|
|
64
|
-
if (message.contentType === 'contact_card' && message.contactCard) {
|
|
65
|
-
placeholders.push(describeContactCard(message.contactCard));
|
|
66
|
-
}
|
|
67
|
-
const rendered = [...placeholders, body].filter(Boolean).join('\n');
|
|
68
|
-
return rendered || '[Empty message]';
|
|
69
|
-
}
|
|
70
|
-
function describeContactCard(card) {
|
|
71
|
-
const parts = [`${card.userType} · userId: ${card.userId}`];
|
|
72
|
-
if (card.ownerName)
|
|
73
|
-
parts.push(`owner: ${card.ownerName}`);
|
|
74
|
-
if (card.about)
|
|
75
|
-
parts.push(`about: ${card.about}`);
|
|
76
|
-
const identity = `📇 Contact card: "${card.displayName}" (${parts.join(' · ')}).`;
|
|
77
|
-
const missingCapabilities = [
|
|
78
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canStartDirectConversation
|
|
79
|
-
? 'start a direct conversation'
|
|
80
|
-
: null,
|
|
81
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canSendContactRequest
|
|
82
|
-
? 'send a contact request'
|
|
83
|
-
: null,
|
|
84
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canApprovePendingContactRequests
|
|
85
|
-
? 'approve pending requests'
|
|
86
|
-
: null,
|
|
87
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canRejectPendingContactRequests
|
|
88
|
-
? 'reject pending requests'
|
|
89
|
-
: null,
|
|
90
|
-
].filter(Boolean).join(', ');
|
|
91
|
-
const hint = `This host can inspect the card, but Canon admission actions are missing here. Missing capabilities: ${missingCapabilities}. Use another Canon surface for userId ${card.userId}.`;
|
|
92
|
-
return `${identity}\n${hint}`;
|
|
93
|
-
}
|
|
94
|
-
function describeAttachment(attachment, materialized) {
|
|
95
|
-
if (attachment.kind === 'image') {
|
|
96
|
-
return '[Image attached]';
|
|
97
|
-
}
|
|
98
|
-
if (attachment.kind === 'audio') {
|
|
99
|
-
const durationMs = materialized?.durationMs ?? attachment.durationMs;
|
|
100
|
-
const duration = durationMs ? ` (${Math.round(durationMs / 1000)}s)` : '';
|
|
101
|
-
const ref = materialized?.path ? ` ${materialized.path}` : '';
|
|
102
|
-
return `[Voice message${duration}${ref}]`;
|
|
103
|
-
}
|
|
104
|
-
// file
|
|
105
|
-
const label = materialized?.fileName ?? attachment.fileName ?? 'File';
|
|
106
|
-
const ref = materialized?.path ? ` ${materialized.path}` : '';
|
|
107
|
-
return `[File: ${label}${ref}]`;
|
|
108
|
-
}
|
|
109
|
-
export function buildHydratedInboundContext(input) {
|
|
110
|
-
const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
|
|
111
|
-
return {
|
|
112
|
-
participantContext: {
|
|
113
|
-
conversationType: input.conversation?.type ?? 'unknown',
|
|
114
|
-
memberCount: input.conversation?.memberIds?.length ?? null,
|
|
115
|
-
senderType: input.message.senderType ?? 'human',
|
|
116
|
-
senderName: input.senderName,
|
|
117
|
-
isOwner: input.isOwner,
|
|
118
|
-
mentionedAgent: Array.isArray(input.message.mentions) && input.message.mentions.includes(input.agentId),
|
|
119
|
-
recentSenderTypes: history.recentSenderTypes,
|
|
120
|
-
recentHumanCount: history.recentHumanCount,
|
|
121
|
-
recentAgentCount: history.recentAgentCount,
|
|
122
|
-
consecutiveAgentTurns: history.consecutiveAgentTurns,
|
|
123
|
-
currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
|
|
124
|
-
},
|
|
125
|
-
behavior: input.page?.behavior ?? input.conversation?.behavior,
|
|
126
|
-
workSessions: input.page?.workSessions ?? [],
|
|
127
|
-
hydratedFromPage: input.page != null,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
export async function publishHostAgentRuntime(agentId, clientType, runtime) {
|
|
131
|
-
await rtdbWrite(`/agent-runtime/${agentId}`, {
|
|
132
|
-
clientType,
|
|
133
|
-
hostMode: true,
|
|
134
|
-
...runtime,
|
|
135
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
export async function publishHostSessionSnapshots(input) {
|
|
139
|
-
if (input.conversationIds.length === 0) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
await Promise.all(input.conversationIds.map(async (conversationId) => {
|
|
143
|
-
const persistedConfig = await loadHostSessionConfig({
|
|
144
|
-
conversationId,
|
|
145
|
-
agentId: input.agentId,
|
|
146
|
-
extraStringFields: ['permissionMode', 'effort'],
|
|
147
|
-
});
|
|
148
|
-
const liveConfig = input.liveSessionConfigByConversation?.get(conversationId) ?? null;
|
|
149
|
-
const mergedConfig = {
|
|
150
|
-
...(persistedConfig ?? {}),
|
|
151
|
-
...(liveConfig ?? {}),
|
|
152
|
-
};
|
|
153
|
-
const snapshot = buildAgentSessionSnapshot({
|
|
154
|
-
conversationId,
|
|
155
|
-
agentId: input.agentId,
|
|
156
|
-
runtime: {
|
|
157
|
-
...input.runtime,
|
|
158
|
-
clientType: input.clientType,
|
|
159
|
-
hostMode: true,
|
|
160
|
-
},
|
|
161
|
-
sessionConfig: {
|
|
162
|
-
...(mergedConfig.model ? { model: mergedConfig.model } : {}),
|
|
163
|
-
...(mergedConfig.permissionMode ? { permissionMode: mergedConfig.permissionMode } : {}),
|
|
164
|
-
...(mergedConfig.effort ? { effort: mergedConfig.effort } : {}),
|
|
165
|
-
...(mergedConfig.runtimeControlValues
|
|
166
|
-
? { runtimeControlValues: mergedConfig.runtimeControlValues }
|
|
167
|
-
: {}),
|
|
168
|
-
...(mergedConfig.workspaceId ? { workspaceId: mergedConfig.workspaceId } : {}),
|
|
169
|
-
...(mergedConfig.executionMode ? { executionMode: mergedConfig.executionMode } : {}),
|
|
170
|
-
},
|
|
171
|
-
lastHeartbeatAt: undefined,
|
|
172
|
-
});
|
|
173
|
-
let executionBranch = liveConfig?.executionBranch ?? null;
|
|
174
|
-
if (!executionBranch && snapshot.executionMode === 'worktree' && snapshot.workspaceId) {
|
|
175
|
-
const workspace = input.workspaceOptions.find((option) => option.id === snapshot.workspaceId);
|
|
176
|
-
if (workspace) {
|
|
177
|
-
executionBranch = buildConversationWorktreeSpec({
|
|
178
|
-
agentId: input.agentId,
|
|
179
|
-
conversationId,
|
|
180
|
-
workspaceCwd: workspace.cwd,
|
|
181
|
-
}).branch;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return patchAgentSessionSnapshot(conversationId, input.agentId, {
|
|
185
|
-
clientType: input.clientType,
|
|
186
|
-
hostMode: true,
|
|
187
|
-
model: snapshot.model ?? null,
|
|
188
|
-
permissionMode: snapshot.permissionMode ?? null,
|
|
189
|
-
effort: snapshot.effort ?? null,
|
|
190
|
-
runtimeControlValues: snapshot.runtimeControlValues ?? null,
|
|
191
|
-
workspaceId: snapshot.workspaceId ?? null,
|
|
192
|
-
executionMode: snapshot.executionMode ?? null,
|
|
193
|
-
executionBranch,
|
|
194
|
-
modelOptions: snapshot.modelOptions,
|
|
195
|
-
permissionModeOptions: snapshot.permissionModeOptions,
|
|
196
|
-
workspaceOptions: snapshot.workspaceOptions,
|
|
197
|
-
availableExecutionModes: snapshot.availableExecutionModes,
|
|
198
|
-
lastHeartbeatAt: { '.sv': 'timestamp' },
|
|
199
|
-
});
|
|
200
|
-
}));
|
|
201
|
-
}
|
|
202
|
-
export function readHostSessionConfig(raw, extraStringFields = []) {
|
|
203
|
-
const baseConfig = readSessionWorkspaceConfig(raw);
|
|
204
|
-
if (!raw || typeof raw !== 'object') {
|
|
205
|
-
return baseConfig;
|
|
206
|
-
}
|
|
207
|
-
const data = raw;
|
|
208
|
-
const extraConfig = Object.fromEntries(extraStringFields.flatMap((field) => {
|
|
209
|
-
const value = normalizeOptionalString(data[field]);
|
|
210
|
-
return value ? [[field, value]] : [];
|
|
211
|
-
}));
|
|
212
|
-
const runtimeControlValues = Object.fromEntries(Object.entries(data.runtimeControlValues && typeof data.runtimeControlValues === 'object'
|
|
213
|
-
? data.runtimeControlValues
|
|
214
|
-
: {}).flatMap(([key, value]) => {
|
|
215
|
-
const normalizedValue = normalizeOptionalString(value);
|
|
216
|
-
return normalizedValue ? [[key, normalizedValue]] : [];
|
|
217
|
-
}));
|
|
218
|
-
return {
|
|
219
|
-
...(baseConfig ?? {}),
|
|
220
|
-
...extraConfig,
|
|
221
|
-
...(Object.keys(runtimeControlValues).length > 0 ? { runtimeControlValues } : {}),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
export async function loadHostSessionConfig(input) {
|
|
225
|
-
const raw = await rtdbRead(`/session-config/${input.conversationId}/${input.agentId}`);
|
|
226
|
-
return readHostSessionConfig(raw, input.extraStringFields);
|
|
227
|
-
}
|
|
228
|
-
export function resolveHostWorkspaceCwd(input) {
|
|
229
|
-
return resolveConfiguredWorkspaceCwd(input);
|
|
230
|
-
}
|
|
231
|
-
export function createConversationMetadataLoader(input) {
|
|
232
|
-
const cacheTtlMs = input.cacheTtlMs ?? 10_000;
|
|
233
|
-
let conversationCacheLoadedAt = 0;
|
|
234
|
-
async function refreshConversationCache(force = false) {
|
|
235
|
-
if (!force
|
|
236
|
-
&& input.conversationCache.size > 0
|
|
237
|
-
&& Date.now() - conversationCacheLoadedAt < cacheTtlMs) {
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
const conversations = await input.client.getConversations();
|
|
241
|
-
input.conversationCache.clear();
|
|
242
|
-
for (const conversation of conversations) {
|
|
243
|
-
input.conversationCache.set(conversation.id, conversation);
|
|
244
|
-
}
|
|
245
|
-
conversationCacheLoadedAt = Date.now();
|
|
246
|
-
}
|
|
247
|
-
async function getConversationMeta(conversationId) {
|
|
248
|
-
try {
|
|
249
|
-
await refreshConversationCache();
|
|
250
|
-
const cached = input.conversationCache.get(conversationId);
|
|
251
|
-
if (cached)
|
|
252
|
-
return cached;
|
|
253
|
-
await refreshConversationCache(true);
|
|
254
|
-
return input.conversationCache.get(conversationId) ?? null;
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
return input.conversationCache.get(conversationId) ?? null;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return {
|
|
261
|
-
refreshConversationCache,
|
|
262
|
-
getConversationMeta,
|
|
263
|
-
};
|
|
264
|
-
}
|