@canonmsg/codex-plugin 0.9.1 → 0.9.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 +30 -79
- 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,10 +56,7 @@ 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);
|
|
@@ -268,6 +216,11 @@ export async function main() {
|
|
|
268
216
|
lastStartedAt: new Date().toISOString(),
|
|
269
217
|
lastHeartbeatAt: new Date().toISOString(),
|
|
270
218
|
});
|
|
219
|
+
const runtimeState = createRuntimeStatePublisher({
|
|
220
|
+
agentId,
|
|
221
|
+
clientType: 'codex',
|
|
222
|
+
hostMode: true,
|
|
223
|
+
});
|
|
271
224
|
const sessions = new Map();
|
|
272
225
|
const pendingSessionCreations = new Map();
|
|
273
226
|
const conversationCache = new Map();
|
|
@@ -321,7 +274,7 @@ export async function main() {
|
|
|
321
274
|
});
|
|
322
275
|
}
|
|
323
276
|
function writeState(session) {
|
|
324
|
-
writeSessionState(session.conversationId,
|
|
277
|
+
runtimeState.writeSessionState(session.conversationId, {
|
|
325
278
|
lastError: session.state.lastError,
|
|
326
279
|
model: session.state.model,
|
|
327
280
|
cwd: session.cwd,
|
|
@@ -338,7 +291,7 @@ export async function main() {
|
|
|
338
291
|
}).catch(() => { });
|
|
339
292
|
}
|
|
340
293
|
function writeTurn(session) {
|
|
341
|
-
writeTurnState(session.conversationId,
|
|
294
|
+
runtimeState.writeTurnState(session.conversationId, {
|
|
342
295
|
turnId: session.currentTurnId,
|
|
343
296
|
state: session.turnState,
|
|
344
297
|
queueDepth: session.queue.length,
|
|
@@ -357,7 +310,7 @@ export async function main() {
|
|
|
357
310
|
await client.updateMessageDisposition(conversationId, sourceMessageId, 'accepted_now').catch(() => { });
|
|
358
311
|
}
|
|
359
312
|
function clearStreaming(conversationId) {
|
|
360
|
-
|
|
313
|
+
runtimeState.clearStreaming(conversationId).catch(() => { });
|
|
361
314
|
}
|
|
362
315
|
async function handoffFinalMessage(conversationId) {
|
|
363
316
|
await sleep(FINAL_MESSAGE_HANDOFF_MS);
|
|
@@ -393,8 +346,8 @@ export async function main() {
|
|
|
393
346
|
stopVisibleWorkSignal(session);
|
|
394
347
|
releaseConversationEnvironment(session.environment);
|
|
395
348
|
clearStreaming(conversationId);
|
|
396
|
-
clearSessionState(conversationId
|
|
397
|
-
clearTurnState(conversationId
|
|
349
|
+
runtimeState.clearSessionState(conversationId).catch(() => { });
|
|
350
|
+
runtimeState.clearTurnState(conversationId).catch(() => { });
|
|
398
351
|
client.setTyping(conversationId, false).catch(() => { });
|
|
399
352
|
sessions.delete(conversationId);
|
|
400
353
|
}
|
|
@@ -605,10 +558,9 @@ export async function main() {
|
|
|
605
558
|
writeState(session);
|
|
606
559
|
writeTurn(session);
|
|
607
560
|
startVisibleWorkSignal(session);
|
|
608
|
-
|
|
561
|
+
runtimeState.writeStreaming(session.conversationId, {
|
|
609
562
|
text: 'Thinking…',
|
|
610
563
|
status: 'thinking',
|
|
611
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
612
564
|
}).catch(() => { });
|
|
613
565
|
try {
|
|
614
566
|
const turnImagePaths = nextTurn.imagePaths ?? [];
|
|
@@ -624,10 +576,9 @@ export async function main() {
|
|
|
624
576
|
writeTurn(session);
|
|
625
577
|
stopVisibleWorkSignal(session);
|
|
626
578
|
client.setTyping(session.conversationId, false).catch(() => { });
|
|
627
|
-
|
|
579
|
+
runtimeState.writeStreaming(session.conversationId, {
|
|
628
580
|
text: event.text,
|
|
629
581
|
status: 'streaming',
|
|
630
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
631
582
|
}).catch(() => { });
|
|
632
583
|
return;
|
|
633
584
|
}
|
|
@@ -635,10 +586,9 @@ export async function main() {
|
|
|
635
586
|
session.turnState = 'tool';
|
|
636
587
|
writeTurn(session);
|
|
637
588
|
startVisibleWorkSignal(session);
|
|
638
|
-
|
|
589
|
+
runtimeState.writeStreaming(session.conversationId, {
|
|
639
590
|
text: summarizeCommand(event.command),
|
|
640
591
|
status: 'tool',
|
|
641
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
642
592
|
}).catch(() => { });
|
|
643
593
|
return;
|
|
644
594
|
}
|
|
@@ -772,6 +722,7 @@ export async function main() {
|
|
|
772
722
|
runtime: runtimeDescriptor,
|
|
773
723
|
workspaceOptions,
|
|
774
724
|
defaultCwd: workingDir,
|
|
725
|
+
extraSessionConfigFields: ['permissionMode'],
|
|
775
726
|
liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
|
|
776
727
|
const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
|
|
777
728
|
return [
|
|
@@ -836,7 +787,7 @@ export async function main() {
|
|
|
836
787
|
'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
788
|
],
|
|
838
789
|
};
|
|
839
|
-
await writeRuntimeInfo(conversationId,
|
|
790
|
+
await runtimeState.writeRuntimeInfo(conversationId, payload);
|
|
840
791
|
})).catch((error) => {
|
|
841
792
|
console.error('[canon-codex] Failed to publish runtime info:', error);
|
|
842
793
|
});
|
|
@@ -872,7 +823,7 @@ export async function main() {
|
|
|
872
823
|
},
|
|
873
824
|
onDisconnected: () => {
|
|
874
825
|
streamConnected = false;
|
|
875
|
-
|
|
826
|
+
runtimeState.clearAgentRuntime().catch(() => { });
|
|
876
827
|
console.error('[canon-codex] SSE disconnected');
|
|
877
828
|
},
|
|
878
829
|
onError: (error) => console.error(`[canon-codex] SSE error: ${error.message}`),
|
|
@@ -924,8 +875,8 @@ export async function main() {
|
|
|
924
875
|
knownConversationIds.add(conversation.id);
|
|
925
876
|
conversationCache.set(conversation.id, conversation);
|
|
926
877
|
clearStreaming(conversation.id);
|
|
927
|
-
clearSessionState(conversation.id
|
|
928
|
-
clearTurnState(conversation.id
|
|
878
|
+
runtimeState.clearSessionState(conversation.id).catch(() => { });
|
|
879
|
+
runtimeState.clearTurnState(conversation.id).catch(() => { });
|
|
929
880
|
}
|
|
930
881
|
for (const conversation of conversations) {
|
|
931
882
|
const cursor = loadRuntimeSessionState(runtimeId, {
|
|
@@ -1066,7 +1017,7 @@ export async function main() {
|
|
|
1066
1017
|
clearInterval(heartbeat);
|
|
1067
1018
|
clearInterval(idleCheck);
|
|
1068
1019
|
stream.stop();
|
|
1069
|
-
await
|
|
1020
|
+
await runtimeState.clearAgentRuntime().catch(() => { });
|
|
1070
1021
|
for (const session of [...sessions.values()]) {
|
|
1071
1022
|
await session.adapter.interrupt().catch(() => { });
|
|
1072
1023
|
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.2",
|
|
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.10.
|
|
33
|
-
"@canonmsg/core": "^0.
|
|
32
|
+
"@canonmsg/agent-sdk": "^0.10.2",
|
|
33
|
+
"@canonmsg/core": "^0.14.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
|
-
}
|