@canonmsg/claude-code-plugin 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/host-runtime.d.ts +89 -0
- package/dist/host-runtime.js +152 -0
- package/dist/host.js +40 -103
- package/dist/server.js +148 -0
- package/package.json +6 -4
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
audioUrl?: string | null;
|
|
32
|
+
audioDurationMs?: number | null;
|
|
33
|
+
imageUrl?: string | null;
|
|
34
|
+
attachments?: CanonMessage['attachments'];
|
|
35
|
+
senderType?: CanonMessage['senderType'];
|
|
36
|
+
mentions?: string[] | null;
|
|
37
|
+
};
|
|
38
|
+
interface HostWorkspaceResolverOption {
|
|
39
|
+
id: string;
|
|
40
|
+
cwd: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function buildCanonHostPrompt(input: {
|
|
43
|
+
hostLabel: string;
|
|
44
|
+
content: string;
|
|
45
|
+
conversationId: string;
|
|
46
|
+
participantContext: HostInboundParticipantContext;
|
|
47
|
+
behavior?: ResolvedAgentBehaviorPolicy | null;
|
|
48
|
+
workSession?: MessageCreatedPayload['message']['workSession'];
|
|
49
|
+
workSessions?: MessageCreatedPayload['workSessions'];
|
|
50
|
+
buildInboundContextLines: (context: HostInboundParticipantContext) => string[];
|
|
51
|
+
}): string;
|
|
52
|
+
export declare function renderCanonHostInboundContent(message: HostInboundMessage): string;
|
|
53
|
+
export declare function buildHydratedInboundContext(input: {
|
|
54
|
+
agentId: string;
|
|
55
|
+
conversation: CanonConversation | null;
|
|
56
|
+
page?: CanonMessagesPage | null;
|
|
57
|
+
message: HostInboundMessage;
|
|
58
|
+
senderName: string;
|
|
59
|
+
isOwner: boolean;
|
|
60
|
+
}): {
|
|
61
|
+
participantContext: HostInboundParticipantContext;
|
|
62
|
+
behavior?: ResolvedAgentBehaviorPolicy | null;
|
|
63
|
+
workSessions: NonNullable<MessageCreatedPayload['workSessions']>;
|
|
64
|
+
hydratedFromPage: boolean;
|
|
65
|
+
};
|
|
66
|
+
export declare function publishHostAgentRuntime(agentId: string, clientType: AgentClientType, runtime: AgentRuntime): Promise<void>;
|
|
67
|
+
export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>>) | null;
|
|
68
|
+
export declare function loadHostSessionConfig<TExtra extends string = never>(input: {
|
|
69
|
+
conversationId: string;
|
|
70
|
+
agentId: string;
|
|
71
|
+
extraStringFields?: readonly TExtra[];
|
|
72
|
+
}): Promise<(SessionWorkspaceConfig & Partial<Record<TExtra, string>>) | null>;
|
|
73
|
+
export declare function resolveHostWorkspaceCwd(input: {
|
|
74
|
+
workspaceOptions: HostWorkspaceResolverOption[];
|
|
75
|
+
config: {
|
|
76
|
+
workspaceId?: string;
|
|
77
|
+
retiredWorkspaceConfig?: boolean;
|
|
78
|
+
} | null;
|
|
79
|
+
defaultCwd: string;
|
|
80
|
+
}): string;
|
|
81
|
+
export declare function createConversationMetadataLoader(input: {
|
|
82
|
+
client: CanonClient;
|
|
83
|
+
conversationCache: Map<string, CanonConversation>;
|
|
84
|
+
cacheTtlMs?: number;
|
|
85
|
+
}): {
|
|
86
|
+
refreshConversationCache(force?: boolean): Promise<void>;
|
|
87
|
+
getConversationMeta(conversationId: string): Promise<CanonConversation | null>;
|
|
88
|
+
};
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
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 { buildBehaviorPolicyLines, buildParticipationHistorySnapshot, buildWorkSessionsPromptLines, mergeWorkSessionContexts, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, rtdbRead, rtdbWrite, } from '@canonmsg/core';
|
|
15
|
+
export function buildCanonHostPrompt(input) {
|
|
16
|
+
const resolvedWorkSessions = mergeWorkSessionContexts(input.workSession, input.workSessions);
|
|
17
|
+
return [
|
|
18
|
+
`You are connected to Canon messaging through a ${input.hostLabel} host wrapper.`,
|
|
19
|
+
'Only the last assistant message from this turn will be delivered as the permanent Canon reply.',
|
|
20
|
+
'Short intermediate assistant messages may be shown as ephemeral status while you work.',
|
|
21
|
+
...input.buildInboundContextLines(input.participantContext),
|
|
22
|
+
...buildBehaviorPolicyLines(input.behavior),
|
|
23
|
+
...buildWorkSessionsPromptLines(resolvedWorkSessions),
|
|
24
|
+
'Canon participants may be humans or AI agents.',
|
|
25
|
+
'Honor the Canon behavior policy above when deciding how proactively to participate.',
|
|
26
|
+
...(resolvedWorkSessions.length > 0
|
|
27
|
+
? ['Honor the Canon work-session context above within its stated disclosure limits.']
|
|
28
|
+
: []),
|
|
29
|
+
`Conversation ID: ${input.conversationId}`,
|
|
30
|
+
'',
|
|
31
|
+
'New Canon message:',
|
|
32
|
+
input.content,
|
|
33
|
+
].join('\n');
|
|
34
|
+
}
|
|
35
|
+
export function renderCanonHostInboundContent(message) {
|
|
36
|
+
let content = message.text || '';
|
|
37
|
+
const attachment = message.attachments?.[0];
|
|
38
|
+
if (attachment?.kind === 'audio' && attachment.url) {
|
|
39
|
+
const duration = attachment.durationMs ? ` (${Math.round(attachment.durationMs / 1000)}s)` : '';
|
|
40
|
+
content = content
|
|
41
|
+
? `[Voice message${duration}: ${attachment.url}]\n${content}`
|
|
42
|
+
: `[Voice message${duration}: ${attachment.url}]`;
|
|
43
|
+
}
|
|
44
|
+
else if (attachment?.kind === 'image' && attachment.url) {
|
|
45
|
+
content = content
|
|
46
|
+
? `[Image: ${attachment.url}]\n${content}`
|
|
47
|
+
: `[Image: ${attachment.url}]`;
|
|
48
|
+
}
|
|
49
|
+
else if (attachment?.kind === 'file' && attachment.url) {
|
|
50
|
+
const label = attachment.fileName || 'File';
|
|
51
|
+
content = content
|
|
52
|
+
? `[File: ${label} ${attachment.url}]\n${content}`
|
|
53
|
+
: `[File: ${label} ${attachment.url}]`;
|
|
54
|
+
}
|
|
55
|
+
else if (message.contentType === 'audio' && message.audioUrl) {
|
|
56
|
+
const duration = message.audioDurationMs ? ` (${Math.round(message.audioDurationMs / 1000)}s)` : '';
|
|
57
|
+
content = content
|
|
58
|
+
? `[Voice message${duration}: ${message.audioUrl}]\n${content}`
|
|
59
|
+
: `[Voice message${duration}: ${message.audioUrl}]`;
|
|
60
|
+
}
|
|
61
|
+
else if (message.contentType === 'image' && message.imageUrl) {
|
|
62
|
+
content = content
|
|
63
|
+
? `[Image: ${message.imageUrl}]\n${content}`
|
|
64
|
+
: `[Image: ${message.imageUrl}]`;
|
|
65
|
+
}
|
|
66
|
+
return content || '[Empty message]';
|
|
67
|
+
}
|
|
68
|
+
export function buildHydratedInboundContext(input) {
|
|
69
|
+
const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
|
|
70
|
+
return {
|
|
71
|
+
participantContext: {
|
|
72
|
+
conversationType: input.conversation?.type ?? 'unknown',
|
|
73
|
+
memberCount: input.conversation?.memberIds?.length ?? null,
|
|
74
|
+
senderType: input.message.senderType ?? 'human',
|
|
75
|
+
senderName: input.senderName,
|
|
76
|
+
isOwner: input.isOwner,
|
|
77
|
+
mentionedAgent: Array.isArray(input.message.mentions) && input.message.mentions.includes(input.agentId),
|
|
78
|
+
recentSenderTypes: history.recentSenderTypes,
|
|
79
|
+
recentHumanCount: history.recentHumanCount,
|
|
80
|
+
recentAgentCount: history.recentAgentCount,
|
|
81
|
+
consecutiveAgentTurns: history.consecutiveAgentTurns,
|
|
82
|
+
currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
|
|
83
|
+
},
|
|
84
|
+
behavior: input.page?.behavior ?? input.conversation?.behavior,
|
|
85
|
+
workSessions: input.page?.workSessions ?? [],
|
|
86
|
+
hydratedFromPage: input.page != null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export async function publishHostAgentRuntime(agentId, clientType, runtime) {
|
|
90
|
+
await rtdbWrite(`/agent-runtime/${agentId}`, {
|
|
91
|
+
clientType,
|
|
92
|
+
hostMode: true,
|
|
93
|
+
...runtime,
|
|
94
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export function readHostSessionConfig(raw, extraStringFields = []) {
|
|
98
|
+
const baseConfig = readSessionWorkspaceConfig(raw);
|
|
99
|
+
if (!raw || typeof raw !== 'object') {
|
|
100
|
+
return baseConfig;
|
|
101
|
+
}
|
|
102
|
+
const data = raw;
|
|
103
|
+
const extraConfig = Object.fromEntries(extraStringFields.flatMap((field) => {
|
|
104
|
+
const value = normalizeOptionalString(data[field]);
|
|
105
|
+
return value ? [[field, value]] : [];
|
|
106
|
+
}));
|
|
107
|
+
return {
|
|
108
|
+
...(baseConfig ?? {}),
|
|
109
|
+
...extraConfig,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export async function loadHostSessionConfig(input) {
|
|
113
|
+
const raw = await rtdbRead(`/session-config/${input.conversationId}/${input.agentId}`);
|
|
114
|
+
return readHostSessionConfig(raw, input.extraStringFields);
|
|
115
|
+
}
|
|
116
|
+
export function resolveHostWorkspaceCwd(input) {
|
|
117
|
+
return resolveConfiguredWorkspaceCwd(input);
|
|
118
|
+
}
|
|
119
|
+
export function createConversationMetadataLoader(input) {
|
|
120
|
+
const cacheTtlMs = input.cacheTtlMs ?? 10_000;
|
|
121
|
+
let conversationCacheLoadedAt = 0;
|
|
122
|
+
async function refreshConversationCache(force = false) {
|
|
123
|
+
if (!force
|
|
124
|
+
&& input.conversationCache.size > 0
|
|
125
|
+
&& Date.now() - conversationCacheLoadedAt < cacheTtlMs) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const conversations = await input.client.getConversations();
|
|
129
|
+
input.conversationCache.clear();
|
|
130
|
+
for (const conversation of conversations) {
|
|
131
|
+
input.conversationCache.set(conversation.id, conversation);
|
|
132
|
+
}
|
|
133
|
+
conversationCacheLoadedAt = Date.now();
|
|
134
|
+
}
|
|
135
|
+
async function getConversationMeta(conversationId) {
|
|
136
|
+
try {
|
|
137
|
+
await refreshConversationCache();
|
|
138
|
+
const cached = input.conversationCache.get(conversationId);
|
|
139
|
+
if (cached)
|
|
140
|
+
return cached;
|
|
141
|
+
await refreshConversationCache(true);
|
|
142
|
+
return input.conversationCache.get(conversationId) ?? null;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return input.conversationCache.get(conversationId) ?? null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
refreshConversationCache,
|
|
150
|
+
getConversationMeta,
|
|
151
|
+
};
|
|
152
|
+
}
|
package/dist/host.js
CHANGED
|
@@ -24,7 +24,8 @@ setDefaultResultOrder('ipv4first');
|
|
|
24
24
|
import { parseArgs } from 'node:util';
|
|
25
25
|
import { randomUUID } from 'node:crypto';
|
|
26
26
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
27
|
-
import {
|
|
27
|
+
import { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildPublicWorkspaceOptions, isEnabledFlag, CanonClient, CanonStream, clearTurnState, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, getActiveProfile, releaseLock, initRTDBAuth, rtdbRead, rtdbWrite, writeSessionState, clearSessionState, writeTurnState, releaseConversationEnvironment, } from '@canonmsg/core';
|
|
28
|
+
import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
|
|
28
29
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
29
30
|
let workingDir = process.cwd();
|
|
30
31
|
let workspaceOptions = [];
|
|
@@ -36,27 +37,21 @@ function toModelOptions(models) {
|
|
|
36
37
|
}));
|
|
37
38
|
}
|
|
38
39
|
async function publishAgentRuntime(agentId, runtime) {
|
|
39
|
-
await
|
|
40
|
-
clientType: 'claude-code',
|
|
41
|
-
hostMode: true,
|
|
42
|
-
...runtime,
|
|
43
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
44
|
-
});
|
|
40
|
+
await publishHostAgentRuntime(agentId, 'claude-code', runtime);
|
|
45
41
|
}
|
|
46
42
|
async function loadSessionConfig(conversationId, agentId) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const permissionMode = normalizeOptionalString(data.permissionMode);
|
|
43
|
+
const config = await loadHostSessionConfig({
|
|
44
|
+
conversationId,
|
|
45
|
+
agentId,
|
|
46
|
+
extraStringFields: ['permissionMode'],
|
|
47
|
+
});
|
|
53
48
|
return {
|
|
54
|
-
...
|
|
55
|
-
permissionMode: permissionMode,
|
|
49
|
+
...config,
|
|
50
|
+
permissionMode: config?.permissionMode,
|
|
56
51
|
};
|
|
57
52
|
}
|
|
58
53
|
function resolveWorkspaceCwd(config) {
|
|
59
|
-
return
|
|
54
|
+
return resolveHostWorkspaceCwd({
|
|
60
55
|
workspaceOptions,
|
|
61
56
|
config,
|
|
62
57
|
defaultCwd: workingDir,
|
|
@@ -104,52 +99,14 @@ const CLAUDE_RUNTIME_CAPABILITIES = {
|
|
|
104
99
|
supportsNonFinalPermanentMessages: false,
|
|
105
100
|
};
|
|
106
101
|
function buildCanonPrompt(input) {
|
|
107
|
-
return
|
|
108
|
-
'
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
...buildBehaviorPolicyLines(input.behavior),
|
|
113
|
-
'Canon participants may be humans or AI agents.',
|
|
114
|
-
'Honor the Canon behavior policy above when deciding how proactively to participate.',
|
|
115
|
-
`Conversation ID: ${input.conversationId}`,
|
|
116
|
-
'',
|
|
117
|
-
'New Canon message:',
|
|
118
|
-
input.content,
|
|
119
|
-
].join('\n');
|
|
102
|
+
return buildCanonHostPrompt({
|
|
103
|
+
hostLabel: 'Claude Code',
|
|
104
|
+
buildInboundContextLines,
|
|
105
|
+
...input,
|
|
106
|
+
});
|
|
120
107
|
}
|
|
121
108
|
function renderInboundContent(message) {
|
|
122
|
-
|
|
123
|
-
const attachment = message.attachments?.[0];
|
|
124
|
-
if (attachment?.kind === 'audio' && attachment.url) {
|
|
125
|
-
const duration = attachment.durationMs ? ` (${Math.round(attachment.durationMs / 1000)}s)` : '';
|
|
126
|
-
content = content
|
|
127
|
-
? `[Voice message${duration}: ${attachment.url}]\n${content}`
|
|
128
|
-
: `[Voice message${duration}: ${attachment.url}]`;
|
|
129
|
-
}
|
|
130
|
-
else if (attachment?.kind === 'image' && attachment.url) {
|
|
131
|
-
content = content
|
|
132
|
-
? `[Image: ${attachment.url}]\n${content}`
|
|
133
|
-
: `[Image: ${attachment.url}]`;
|
|
134
|
-
}
|
|
135
|
-
else if (attachment?.kind === 'file' && attachment.url) {
|
|
136
|
-
const label = attachment.fileName || 'File';
|
|
137
|
-
content = content
|
|
138
|
-
? `[File: ${label} ${attachment.url}]\n${content}`
|
|
139
|
-
: `[File: ${label} ${attachment.url}]`;
|
|
140
|
-
}
|
|
141
|
-
else if (message.contentType === 'audio' && message.audioUrl) {
|
|
142
|
-
const duration = message.audioDurationMs ? ` (${Math.round(message.audioDurationMs / 1000)}s)` : '';
|
|
143
|
-
content = content
|
|
144
|
-
? `[Voice message${duration}: ${message.audioUrl}]\n${content}`
|
|
145
|
-
: `[Voice message${duration}: ${message.audioUrl}]`;
|
|
146
|
-
}
|
|
147
|
-
else if (message.contentType === 'image' && message.imageUrl) {
|
|
148
|
-
content = content
|
|
149
|
-
? `[Image: ${message.imageUrl}]\n${content}`
|
|
150
|
-
: `[Image: ${message.imageUrl}]`;
|
|
151
|
-
}
|
|
152
|
-
return content || '[Empty message]';
|
|
109
|
+
return renderCanonHostInboundContent(message);
|
|
153
110
|
}
|
|
154
111
|
// ── Session factory ─────────────────────────────────────────────────
|
|
155
112
|
function createSession(conversationId, environment, agentId, client, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
|
|
@@ -760,50 +717,23 @@ async function main() {
|
|
|
760
717
|
/** Saved SDK session IDs for resume after idle eviction, keyed by conversation + workspace */
|
|
761
718
|
const savedSessionIds = new Map();
|
|
762
719
|
const conversationCache = new Map();
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
const
|
|
769
|
-
conversationCache.clear();
|
|
770
|
-
for (const conversation of conversations) {
|
|
771
|
-
conversationCache.set(conversation.id, conversation);
|
|
772
|
-
}
|
|
773
|
-
conversationCacheLoadedAt = Date.now();
|
|
774
|
-
}
|
|
775
|
-
async function getConversationMeta(conversationId) {
|
|
776
|
-
try {
|
|
777
|
-
await refreshConversationCache();
|
|
778
|
-
const cached = conversationCache.get(conversationId);
|
|
779
|
-
if (cached)
|
|
780
|
-
return cached;
|
|
781
|
-
await refreshConversationCache(true);
|
|
782
|
-
return conversationCache.get(conversationId) ?? null;
|
|
783
|
-
}
|
|
784
|
-
catch {
|
|
785
|
-
return conversationCache.get(conversationId) ?? null;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
async function loadParticipantContext(input) {
|
|
789
|
-
const [conversation, recentMessages] = await Promise.all([
|
|
720
|
+
const { getConversationMeta } = createConversationMetadataLoader({
|
|
721
|
+
client,
|
|
722
|
+
conversationCache,
|
|
723
|
+
});
|
|
724
|
+
async function loadHydratedInboundContext(input) {
|
|
725
|
+
const [conversation, page] = await Promise.all([
|
|
790
726
|
getConversationMeta(input.conversationId),
|
|
791
|
-
client.
|
|
727
|
+
client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
|
|
792
728
|
]);
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
729
|
+
return buildHydratedInboundContext({
|
|
730
|
+
agentId,
|
|
731
|
+
conversation,
|
|
732
|
+
page,
|
|
733
|
+
message: input.message,
|
|
798
734
|
senderName: input.senderName,
|
|
799
735
|
isOwner: input.isOwner,
|
|
800
|
-
|
|
801
|
-
recentSenderTypes: history.recentSenderTypes,
|
|
802
|
-
recentHumanCount: history.recentHumanCount,
|
|
803
|
-
recentAgentCount: history.recentAgentCount,
|
|
804
|
-
consecutiveAgentTurns: history.consecutiveAgentTurns,
|
|
805
|
-
currentAgentStreakStartedByHuman: history.currentAgentStreakStartedByHuman,
|
|
806
|
-
};
|
|
736
|
+
});
|
|
807
737
|
}
|
|
808
738
|
function closeSession(conversationId) {
|
|
809
739
|
const session = sessions.get(conversationId);
|
|
@@ -936,14 +866,19 @@ async function main() {
|
|
|
936
866
|
const content = renderInboundContent(m);
|
|
937
867
|
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
938
868
|
void (async () => {
|
|
939
|
-
const
|
|
940
|
-
const behavior = payload.behavior ?? conversation?.behavior;
|
|
941
|
-
const participantContext = await loadParticipantContext({
|
|
869
|
+
const hydrated = await loadHydratedInboundContext({
|
|
942
870
|
conversationId: convoId,
|
|
943
871
|
message: m,
|
|
944
872
|
senderName: sender,
|
|
945
873
|
isOwner,
|
|
946
874
|
});
|
|
875
|
+
const behavior = payload.behavior ?? hydrated.behavior;
|
|
876
|
+
const workSessions = hydrated.hydratedFromPage
|
|
877
|
+
? hydrated.workSessions
|
|
878
|
+
: Array.isArray(payload.workSessions)
|
|
879
|
+
? payload.workSessions
|
|
880
|
+
: hydrated.workSessions;
|
|
881
|
+
const participantContext = hydrated.participantContext;
|
|
947
882
|
const autoReply = decideAutoReply(participantContext, behavior);
|
|
948
883
|
if (!autoReply.allow) {
|
|
949
884
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
@@ -973,6 +908,8 @@ async function main() {
|
|
|
973
908
|
conversationId: convoId,
|
|
974
909
|
participantContext,
|
|
975
910
|
behavior,
|
|
911
|
+
workSession: m.workSession,
|
|
912
|
+
workSessions,
|
|
976
913
|
}),
|
|
977
914
|
},
|
|
978
915
|
parent_tool_use_id: null,
|
package/dist/server.js
CHANGED
|
@@ -125,6 +125,48 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
125
125
|
required: ['conversation_id'],
|
|
126
126
|
},
|
|
127
127
|
},
|
|
128
|
+
{
|
|
129
|
+
name: 'send_linked_message',
|
|
130
|
+
description: 'Send a message into another Canon conversation under an existing or lazily created shared work session',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
source_conversation_id: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
description: 'The originating conversation for the shared task',
|
|
137
|
+
},
|
|
138
|
+
target_conversation_id: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'The destination conversation to send into',
|
|
141
|
+
},
|
|
142
|
+
text: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'Message text to send',
|
|
145
|
+
},
|
|
146
|
+
work_session_id: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
description: 'Reuse an existing Canon work session when known',
|
|
149
|
+
},
|
|
150
|
+
create_work_session: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
description: 'Optional initial work-session fields when creating a new shared task',
|
|
153
|
+
},
|
|
154
|
+
source_context: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
description: 'Optional scoped context patch for the source conversation',
|
|
157
|
+
},
|
|
158
|
+
target_context: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
description: 'Optional scoped context patch for the target conversation',
|
|
161
|
+
},
|
|
162
|
+
message_options: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
description: 'Optional Canon send options for the linked outbound message',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
required: ['source_conversation_id', 'target_conversation_id', 'text'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
128
170
|
{
|
|
129
171
|
name: 'list_conversations',
|
|
130
172
|
description: "List Canon conversations this agent participates in",
|
|
@@ -234,6 +276,58 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
234
276
|
],
|
|
235
277
|
};
|
|
236
278
|
}
|
|
279
|
+
case 'send_linked_message': {
|
|
280
|
+
const sourceConversationId = args.source_conversation_id;
|
|
281
|
+
const targetConversationId = args.target_conversation_id;
|
|
282
|
+
const text = args.text;
|
|
283
|
+
const workSessionId = typeof args.work_session_id === 'string' ? args.work_session_id : undefined;
|
|
284
|
+
const createWorkSession = typeof args.create_work_session === 'object' && args.create_work_session != null
|
|
285
|
+
? args.create_work_session
|
|
286
|
+
: undefined;
|
|
287
|
+
const sourceContext = typeof args.source_context === 'object' && args.source_context != null
|
|
288
|
+
? args.source_context
|
|
289
|
+
: undefined;
|
|
290
|
+
const targetContext = typeof args.target_context === 'object' && args.target_context != null
|
|
291
|
+
? args.target_context
|
|
292
|
+
: undefined;
|
|
293
|
+
const messageOptions = typeof args.message_options === 'object' && args.message_options != null
|
|
294
|
+
? args.message_options
|
|
295
|
+
: undefined;
|
|
296
|
+
const result = await client.sendLinkedMessage({
|
|
297
|
+
sourceConversationId,
|
|
298
|
+
targetConversationId,
|
|
299
|
+
text,
|
|
300
|
+
...(workSessionId ? { workSessionId } : {}),
|
|
301
|
+
...(createWorkSession
|
|
302
|
+
? {
|
|
303
|
+
createWorkSession: createWorkSession,
|
|
304
|
+
}
|
|
305
|
+
: {}),
|
|
306
|
+
...(sourceContext
|
|
307
|
+
? {
|
|
308
|
+
sourceContext: sourceContext,
|
|
309
|
+
}
|
|
310
|
+
: {}),
|
|
311
|
+
...(targetContext
|
|
312
|
+
? {
|
|
313
|
+
targetContext: targetContext,
|
|
314
|
+
}
|
|
315
|
+
: {}),
|
|
316
|
+
...(messageOptions
|
|
317
|
+
? {
|
|
318
|
+
messageOptions: messageOptions,
|
|
319
|
+
}
|
|
320
|
+
: {}),
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
content: [
|
|
324
|
+
{
|
|
325
|
+
type: 'text',
|
|
326
|
+
text: `Linked message sent (${result.messageId}) via work session ${result.workSessionId}${result.workSessionCreated ? ' [created]' : ''}`,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
237
331
|
case 'list_conversations': {
|
|
238
332
|
const limit = args.limit || 20;
|
|
239
333
|
const convos = await client.getConversations();
|
|
@@ -337,6 +431,60 @@ async function handleInboundMessage(payload) {
|
|
|
337
431
|
meta.behavior_instructions = payload.behavior.instructions.join('\n\n');
|
|
338
432
|
}
|
|
339
433
|
}
|
|
434
|
+
if (m.workSession) {
|
|
435
|
+
meta.work_session_id = m.workSession.id;
|
|
436
|
+
if (m.workSession.title)
|
|
437
|
+
meta.work_session_title = m.workSession.title;
|
|
438
|
+
if (m.workSession.objective)
|
|
439
|
+
meta.work_session_objective = m.workSession.objective;
|
|
440
|
+
if (m.workSession.status)
|
|
441
|
+
meta.work_session_status = m.workSession.status;
|
|
442
|
+
if (m.workSession.currentConversationRole) {
|
|
443
|
+
meta.work_session_role = m.workSession.currentConversationRole;
|
|
444
|
+
}
|
|
445
|
+
if (m.workSession.representation) {
|
|
446
|
+
meta.work_session_representation = m.workSession.representation;
|
|
447
|
+
}
|
|
448
|
+
if (m.workSession.disclosure) {
|
|
449
|
+
meta.work_session_disclosure = m.workSession.disclosure;
|
|
450
|
+
}
|
|
451
|
+
if (m.workSession.summary)
|
|
452
|
+
meta.work_session_summary = m.workSession.summary;
|
|
453
|
+
if (m.workSession.sourceConversationLabel) {
|
|
454
|
+
meta.work_session_source_conversation = m.workSession.sourceConversationLabel;
|
|
455
|
+
}
|
|
456
|
+
else if (m.workSession.sourceConversationId) {
|
|
457
|
+
meta.work_session_source_conversation = m.workSession.sourceConversationId;
|
|
458
|
+
}
|
|
459
|
+
if (m.workSession.sourceParticipantLabel) {
|
|
460
|
+
meta.work_session_source_participant = m.workSession.sourceParticipantLabel;
|
|
461
|
+
}
|
|
462
|
+
else if (m.workSession.sourceParticipantId) {
|
|
463
|
+
meta.work_session_source_participant = m.workSession.sourceParticipantId;
|
|
464
|
+
}
|
|
465
|
+
if (Array.isArray(m.workSession.visibleFacts) && m.workSession.visibleFacts.length > 0) {
|
|
466
|
+
meta.work_session_visible_facts = m.workSession.visibleFacts.join('\n\n');
|
|
467
|
+
}
|
|
468
|
+
if (Array.isArray(m.workSession.pendingQuestions)
|
|
469
|
+
&& m.workSession.pendingQuestions.length > 0) {
|
|
470
|
+
meta.work_session_pending_questions = m.workSession.pendingQuestions.join('\n\n');
|
|
471
|
+
}
|
|
472
|
+
if (Array.isArray(m.workSession.disclosureNotes)
|
|
473
|
+
&& m.workSession.disclosureNotes.length > 0) {
|
|
474
|
+
meta.work_session_disclosure_notes = m.workSession.disclosureNotes.join('\n\n');
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (Array.isArray(payload.workSessions) && payload.workSessions.length > 0) {
|
|
478
|
+
meta.active_work_session_ids = payload.workSessions
|
|
479
|
+
.map((workSession) => workSession.id)
|
|
480
|
+
.join('\n');
|
|
481
|
+
const titled = payload.workSessions
|
|
482
|
+
.map((workSession) => workSession.title || workSession.id)
|
|
483
|
+
.filter((value) => value.length > 0);
|
|
484
|
+
if (titled.length > 0) {
|
|
485
|
+
meta.active_work_session_titles = titled.join('\n');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
340
488
|
const primaryAttachment = getPrimaryAttachment(m);
|
|
341
489
|
if (m.imageUrl)
|
|
342
490
|
meta.image_url = m.imageUrl;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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/server.js",
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
20
20
|
"dev": "tsc --watch",
|
|
21
|
-
"prepack": "npm run build"
|
|
21
|
+
"prepack": "npm run build",
|
|
22
|
+
"test": "vitest run"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@anthropic-ai/claude-agent-sdk": "^0.2.101",
|
|
25
|
-
"@canonmsg/core": "^0.
|
|
26
|
+
"@canonmsg/core": "^0.7.0",
|
|
26
27
|
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
27
28
|
},
|
|
28
29
|
"engines": {
|
|
@@ -47,7 +48,8 @@
|
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@types/node": "^22.0.0",
|
|
50
|
-
"typescript": "~5.7.0"
|
|
51
|
+
"typescript": "~5.7.0",
|
|
52
|
+
"vitest": "^3.0.0"
|
|
51
53
|
},
|
|
52
54
|
"license": "MIT"
|
|
53
55
|
}
|