@canonmsg/codex-plugin 0.9.1 → 0.9.3
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 +35 -102
- package/package.json +3 -3
- package/dist/host-runtime.d.ts +0 -133
- package/dist/host-runtime.js +0 -263
package/dist/host.js
CHANGED
|
@@ -3,8 +3,7 @@ import { setDefaultResultOrder } from 'node:dns';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { getCodexImagePath, materializeMessageMedia, } from '@canonmsg/agent-sdk';
|
|
6
|
-
import { buildConfiguredWorkspaceOptionsWithRoots, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream,
|
|
7
|
-
import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
|
|
6
|
+
import { buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, shouldTriggerAgentTurn, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
8
7
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
9
8
|
import { CodexConversationAdapter, } from './adapter.js';
|
|
10
9
|
import { clearStoredThreadId, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
|
|
@@ -26,63 +25,15 @@ let workspaceOptions = [];
|
|
|
26
25
|
let workspaceRoots = [];
|
|
27
26
|
let workspaceRootMetadata = [];
|
|
28
27
|
function buildCodexRuntimeDescriptor(input) {
|
|
29
|
-
return {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
selectionPolicy: 'inherit',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
id: 'workspace',
|
|
42
|
-
label: 'Project',
|
|
43
|
-
options: input.workspaces.map((workspace) => ({
|
|
44
|
-
value: workspace.id,
|
|
45
|
-
label: workspace.label,
|
|
46
|
-
...(workspace.description ? { description: workspace.description } : {}),
|
|
47
|
-
...(workspace.workspaceRootId ? { workspaceRootId: workspace.workspaceRootId } : {}),
|
|
48
|
-
...(workspace.workspaceRelativePath ? { workspaceRelativePath: workspace.workspaceRelativePath } : {}),
|
|
49
|
-
...(workspace.source ? { source: workspace.source } : {}),
|
|
50
|
-
})),
|
|
51
|
-
defaultValue: input.workspaces[0]?.id ?? null,
|
|
52
|
-
availability: 'setup',
|
|
53
|
-
liveBehavior: 'none',
|
|
54
|
-
selectionPolicy: 'inherit',
|
|
55
|
-
description: input.workspaceRoots?.length
|
|
56
|
-
? 'Choose one of the projects discovered inside the approved local roots for this host.'
|
|
57
|
-
: 'Choose one of the local projects advertised by this host.',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
id: 'executionMode',
|
|
61
|
-
label: 'Execution mode',
|
|
62
|
-
options: input.executionModes.map((mode) => ({
|
|
63
|
-
value: mode,
|
|
64
|
-
label: mode === 'worktree' ? 'Isolated worktree' : 'Use shared project',
|
|
65
|
-
description: mode === 'worktree'
|
|
66
|
-
? 'Creates or reuses a per-conversation git worktree under ~/.canon/conversation-worktrees when the selected project is a git repo.'
|
|
67
|
-
: 'Runs directly in the selected project folder. Changes happen there.',
|
|
68
|
-
})),
|
|
69
|
-
defaultValue: null,
|
|
70
|
-
availability: 'setup',
|
|
71
|
-
liveBehavior: 'none',
|
|
72
|
-
selectionPolicy: 'required_explicit',
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
runtimeControls: [
|
|
76
|
-
{
|
|
77
|
-
id: 'permissionMode',
|
|
78
|
-
label: 'Execution policy',
|
|
79
|
-
options: input.permissionModes,
|
|
80
|
-
defaultValue: input.defaultPermissionMode ?? null,
|
|
81
|
-
availability: 'setup',
|
|
82
|
-
liveBehavior: 'none',
|
|
83
|
-
selectionPolicy: 'inherit',
|
|
84
|
-
},
|
|
85
|
-
],
|
|
28
|
+
return buildFirstPartyCodingRuntimeDescriptor({
|
|
29
|
+
clientType: 'codex',
|
|
30
|
+
models: input.models,
|
|
31
|
+
workspaces: input.workspaces,
|
|
32
|
+
workspaceRoots: input.workspaceRoots,
|
|
33
|
+
executionModes: input.executionModes,
|
|
34
|
+
permissionModes: input.permissionModes,
|
|
35
|
+
defaultPermissionMode: input.defaultPermissionMode,
|
|
36
|
+
streamingTextMode: 'snapshot',
|
|
86
37
|
actions: [
|
|
87
38
|
{
|
|
88
39
|
id: 'stop',
|
|
@@ -105,25 +56,13 @@ function buildCodexRuntimeDescriptor(input) {
|
|
|
105
56
|
dispatch: { kind: 'signal', signal: 'stop_and_drop' },
|
|
106
57
|
},
|
|
107
58
|
],
|
|
108
|
-
|
|
109
|
-
supportsInterrupt: true,
|
|
110
|
-
streamingTextMode: 'snapshot',
|
|
111
|
-
};
|
|
59
|
+
});
|
|
112
60
|
}
|
|
113
61
|
function normalizeRuntimeTurnState(value) {
|
|
114
62
|
const normalizedTurn = normalizeTurnState(value);
|
|
115
63
|
if (normalizedTurn) {
|
|
116
64
|
return { state: normalizedTurn.state };
|
|
117
65
|
}
|
|
118
|
-
if (!value || typeof value !== 'object')
|
|
119
|
-
return null;
|
|
120
|
-
const state = value.state;
|
|
121
|
-
if (state === 'running') {
|
|
122
|
-
return { state: 'streaming' };
|
|
123
|
-
}
|
|
124
|
-
if (state === 'requires_action') {
|
|
125
|
-
return { state: 'waiting_input' };
|
|
126
|
-
}
|
|
127
66
|
return null;
|
|
128
67
|
}
|
|
129
68
|
async function publishAgentRuntime(agentId, runtime) {
|
|
@@ -268,6 +207,11 @@ export async function main() {
|
|
|
268
207
|
lastStartedAt: new Date().toISOString(),
|
|
269
208
|
lastHeartbeatAt: new Date().toISOString(),
|
|
270
209
|
});
|
|
210
|
+
const runtimeState = createRuntimeStatePublisher({
|
|
211
|
+
agentId,
|
|
212
|
+
clientType: 'codex',
|
|
213
|
+
hostMode: true,
|
|
214
|
+
});
|
|
271
215
|
const sessions = new Map();
|
|
272
216
|
const pendingSessionCreations = new Map();
|
|
273
217
|
const conversationCache = new Map();
|
|
@@ -294,11 +238,7 @@ export async function main() {
|
|
|
294
238
|
}
|
|
295
239
|
async function loadSenderRuntimeState(conversationId, senderId) {
|
|
296
240
|
try {
|
|
297
|
-
|
|
298
|
-
rtdbRead(`/turn-state/${conversationId}/${senderId}`),
|
|
299
|
-
rtdbRead(`/session-state/${conversationId}/${senderId}`),
|
|
300
|
-
]);
|
|
301
|
-
return normalizeRuntimeTurnState(turnState) ?? normalizeRuntimeTurnState(sessionState);
|
|
241
|
+
return normalizeRuntimeTurnState(await rtdbRead(`/turn-state/${conversationId}/${senderId}`));
|
|
302
242
|
}
|
|
303
243
|
catch {
|
|
304
244
|
return null;
|
|
@@ -321,7 +261,7 @@ export async function main() {
|
|
|
321
261
|
});
|
|
322
262
|
}
|
|
323
263
|
function writeState(session) {
|
|
324
|
-
writeSessionState(session.conversationId,
|
|
264
|
+
runtimeState.writeSessionState(session.conversationId, {
|
|
325
265
|
lastError: session.state.lastError,
|
|
326
266
|
model: session.state.model,
|
|
327
267
|
cwd: session.cwd,
|
|
@@ -333,12 +273,11 @@ export async function main() {
|
|
|
333
273
|
: {}),
|
|
334
274
|
hostMode: true,
|
|
335
275
|
clientType: 'codex',
|
|
336
|
-
state: session.state.state,
|
|
337
276
|
isActive: true,
|
|
338
277
|
}).catch(() => { });
|
|
339
278
|
}
|
|
340
279
|
function writeTurn(session) {
|
|
341
|
-
writeTurnState(session.conversationId,
|
|
280
|
+
runtimeState.writeTurnState(session.conversationId, {
|
|
342
281
|
turnId: session.currentTurnId,
|
|
343
282
|
state: session.turnState,
|
|
344
283
|
queueDepth: session.queue.length,
|
|
@@ -357,7 +296,7 @@ export async function main() {
|
|
|
357
296
|
await client.updateMessageDisposition(conversationId, sourceMessageId, 'accepted_now').catch(() => { });
|
|
358
297
|
}
|
|
359
298
|
function clearStreaming(conversationId) {
|
|
360
|
-
|
|
299
|
+
runtimeState.clearStreaming(conversationId).catch(() => { });
|
|
361
300
|
}
|
|
362
301
|
async function handoffFinalMessage(conversationId) {
|
|
363
302
|
await sleep(FINAL_MESSAGE_HANDOFF_MS);
|
|
@@ -393,8 +332,8 @@ export async function main() {
|
|
|
393
332
|
stopVisibleWorkSignal(session);
|
|
394
333
|
releaseConversationEnvironment(session.environment);
|
|
395
334
|
clearStreaming(conversationId);
|
|
396
|
-
clearSessionState(conversationId
|
|
397
|
-
clearTurnState(conversationId
|
|
335
|
+
runtimeState.clearSessionState(conversationId).catch(() => { });
|
|
336
|
+
runtimeState.clearTurnState(conversationId).catch(() => { });
|
|
398
337
|
client.setTyping(conversationId, false).catch(() => { });
|
|
399
338
|
sessions.delete(conversationId);
|
|
400
339
|
}
|
|
@@ -605,10 +544,9 @@ export async function main() {
|
|
|
605
544
|
writeState(session);
|
|
606
545
|
writeTurn(session);
|
|
607
546
|
startVisibleWorkSignal(session);
|
|
608
|
-
|
|
547
|
+
runtimeState.writeStreaming(session.conversationId, {
|
|
609
548
|
text: 'Thinking…',
|
|
610
549
|
status: 'thinking',
|
|
611
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
612
550
|
}).catch(() => { });
|
|
613
551
|
try {
|
|
614
552
|
const turnImagePaths = nextTurn.imagePaths ?? [];
|
|
@@ -624,10 +562,9 @@ export async function main() {
|
|
|
624
562
|
writeTurn(session);
|
|
625
563
|
stopVisibleWorkSignal(session);
|
|
626
564
|
client.setTyping(session.conversationId, false).catch(() => { });
|
|
627
|
-
|
|
565
|
+
runtimeState.writeStreaming(session.conversationId, {
|
|
628
566
|
text: event.text,
|
|
629
567
|
status: 'streaming',
|
|
630
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
631
568
|
}).catch(() => { });
|
|
632
569
|
return;
|
|
633
570
|
}
|
|
@@ -635,10 +572,9 @@ export async function main() {
|
|
|
635
572
|
session.turnState = 'tool';
|
|
636
573
|
writeTurn(session);
|
|
637
574
|
startVisibleWorkSignal(session);
|
|
638
|
-
|
|
575
|
+
runtimeState.writeStreaming(session.conversationId, {
|
|
639
576
|
text: summarizeCommand(event.command),
|
|
640
577
|
status: 'tool',
|
|
641
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
642
578
|
}).catch(() => { });
|
|
643
579
|
return;
|
|
644
580
|
}
|
|
@@ -772,6 +708,7 @@ export async function main() {
|
|
|
772
708
|
runtime: runtimeDescriptor,
|
|
773
709
|
workspaceOptions,
|
|
774
710
|
defaultCwd: workingDir,
|
|
711
|
+
extraSessionConfigFields: ['permissionMode'],
|
|
775
712
|
liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
|
|
776
713
|
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
777
714
|
return [
|
|
@@ -793,15 +730,11 @@ export async function main() {
|
|
|
793
730
|
? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
|
|
794
731
|
: runtimeDescriptor.defaultWorkspaceId;
|
|
795
732
|
const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
|
|
733
|
+
const descriptor = runtimeDescriptor.runtimeDescriptor;
|
|
734
|
+
if (!descriptor)
|
|
735
|
+
return;
|
|
796
736
|
const payload = {
|
|
797
|
-
descriptor
|
|
798
|
-
models: runtimeDescriptor.availableModels ?? [],
|
|
799
|
-
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
800
|
-
workspaceRoots: workspaceRootMetadata,
|
|
801
|
-
executionModes: hostAvailableExecutionModes,
|
|
802
|
-
permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
|
|
803
|
-
defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
|
|
804
|
-
}),
|
|
737
|
+
descriptor,
|
|
805
738
|
surfaceMode: 'host',
|
|
806
739
|
statusItems: [
|
|
807
740
|
{
|
|
@@ -836,7 +769,7 @@ export async function main() {
|
|
|
836
769
|
'Codex review, compact/rollback, live plan/diff/reasoning updates, PTY command execution, plugin/app/MCP inventory, and structured approvals require the future app-server transport.',
|
|
837
770
|
],
|
|
838
771
|
};
|
|
839
|
-
await writeRuntimeInfo(conversationId,
|
|
772
|
+
await runtimeState.writeRuntimeInfo(conversationId, payload);
|
|
840
773
|
})).catch((error) => {
|
|
841
774
|
console.error('[canon-codex] Failed to publish runtime info:', error);
|
|
842
775
|
});
|
|
@@ -872,7 +805,7 @@ export async function main() {
|
|
|
872
805
|
},
|
|
873
806
|
onDisconnected: () => {
|
|
874
807
|
streamConnected = false;
|
|
875
|
-
|
|
808
|
+
runtimeState.clearAgentRuntime().catch(() => { });
|
|
876
809
|
console.error('[canon-codex] SSE disconnected');
|
|
877
810
|
},
|
|
878
811
|
onError: (error) => console.error(`[canon-codex] SSE error: ${error.message}`),
|
|
@@ -924,8 +857,8 @@ export async function main() {
|
|
|
924
857
|
knownConversationIds.add(conversation.id);
|
|
925
858
|
conversationCache.set(conversation.id, conversation);
|
|
926
859
|
clearStreaming(conversation.id);
|
|
927
|
-
clearSessionState(conversation.id
|
|
928
|
-
clearTurnState(conversation.id
|
|
860
|
+
runtimeState.clearSessionState(conversation.id).catch(() => { });
|
|
861
|
+
runtimeState.clearTurnState(conversation.id).catch(() => { });
|
|
929
862
|
}
|
|
930
863
|
for (const conversation of conversations) {
|
|
931
864
|
const cursor = loadRuntimeSessionState(runtimeId, {
|
|
@@ -1066,7 +999,7 @@ export async function main() {
|
|
|
1066
999
|
clearInterval(heartbeat);
|
|
1067
1000
|
clearInterval(idleCheck);
|
|
1068
1001
|
stream.stop();
|
|
1069
|
-
await
|
|
1002
|
+
await runtimeState.clearAgentRuntime().catch(() => { });
|
|
1070
1003
|
for (const session of [...sessions.values()]) {
|
|
1071
1004
|
await session.adapter.interrupt().catch(() => { });
|
|
1072
1005
|
closeSession(session.conversationId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"prepack": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@canonmsg/agent-sdk": "^0.
|
|
33
|
-
"@canonmsg/core": "^0.
|
|
32
|
+
"@canonmsg/agent-sdk": "^1.0.0",
|
|
33
|
+
"@canonmsg/core": "^0.15.0"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18.0.0"
|
package/dist/host-runtime.d.ts
DELETED
|
@@ -1,133 +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/claude-code-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/codex-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 are
|
|
58
|
-
* referenced by short placeholders — their actual bytes are delivered to the
|
|
59
|
-
* host as native vision/media inputs (Codex `-i <file>`, Anthropic image
|
|
60
|
-
* blocks). URLs are intentionally *not* inlined, since the harness never
|
|
61
|
-
* needs to refetch and earlier `[Image: <url>]` inlining caused vision
|
|
62
|
-
* models to see a string about an image instead of the image itself.
|
|
63
|
-
*
|
|
64
|
-
* `materialized` may be passed so non-image attachments can reference a
|
|
65
|
-
* local path the agent can Read. Without it we fall back to an unadorned
|
|
66
|
-
* placeholder; the vision path still works because image args carry the
|
|
67
|
-
* file path directly.
|
|
68
|
-
*/
|
|
69
|
-
export declare function renderCanonHostInboundContent(message: HostInboundMessage, materialized?: ReadonlyArray<{
|
|
70
|
-
kind: 'image' | 'audio' | 'file';
|
|
71
|
-
path: string;
|
|
72
|
-
fileName?: string;
|
|
73
|
-
durationMs?: number;
|
|
74
|
-
index: number;
|
|
75
|
-
}>): string;
|
|
76
|
-
export declare function buildHydratedInboundContext(input: {
|
|
77
|
-
agentId: string;
|
|
78
|
-
conversation: CanonConversation | null;
|
|
79
|
-
page?: CanonMessagesPage | null;
|
|
80
|
-
message: HostInboundMessage;
|
|
81
|
-
senderName: string;
|
|
82
|
-
isOwner: boolean;
|
|
83
|
-
}): {
|
|
84
|
-
participantContext: HostInboundParticipantContext;
|
|
85
|
-
behavior?: ResolvedAgentBehaviorPolicy | null;
|
|
86
|
-
workSessions: NonNullable<MessageCreatedPayload['workSessions']>;
|
|
87
|
-
hydratedFromPage: boolean;
|
|
88
|
-
};
|
|
89
|
-
export declare function publishHostAgentRuntime(agentId: string, clientType: AgentClientType, runtime: AgentRuntime): Promise<void>;
|
|
90
|
-
export declare function publishHostSessionSnapshots(input: {
|
|
91
|
-
conversationIds: string[];
|
|
92
|
-
agentId: string;
|
|
93
|
-
clientType: AgentClientType;
|
|
94
|
-
runtime: AgentRuntime;
|
|
95
|
-
workspaceOptions: HostWorkspaceResolverOption[];
|
|
96
|
-
defaultCwd: string;
|
|
97
|
-
liveSessionConfigByConversation?: ReadonlyMap<string, {
|
|
98
|
-
model?: string;
|
|
99
|
-
permissionMode?: string;
|
|
100
|
-
effort?: string;
|
|
101
|
-
runtimeControlValues?: Record<string, string>;
|
|
102
|
-
workspaceId?: string;
|
|
103
|
-
executionMode?: SessionWorkspaceConfig['executionMode'];
|
|
104
|
-
executionBranch?: string | null;
|
|
105
|
-
}>;
|
|
106
|
-
}): Promise<void>;
|
|
107
|
-
export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>> & {
|
|
108
|
-
runtimeControlValues?: Record<string, string>;
|
|
109
|
-
}) | null;
|
|
110
|
-
export declare function loadHostSessionConfig<TExtra extends string = never>(input: {
|
|
111
|
-
conversationId: string;
|
|
112
|
-
agentId: string;
|
|
113
|
-
extraStringFields?: readonly TExtra[];
|
|
114
|
-
}): Promise<(SessionWorkspaceConfig & Partial<Record<TExtra, string>> & {
|
|
115
|
-
runtimeControlValues?: Record<string, string>;
|
|
116
|
-
}) | null>;
|
|
117
|
-
export declare function resolveHostWorkspaceCwd(input: {
|
|
118
|
-
workspaceOptions: HostWorkspaceResolverOption[];
|
|
119
|
-
config: {
|
|
120
|
-
workspaceId?: string;
|
|
121
|
-
retiredWorkspaceConfig?: boolean;
|
|
122
|
-
} | null;
|
|
123
|
-
defaultCwd: string;
|
|
124
|
-
}): string;
|
|
125
|
-
export declare function createConversationMetadataLoader(input: {
|
|
126
|
-
client: CanonClient;
|
|
127
|
-
conversationCache: Map<string, CanonConversation>;
|
|
128
|
-
cacheTtlMs?: number;
|
|
129
|
-
}): {
|
|
130
|
-
refreshConversationCache(force?: boolean): Promise<void>;
|
|
131
|
-
getConversationMeta(conversationId: string): Promise<CanonConversation | null>;
|
|
132
|
-
};
|
|
133
|
-
export {};
|
package/dist/host-runtime.js
DELETED
|
@@ -1,263 +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/claude-code-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/codex-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 are
|
|
43
|
-
* referenced by short placeholders — their actual bytes are delivered to the
|
|
44
|
-
* host as native vision/media inputs (Codex `-i <file>`, Anthropic image
|
|
45
|
-
* blocks). URLs are intentionally *not* inlined, since the harness never
|
|
46
|
-
* needs to refetch and earlier `[Image: <url>]` inlining caused vision
|
|
47
|
-
* models to see a string about an image instead of the image itself.
|
|
48
|
-
*
|
|
49
|
-
* `materialized` may be passed so non-image attachments can reference a
|
|
50
|
-
* local path the agent can Read. Without it we fall back to an unadorned
|
|
51
|
-
* placeholder; the vision path still works because image args carry the
|
|
52
|
-
* file path directly.
|
|
53
|
-
*/
|
|
54
|
-
export function renderCanonHostInboundContent(message, materialized) {
|
|
55
|
-
const body = message.text || '';
|
|
56
|
-
const placeholders = [];
|
|
57
|
-
const attachments = message.attachments ?? [];
|
|
58
|
-
for (let i = 0; i < attachments.length; i += 1) {
|
|
59
|
-
const att = attachments[i];
|
|
60
|
-
const mat = materialized?.find((m) => m.index === i) ?? null;
|
|
61
|
-
placeholders.push(describeAttachment(att, mat));
|
|
62
|
-
}
|
|
63
|
-
if (message.contentType === 'contact_card' && message.contactCard) {
|
|
64
|
-
placeholders.push(describeContactCard(message.contactCard));
|
|
65
|
-
}
|
|
66
|
-
const rendered = [...placeholders, body].filter(Boolean).join('\n');
|
|
67
|
-
return rendered || '[Empty message]';
|
|
68
|
-
}
|
|
69
|
-
function describeContactCard(card) {
|
|
70
|
-
const parts = [`${card.userType} · userId: ${card.userId}`];
|
|
71
|
-
if (card.ownerName)
|
|
72
|
-
parts.push(`owner: ${card.ownerName}`);
|
|
73
|
-
if (card.about)
|
|
74
|
-
parts.push(`about: ${card.about}`);
|
|
75
|
-
const identity = `📇 Contact card: "${card.displayName}" (${parts.join(' · ')}).`;
|
|
76
|
-
const missingCapabilities = [
|
|
77
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canStartDirectConversation
|
|
78
|
-
? 'start a direct conversation'
|
|
79
|
-
: null,
|
|
80
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canSendContactRequest
|
|
81
|
-
? 'send a contact request'
|
|
82
|
-
: null,
|
|
83
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canApprovePendingContactRequests
|
|
84
|
-
? 'approve pending requests'
|
|
85
|
-
: null,
|
|
86
|
-
!HOST_ADMISSION_ACTION_CAPABILITIES.canRejectPendingContactRequests
|
|
87
|
-
? 'reject pending requests'
|
|
88
|
-
: null,
|
|
89
|
-
].filter(Boolean).join(', ');
|
|
90
|
-
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}.`;
|
|
91
|
-
return `${identity}\n${hint}`;
|
|
92
|
-
}
|
|
93
|
-
function describeAttachment(attachment, materialized) {
|
|
94
|
-
if (attachment.kind === 'image') {
|
|
95
|
-
return '[Image attached]';
|
|
96
|
-
}
|
|
97
|
-
if (attachment.kind === 'audio') {
|
|
98
|
-
const durationMs = materialized?.durationMs ?? attachment.durationMs;
|
|
99
|
-
const duration = durationMs ? ` (${Math.round(durationMs / 1000)}s)` : '';
|
|
100
|
-
const ref = materialized?.path ? ` ${materialized.path}` : '';
|
|
101
|
-
return `[Voice message${duration}${ref}]`;
|
|
102
|
-
}
|
|
103
|
-
// file
|
|
104
|
-
const label = materialized?.fileName ?? attachment.fileName ?? 'File';
|
|
105
|
-
const ref = materialized?.path ? ` ${materialized.path}` : '';
|
|
106
|
-
return `[File: ${label}${ref}]`;
|
|
107
|
-
}
|
|
108
|
-
export function buildHydratedInboundContext(input) {
|
|
109
|
-
const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
|
|
110
|
-
return {
|
|
111
|
-
participantContext: {
|
|
112
|
-
conversationType: input.conversation?.type ?? 'unknown',
|
|
113
|
-
memberCount: input.conversation?.memberIds?.length ?? null,
|
|
114
|
-
senderType: input.message.senderType ?? 'human',
|
|
115
|
-
senderName: input.senderName,
|
|
116
|
-
isOwner: input.isOwner,
|
|
117
|
-
mentionedAgent: Array.isArray(input.message.mentions) && input.message.mentions.includes(input.agentId),
|
|
118
|
-
recentSenderTypes: history.recentSenderTypes,
|
|
119
|
-
recentHumanCount: history.recentHumanCount,
|
|
120
|
-
recentAgentCount: history.recentAgentCount,
|
|
121
|
-
consecutiveAgentTurns: history.consecutiveAgentTurns,
|
|
122
|
-
currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
|
|
123
|
-
},
|
|
124
|
-
behavior: input.page?.behavior ?? input.conversation?.behavior,
|
|
125
|
-
workSessions: input.page?.workSessions ?? [],
|
|
126
|
-
hydratedFromPage: input.page != null,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
export async function publishHostAgentRuntime(agentId, clientType, runtime) {
|
|
130
|
-
await rtdbWrite(`/agent-runtime/${agentId}`, {
|
|
131
|
-
clientType,
|
|
132
|
-
hostMode: true,
|
|
133
|
-
...runtime,
|
|
134
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
export async function publishHostSessionSnapshots(input) {
|
|
138
|
-
if (input.conversationIds.length === 0) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
await Promise.all(input.conversationIds.map(async (conversationId) => {
|
|
142
|
-
const persistedConfig = await loadHostSessionConfig({
|
|
143
|
-
conversationId,
|
|
144
|
-
agentId: input.agentId,
|
|
145
|
-
extraStringFields: ['permissionMode'],
|
|
146
|
-
});
|
|
147
|
-
const liveConfig = input.liveSessionConfigByConversation?.get(conversationId) ?? null;
|
|
148
|
-
const mergedConfig = {
|
|
149
|
-
...(persistedConfig ?? {}),
|
|
150
|
-
...(liveConfig ?? {}),
|
|
151
|
-
};
|
|
152
|
-
const snapshot = buildAgentSessionSnapshot({
|
|
153
|
-
conversationId,
|
|
154
|
-
agentId: input.agentId,
|
|
155
|
-
runtime: {
|
|
156
|
-
...input.runtime,
|
|
157
|
-
clientType: input.clientType,
|
|
158
|
-
hostMode: true,
|
|
159
|
-
},
|
|
160
|
-
sessionConfig: {
|
|
161
|
-
...(mergedConfig.model ? { model: mergedConfig.model } : {}),
|
|
162
|
-
...(mergedConfig.permissionMode ? { permissionMode: mergedConfig.permissionMode } : {}),
|
|
163
|
-
...(mergedConfig.effort ? { effort: mergedConfig.effort } : {}),
|
|
164
|
-
...(mergedConfig.runtimeControlValues
|
|
165
|
-
? { runtimeControlValues: mergedConfig.runtimeControlValues }
|
|
166
|
-
: {}),
|
|
167
|
-
...(mergedConfig.workspaceId ? { workspaceId: mergedConfig.workspaceId } : {}),
|
|
168
|
-
...(mergedConfig.executionMode ? { executionMode: mergedConfig.executionMode } : {}),
|
|
169
|
-
},
|
|
170
|
-
lastHeartbeatAt: undefined,
|
|
171
|
-
});
|
|
172
|
-
let executionBranch = liveConfig?.executionBranch ?? null;
|
|
173
|
-
if (!executionBranch && snapshot.executionMode === 'worktree' && snapshot.workspaceId) {
|
|
174
|
-
const workspace = input.workspaceOptions.find((option) => option.id === snapshot.workspaceId);
|
|
175
|
-
if (workspace) {
|
|
176
|
-
executionBranch = buildConversationWorktreeSpec({
|
|
177
|
-
agentId: input.agentId,
|
|
178
|
-
conversationId,
|
|
179
|
-
workspaceCwd: workspace.cwd,
|
|
180
|
-
}).branch;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return patchAgentSessionSnapshot(conversationId, input.agentId, {
|
|
184
|
-
clientType: input.clientType,
|
|
185
|
-
hostMode: true,
|
|
186
|
-
model: snapshot.model ?? null,
|
|
187
|
-
permissionMode: snapshot.permissionMode ?? null,
|
|
188
|
-
effort: snapshot.effort ?? null,
|
|
189
|
-
runtimeControlValues: snapshot.runtimeControlValues ?? null,
|
|
190
|
-
workspaceId: snapshot.workspaceId ?? null,
|
|
191
|
-
executionMode: snapshot.executionMode ?? null,
|
|
192
|
-
executionBranch,
|
|
193
|
-
modelOptions: snapshot.modelOptions,
|
|
194
|
-
permissionModeOptions: snapshot.permissionModeOptions,
|
|
195
|
-
workspaceOptions: snapshot.workspaceOptions,
|
|
196
|
-
availableExecutionModes: snapshot.availableExecutionModes,
|
|
197
|
-
lastHeartbeatAt: { '.sv': 'timestamp' },
|
|
198
|
-
});
|
|
199
|
-
}));
|
|
200
|
-
}
|
|
201
|
-
export function readHostSessionConfig(raw, extraStringFields = []) {
|
|
202
|
-
const baseConfig = readSessionWorkspaceConfig(raw);
|
|
203
|
-
if (!raw || typeof raw !== 'object') {
|
|
204
|
-
return baseConfig;
|
|
205
|
-
}
|
|
206
|
-
const data = raw;
|
|
207
|
-
const extraConfig = Object.fromEntries(extraStringFields.flatMap((field) => {
|
|
208
|
-
const value = normalizeOptionalString(data[field]);
|
|
209
|
-
return value ? [[field, value]] : [];
|
|
210
|
-
}));
|
|
211
|
-
const runtimeControlValues = Object.fromEntries(Object.entries(data.runtimeControlValues && typeof data.runtimeControlValues === 'object'
|
|
212
|
-
? data.runtimeControlValues
|
|
213
|
-
: {}).flatMap(([key, value]) => {
|
|
214
|
-
const normalizedValue = normalizeOptionalString(value);
|
|
215
|
-
return normalizedValue ? [[key, normalizedValue]] : [];
|
|
216
|
-
}));
|
|
217
|
-
return {
|
|
218
|
-
...(baseConfig ?? {}),
|
|
219
|
-
...extraConfig,
|
|
220
|
-
...(Object.keys(runtimeControlValues).length > 0 ? { runtimeControlValues } : {}),
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
export async function loadHostSessionConfig(input) {
|
|
224
|
-
const raw = await rtdbRead(`/session-config/${input.conversationId}/${input.agentId}`);
|
|
225
|
-
return readHostSessionConfig(raw, input.extraStringFields);
|
|
226
|
-
}
|
|
227
|
-
export function resolveHostWorkspaceCwd(input) {
|
|
228
|
-
return resolveConfiguredWorkspaceCwd(input);
|
|
229
|
-
}
|
|
230
|
-
export function createConversationMetadataLoader(input) {
|
|
231
|
-
const cacheTtlMs = input.cacheTtlMs ?? 10_000;
|
|
232
|
-
let conversationCacheLoadedAt = 0;
|
|
233
|
-
async function refreshConversationCache(force = false) {
|
|
234
|
-
if (!force
|
|
235
|
-
&& input.conversationCache.size > 0
|
|
236
|
-
&& Date.now() - conversationCacheLoadedAt < cacheTtlMs) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
const conversations = await input.client.getConversations();
|
|
240
|
-
input.conversationCache.clear();
|
|
241
|
-
for (const conversation of conversations) {
|
|
242
|
-
input.conversationCache.set(conversation.id, conversation);
|
|
243
|
-
}
|
|
244
|
-
conversationCacheLoadedAt = Date.now();
|
|
245
|
-
}
|
|
246
|
-
async function getConversationMeta(conversationId) {
|
|
247
|
-
try {
|
|
248
|
-
await refreshConversationCache();
|
|
249
|
-
const cached = input.conversationCache.get(conversationId);
|
|
250
|
-
if (cached)
|
|
251
|
-
return cached;
|
|
252
|
-
await refreshConversationCache(true);
|
|
253
|
-
return input.conversationCache.get(conversationId) ?? null;
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
return input.conversationCache.get(conversationId) ?? null;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return {
|
|
260
|
-
refreshConversationCache,
|
|
261
|
-
getConversationMeta,
|
|
262
|
-
};
|
|
263
|
-
}
|