@canonmsg/core 0.3.0 → 0.5.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/browser.d.ts +3 -1
- package/dist/browser.js +2 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +10 -0
- package/dist/execution-environment.d.ts +59 -0
- package/dist/execution-environment.js +284 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/dist/policy.d.ts +66 -9
- package/dist/policy.js +189 -1
- package/dist/registration.js +1 -0
- package/dist/rtdb-rest.d.ts +2 -0
- package/dist/turn-protocol.d.ts +1 -0
- package/dist/turn-protocol.js +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +0 -1
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
2
|
export type { AgentCapabilities, AgentClientType, AgentRuntime, MediaAttachment, MediaAttachmentKind, ModelOption, SessionConfig, WorkspaceOption, } from './types.js';
|
|
3
|
-
export {
|
|
3
|
+
export type { AgentBehaviorSettings, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationStyle, ResolvedAgentBehaviorPolicy, } from './policy.js';
|
|
4
|
+
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
5
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
4
6
|
export type { DeliveryIntent, InboundDisposition, RuntimeCapabilities, TriggerDecision, TurnLifecycleState, TurnMessageSemantics, TurnMetadata, TurnState, } from './turn-protocol.js';
|
package/dist/browser.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
|
-
export {
|
|
2
|
+
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
3
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CanonMessage, type CanonConversation, type AgentContext, type MediaAttachment, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetStreamingOptions } from './types.js';
|
|
1
|
+
import { type CanonMessage, type CanonConversation, type CanonMessagesPage, type AgentContext, type MediaAttachment, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetStreamingOptions } from './types.js';
|
|
2
2
|
import type { InboundDisposition } from './turn-protocol.js';
|
|
3
3
|
/**
|
|
4
4
|
* Thin REST client for Canon's agent API.
|
|
@@ -17,6 +17,7 @@ export declare class CanonClient {
|
|
|
17
17
|
getAgentMe(): Promise<AgentContext>;
|
|
18
18
|
getConversations(): Promise<CanonConversation[]>;
|
|
19
19
|
getMessages(conversationId: string, limit?: number, before?: string): Promise<CanonMessage[]>;
|
|
20
|
+
getMessagesPage(conversationId: string, limit?: number, before?: string): Promise<CanonMessagesPage>;
|
|
20
21
|
sendMessage(conversationId: string, text: string, options?: SendMessageOptions): Promise<{
|
|
21
22
|
messageId: string;
|
|
22
23
|
}>;
|
|
@@ -46,6 +47,7 @@ export declare class CanonClient {
|
|
|
46
47
|
developerInfo: string;
|
|
47
48
|
avatarUrl?: string;
|
|
48
49
|
clientType?: string;
|
|
50
|
+
requestedAgentId?: string;
|
|
49
51
|
}): Promise<{
|
|
50
52
|
requestId: string;
|
|
51
53
|
}>;
|
package/dist/client.js
CHANGED
|
@@ -53,6 +53,16 @@ export class CanonClient {
|
|
|
53
53
|
const data = await res.json();
|
|
54
54
|
return data.messages;
|
|
55
55
|
}
|
|
56
|
+
async getMessagesPage(conversationId, limit = 50, before) {
|
|
57
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
58
|
+
if (before)
|
|
59
|
+
params.set('before', before);
|
|
60
|
+
params.set('includeBehavior', '1');
|
|
61
|
+
const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, { headers: this.authHeaders() });
|
|
62
|
+
if (!res.ok)
|
|
63
|
+
throw new CanonApiError(res.status, await res.text());
|
|
64
|
+
return res.json();
|
|
65
|
+
}
|
|
56
66
|
async sendMessage(conversationId, text, options) {
|
|
57
67
|
const res = await fetch(`${this.baseUrl}/messages/send`, {
|
|
58
68
|
method: 'POST',
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { WorkspaceOption } from './types.js';
|
|
2
|
+
export type ExecutionEnvironmentMode = 'worktree' | 'locked';
|
|
3
|
+
export interface PreparedExecutionEnvironment {
|
|
4
|
+
cwd: string;
|
|
5
|
+
baseCwd: string;
|
|
6
|
+
mode: ExecutionEnvironmentMode;
|
|
7
|
+
repoRoot?: string;
|
|
8
|
+
branch?: string;
|
|
9
|
+
worktreePath?: string;
|
|
10
|
+
lockPath?: string;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class ExecutionEnvironmentError extends Error {
|
|
14
|
+
readonly userMessage: string;
|
|
15
|
+
constructor(message: string, userMessage?: string);
|
|
16
|
+
}
|
|
17
|
+
interface WorkspaceResolverOption {
|
|
18
|
+
id: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ConfiguredWorkspaceOption extends WorkspaceResolverOption {
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SessionWorkspaceConfig {
|
|
25
|
+
workspaceId?: string;
|
|
26
|
+
legacyCwd?: string;
|
|
27
|
+
model?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function normalizeOptionalString(value: unknown): string | undefined;
|
|
30
|
+
export declare function isEnabledFlag(value: unknown): boolean;
|
|
31
|
+
export declare function buildConversationEnvironmentKey(conversationId: string, workspaceCwd: string): string;
|
|
32
|
+
export declare function buildWorkspaceOptionId(workspaceCwd: string): string;
|
|
33
|
+
export declare function buildConfiguredWorkspaceOptions(primaryCwd: string, configured: string[]): ConfiguredWorkspaceOption[];
|
|
34
|
+
export declare function buildPublicWorkspaceOptions(workspaceOptions: Array<Pick<ConfiguredWorkspaceOption, 'id' | 'label'>>): WorkspaceOption[];
|
|
35
|
+
export declare function readSessionWorkspaceConfig(raw: unknown): SessionWorkspaceConfig | null;
|
|
36
|
+
export declare function resolveConfiguredWorkspaceCwd(input: {
|
|
37
|
+
workspaceOptions: WorkspaceResolverOption[];
|
|
38
|
+
config: {
|
|
39
|
+
workspaceId?: string;
|
|
40
|
+
legacyCwd?: string;
|
|
41
|
+
} | null;
|
|
42
|
+
defaultCwd: string;
|
|
43
|
+
}): string;
|
|
44
|
+
export declare function buildConversationWorktreeSpec(input: {
|
|
45
|
+
agentId: string;
|
|
46
|
+
conversationId: string;
|
|
47
|
+
workspaceCwd: string;
|
|
48
|
+
}): {
|
|
49
|
+
branch: string;
|
|
50
|
+
worktreePath: string;
|
|
51
|
+
};
|
|
52
|
+
export declare function prepareConversationEnvironment(input: {
|
|
53
|
+
agentId: string;
|
|
54
|
+
conversationId: string;
|
|
55
|
+
workspaceCwd: string;
|
|
56
|
+
allowWorktrees?: boolean;
|
|
57
|
+
}): PreparedExecutionEnvironment;
|
|
58
|
+
export declare function releaseConversationEnvironment(environment: Pick<PreparedExecutionEnvironment, 'lockPath'>): void;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { CANON_DIR, isProcessAlive } from './agent-profiles.js';
|
|
6
|
+
export class ExecutionEnvironmentError extends Error {
|
|
7
|
+
userMessage;
|
|
8
|
+
constructor(message, userMessage = message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'ExecutionEnvironmentError';
|
|
11
|
+
this.userMessage = userMessage;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const WORKTREE_ROOT = join(CANON_DIR, 'conversation-worktrees');
|
|
15
|
+
const WORKSPACE_LOCKS_ROOT = join(CANON_DIR, 'workspace-locks');
|
|
16
|
+
export function normalizeOptionalString(value) {
|
|
17
|
+
if (typeof value !== 'string')
|
|
18
|
+
return undefined;
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
return trimmed ? trimmed : undefined;
|
|
21
|
+
}
|
|
22
|
+
export function isEnabledFlag(value) {
|
|
23
|
+
if (value === true)
|
|
24
|
+
return true;
|
|
25
|
+
if (typeof value !== 'string')
|
|
26
|
+
return false;
|
|
27
|
+
const normalized = value.trim().toLowerCase();
|
|
28
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
29
|
+
}
|
|
30
|
+
function shortHash(value) {
|
|
31
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
32
|
+
}
|
|
33
|
+
function sanitizeSegment(value) {
|
|
34
|
+
const normalized = value
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
37
|
+
.replace(/^-+|-+$/g, '');
|
|
38
|
+
return normalized.slice(0, 24) || 'workspace';
|
|
39
|
+
}
|
|
40
|
+
function runGit(cwd, args) {
|
|
41
|
+
const result = spawnSync('git', ['-C', cwd, ...args], {
|
|
42
|
+
encoding: 'utf-8',
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
ok: result.status === 0,
|
|
46
|
+
stdout: result.stdout ?? '',
|
|
47
|
+
stderr: result.stderr ?? '',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function detectRepoRoot(cwd) {
|
|
51
|
+
const result = runGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
52
|
+
if (!result.ok)
|
|
53
|
+
return null;
|
|
54
|
+
const repoRoot = result.stdout.trim();
|
|
55
|
+
return repoRoot ? resolve(repoRoot) : null;
|
|
56
|
+
}
|
|
57
|
+
function isRepoClean(repoRoot) {
|
|
58
|
+
const result = runGit(repoRoot, ['status', '--porcelain']);
|
|
59
|
+
return result.ok && result.stdout.trim() === '';
|
|
60
|
+
}
|
|
61
|
+
function branchExists(repoRoot, branch) {
|
|
62
|
+
return runGit(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]).ok;
|
|
63
|
+
}
|
|
64
|
+
function loadWorkspaceLock(lockPath) {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function cleanupStaleWorkspaceLock(lockPath) {
|
|
73
|
+
const record = loadWorkspaceLock(lockPath);
|
|
74
|
+
if (!record) {
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(lockPath);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (isProcessAlive(record.pid))
|
|
84
|
+
return false;
|
|
85
|
+
try {
|
|
86
|
+
unlinkSync(lockPath);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function acquireWorkspaceLock(input) {
|
|
94
|
+
mkdirSync(WORKSPACE_LOCKS_ROOT, { recursive: true });
|
|
95
|
+
const workspaceCwd = resolve(input.workspaceCwd);
|
|
96
|
+
const lockPath = join(WORKSPACE_LOCKS_ROOT, `${shortHash(workspaceCwd)}.json`);
|
|
97
|
+
const record = {
|
|
98
|
+
pid: process.pid,
|
|
99
|
+
agentId: input.agentId,
|
|
100
|
+
conversationId: input.conversationId,
|
|
101
|
+
workspaceCwd,
|
|
102
|
+
createdAt: new Date().toISOString(),
|
|
103
|
+
};
|
|
104
|
+
while (true) {
|
|
105
|
+
try {
|
|
106
|
+
writeFileSync(lockPath, JSON.stringify(record, null, 2), { flag: 'wx' });
|
|
107
|
+
return lockPath;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const code = error.code;
|
|
111
|
+
if (code !== 'EEXIST') {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
const existing = loadWorkspaceLock(lockPath);
|
|
115
|
+
if (!existing) {
|
|
116
|
+
if (cleanupStaleWorkspaceLock(lockPath))
|
|
117
|
+
continue;
|
|
118
|
+
throw new Error(`Workspace lock file is unreadable: ${workspaceCwd}`);
|
|
119
|
+
}
|
|
120
|
+
if (!isProcessAlive(existing.pid)) {
|
|
121
|
+
if (cleanupStaleWorkspaceLock(lockPath))
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (existing.pid === process.pid
|
|
125
|
+
&& existing.agentId === input.agentId
|
|
126
|
+
&& existing.conversationId === input.conversationId) {
|
|
127
|
+
return lockPath;
|
|
128
|
+
}
|
|
129
|
+
throw new ExecutionEnvironmentError(`Workspace is already in use by Canon session ${existing.conversationId.slice(0, 8)} (PID ${existing.pid}).`, 'That workspace is already in use by another Canon coding session on this machine.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function buildConversationEnvironmentKey(conversationId, workspaceCwd) {
|
|
134
|
+
return `${conversationId}:${resolve(workspaceCwd)}`;
|
|
135
|
+
}
|
|
136
|
+
export function buildWorkspaceOptionId(workspaceCwd) {
|
|
137
|
+
return `workspace-${shortHash(resolve(workspaceCwd))}`;
|
|
138
|
+
}
|
|
139
|
+
export function buildConfiguredWorkspaceOptions(primaryCwd, configured) {
|
|
140
|
+
const uniqueDirs = Array.from(new Set([primaryCwd, ...configured].map((dir) => resolve(dir))));
|
|
141
|
+
const seenLabels = new Map();
|
|
142
|
+
return uniqueDirs.map((cwd) => {
|
|
143
|
+
const baseLabel = basename(cwd) || cwd;
|
|
144
|
+
const seenCount = (seenLabels.get(baseLabel) ?? 0) + 1;
|
|
145
|
+
seenLabels.set(baseLabel, seenCount);
|
|
146
|
+
return {
|
|
147
|
+
id: buildWorkspaceOptionId(cwd),
|
|
148
|
+
label: seenCount === 1 ? baseLabel : `${baseLabel} (${seenCount})`,
|
|
149
|
+
cwd,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export function buildPublicWorkspaceOptions(workspaceOptions) {
|
|
154
|
+
return workspaceOptions.map(({ id, label }) => ({ id, label }));
|
|
155
|
+
}
|
|
156
|
+
export function readSessionWorkspaceConfig(raw) {
|
|
157
|
+
if (!raw || typeof raw !== 'object')
|
|
158
|
+
return null;
|
|
159
|
+
const data = raw;
|
|
160
|
+
return {
|
|
161
|
+
workspaceId: normalizeOptionalString(data.workspaceId),
|
|
162
|
+
legacyCwd: normalizeOptionalString(data.cwd),
|
|
163
|
+
model: normalizeOptionalString(data.model),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function findWorkspaceByLegacyCwd(workspaceOptions, legacyCwd) {
|
|
167
|
+
if (!legacyCwd)
|
|
168
|
+
return undefined;
|
|
169
|
+
const resolvedLegacyCwd = resolve(legacyCwd);
|
|
170
|
+
return workspaceOptions.find((workspace) => resolve(workspace.cwd) === resolvedLegacyCwd);
|
|
171
|
+
}
|
|
172
|
+
export function resolveConfiguredWorkspaceCwd(input) {
|
|
173
|
+
const fallbackCwd = input.workspaceOptions[0]?.cwd ?? resolve(input.defaultCwd);
|
|
174
|
+
if (!input.config)
|
|
175
|
+
return fallbackCwd;
|
|
176
|
+
const legacyWorkspace = findWorkspaceByLegacyCwd(input.workspaceOptions, input.config.legacyCwd);
|
|
177
|
+
const workspaceId = input.config.workspaceId;
|
|
178
|
+
if (workspaceId) {
|
|
179
|
+
const workspace = input.workspaceOptions.find((option) => option.id === workspaceId);
|
|
180
|
+
if (workspace)
|
|
181
|
+
return workspace.cwd;
|
|
182
|
+
if (workspaceId === 'default') {
|
|
183
|
+
return fallbackCwd;
|
|
184
|
+
}
|
|
185
|
+
const legacyWorkspaceMatch = /^workspace-(\d+)$/.exec(workspaceId);
|
|
186
|
+
if (legacyWorkspaceMatch) {
|
|
187
|
+
const legacyIndex = Number.parseInt(legacyWorkspaceMatch[1] ?? '', 10) - 1;
|
|
188
|
+
if (legacyIndex >= 0 && input.workspaceOptions[legacyIndex]) {
|
|
189
|
+
return input.workspaceOptions[legacyIndex].cwd;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (legacyWorkspace)
|
|
193
|
+
return legacyWorkspace.cwd;
|
|
194
|
+
throw new ExecutionEnvironmentError(`Workspace ${workspaceId} is not configured on this machine.`, 'The workspace saved for this Canon coding session is no longer configured on this machine.');
|
|
195
|
+
}
|
|
196
|
+
if (legacyWorkspace)
|
|
197
|
+
return legacyWorkspace.cwd;
|
|
198
|
+
return fallbackCwd;
|
|
199
|
+
}
|
|
200
|
+
export function buildConversationWorktreeSpec(input) {
|
|
201
|
+
const resolvedWorkspace = resolve(input.workspaceCwd);
|
|
202
|
+
const baseLabel = sanitizeSegment(basename(resolvedWorkspace) || 'workspace');
|
|
203
|
+
const workspaceHash = shortHash(resolvedWorkspace);
|
|
204
|
+
const conversationHash = shortHash(`${input.agentId}:${input.conversationId}`);
|
|
205
|
+
return {
|
|
206
|
+
branch: `canon/${baseLabel}-${workspaceHash.slice(0, 6)}-${conversationHash.slice(0, 8)}`,
|
|
207
|
+
worktreePath: join(WORKTREE_ROOT, baseLabel, workspaceHash, conversationHash),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export function prepareConversationEnvironment(input) {
|
|
211
|
+
const baseCwd = resolve(input.workspaceCwd);
|
|
212
|
+
const allowWorktrees = input.allowWorktrees === true;
|
|
213
|
+
if (!allowWorktrees) {
|
|
214
|
+
return {
|
|
215
|
+
cwd: baseCwd,
|
|
216
|
+
baseCwd,
|
|
217
|
+
mode: 'locked',
|
|
218
|
+
lockPath: acquireWorkspaceLock({
|
|
219
|
+
agentId: input.agentId,
|
|
220
|
+
conversationId: input.conversationId,
|
|
221
|
+
workspaceCwd: baseCwd,
|
|
222
|
+
}),
|
|
223
|
+
reason: 'Worktree isolation is disabled for this host',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const repoRoot = detectRepoRoot(baseCwd);
|
|
227
|
+
if (repoRoot) {
|
|
228
|
+
const spec = buildConversationWorktreeSpec(input);
|
|
229
|
+
const relativeCwd = relative(repoRoot, baseCwd);
|
|
230
|
+
const sessionCwd = relativeCwd ? join(spec.worktreePath, relativeCwd) : spec.worktreePath;
|
|
231
|
+
if (existsSync(spec.worktreePath)) {
|
|
232
|
+
if (detectRepoRoot(spec.worktreePath)) {
|
|
233
|
+
return {
|
|
234
|
+
cwd: sessionCwd,
|
|
235
|
+
baseCwd,
|
|
236
|
+
mode: 'worktree',
|
|
237
|
+
repoRoot,
|
|
238
|
+
branch: spec.branch,
|
|
239
|
+
worktreePath: spec.worktreePath,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (isRepoClean(repoRoot)) {
|
|
244
|
+
mkdirSync(dirname(spec.worktreePath), { recursive: true });
|
|
245
|
+
const addArgs = branchExists(repoRoot, spec.branch)
|
|
246
|
+
? ['worktree', 'add', spec.worktreePath, spec.branch]
|
|
247
|
+
: ['worktree', 'add', '-b', spec.branch, spec.worktreePath, 'HEAD'];
|
|
248
|
+
const addResult = runGit(repoRoot, addArgs);
|
|
249
|
+
if (addResult.ok && detectRepoRoot(spec.worktreePath)) {
|
|
250
|
+
return {
|
|
251
|
+
cwd: sessionCwd,
|
|
252
|
+
baseCwd,
|
|
253
|
+
mode: 'worktree',
|
|
254
|
+
repoRoot,
|
|
255
|
+
branch: spec.branch,
|
|
256
|
+
worktreePath: spec.worktreePath,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
cwd: baseCwd,
|
|
263
|
+
baseCwd,
|
|
264
|
+
mode: 'locked',
|
|
265
|
+
lockPath: acquireWorkspaceLock({
|
|
266
|
+
agentId: input.agentId,
|
|
267
|
+
conversationId: input.conversationId,
|
|
268
|
+
workspaceCwd: baseCwd,
|
|
269
|
+
}),
|
|
270
|
+
reason: repoRoot
|
|
271
|
+
? 'Base repository is dirty or worktree creation failed'
|
|
272
|
+
: 'Workspace is not a git repository',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export function releaseConversationEnvironment(environment) {
|
|
276
|
+
if (!environment.lockPath)
|
|
277
|
+
return;
|
|
278
|
+
try {
|
|
279
|
+
unlinkSync(environment.lockPath);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Ignore lock cleanup failures; stale locks are cleaned up on next acquire.
|
|
283
|
+
}
|
|
284
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
|
-
export type { AgentCapabilities, AgentClientType, CanonMessage, CanonConversation, AgentContext, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, ModelOption, WorkspaceOption, } from './types.js';
|
|
2
|
+
export type { AgentCapabilities, AgentClientType, CanonMessage, CanonConversation, CanonMessagesPage, AgentContext, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, ModelOption, WorkspaceOption, } from './types.js';
|
|
3
3
|
export { CanonClient, CanonApiError } from './client.js';
|
|
4
4
|
export { CanonStream } from './stream.js';
|
|
5
5
|
export type { StreamHandler } from './stream.js';
|
|
6
|
-
export type { PolicyRole, ParticipationStyle, RepresentationMode, PermissionLevel, ConversationScope, Participant, Relationship, ContextOverlay, BehaviorProfile, AdmissionPolicy, RuntimeControlPolicy, ActionApprovalPolicy, ParticipationPolicy, WorkSession, ResolvedPolicy, ResolvedTurnEligibility, } from './policy.js';
|
|
7
|
-
export {
|
|
6
|
+
export type { PolicyRole, ParticipationStyle, RepresentationMode, PermissionLevel, ConversationScope, AgentBehaviorSettings, Participant, Relationship, ContextOverlay, BehaviorProfile, AdmissionPolicy, RuntimeControlPolicy, ActionApprovalPolicy, ParticipationPolicy, WorkSession, ResolvedPolicy, ResolvedTurnEligibility, ResolvedAgentBehaviorPolicy, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationDecisionInput, ParticipationDecision, } from './policy.js';
|
|
7
|
+
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
8
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
8
9
|
export type { DeliveryIntent, TurnMessageSemantics, InboundDisposition, TurnLifecycleState, RuntimeCapabilities, TurnState, TurnMetadata, TriggerDecision, } from './turn-protocol.js';
|
|
9
10
|
export { registerAndWaitForApproval } from './registration.js';
|
|
10
11
|
export { ApprovalManager } from './approval-manager.js';
|
|
@@ -17,6 +18,8 @@ export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive
|
|
|
17
18
|
export type { AgentProfile } from './agent-profiles.js';
|
|
18
19
|
export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
|
|
19
20
|
export type { ResolvedAgent } from './agent-resolver.js';
|
|
21
|
+
export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, isEnabledFlag, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
|
|
22
|
+
export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecutionEnvironment, SessionWorkspaceConfig, } from './execution-environment.js';
|
|
20
23
|
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
|
|
21
24
|
export type { SessionStatePayload, TurnStatePayload } from './rtdb-rest.js';
|
|
22
25
|
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,9 @@ export { AGENT_CAPABILITIES, } from './types.js';
|
|
|
4
4
|
export { CanonClient, CanonApiError } from './client.js';
|
|
5
5
|
// Stream
|
|
6
6
|
export { CanonStream } from './stream.js';
|
|
7
|
+
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
7
8
|
// Turn protocol
|
|
8
|
-
export { DEFAULT_RUNTIME_CAPABILITIES, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
9
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
9
10
|
// Registration
|
|
10
11
|
export { registerAndWaitForApproval } from './registration.js';
|
|
11
12
|
// Approval
|
|
@@ -18,6 +19,8 @@ export { createStreamingHelper } from './streaming.js';
|
|
|
18
19
|
export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
|
|
19
20
|
// Agent resolver
|
|
20
21
|
export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
|
|
22
|
+
// Execution environments for host-mode coding sessions
|
|
23
|
+
export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, isEnabledFlag, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
|
|
21
24
|
// RTDB REST helpers (token exchange, session state, generic read/write)
|
|
22
25
|
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
|
|
23
26
|
// Constants
|
package/dist/policy.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export type PolicyRole =
|
|
2
|
-
export type ParticipationStyle =
|
|
3
|
-
export type RepresentationMode =
|
|
4
|
-
export type PermissionLevel =
|
|
5
|
-
export type ConversationScope =
|
|
1
|
+
export type PolicyRole = "owner" | "conversation_member" | "group_admin" | "external_requester" | "agent_self";
|
|
2
|
+
export type ParticipationStyle = "natural" | "collaborative" | "mention-first" | "approval-gated" | "handoff-only" | "observer";
|
|
3
|
+
export type RepresentationMode = "self" | "delegate";
|
|
4
|
+
export type PermissionLevel = "deny" | "allow" | "require_approval";
|
|
5
|
+
export type ConversationScope = "global" | "relationship" | "conversation" | "work_session" | "message";
|
|
6
6
|
export interface Participant {
|
|
7
7
|
id: string;
|
|
8
|
-
type:
|
|
8
|
+
type: "human" | "ai_agent";
|
|
9
9
|
ownerId?: string | null;
|
|
10
10
|
relationshipIds?: string[];
|
|
11
11
|
}
|
|
@@ -21,6 +21,16 @@ export interface ContextOverlay {
|
|
|
21
21
|
instructions?: string | null;
|
|
22
22
|
notes?: string | null;
|
|
23
23
|
}
|
|
24
|
+
export interface AgentBehaviorSettings {
|
|
25
|
+
participationStyle?: ParticipationStyle | null;
|
|
26
|
+
allowAgentToAgent?: boolean | null;
|
|
27
|
+
allowLongRunningCollaboration?: boolean | null;
|
|
28
|
+
requireMentionForGroupReplies?: boolean | null;
|
|
29
|
+
/** @deprecated Use requireMentionForGroupReplies instead. */
|
|
30
|
+
requireMentionForGroupAgentReplies?: boolean | null;
|
|
31
|
+
maxConsecutiveAgentTurns?: number | null;
|
|
32
|
+
instructions?: string | null;
|
|
33
|
+
}
|
|
24
34
|
export interface BehaviorProfile {
|
|
25
35
|
id: string;
|
|
26
36
|
label: string;
|
|
@@ -28,7 +38,7 @@ export interface BehaviorProfile {
|
|
|
28
38
|
defaultRepresentation: RepresentationMode;
|
|
29
39
|
allowAgentToAgent: boolean;
|
|
30
40
|
allowLongRunningCollaboration: boolean;
|
|
31
|
-
|
|
41
|
+
requireMentionForGroupReplies: boolean;
|
|
32
42
|
maxConsecutiveAgentTurns?: number | null;
|
|
33
43
|
}
|
|
34
44
|
export interface AdmissionPolicy {
|
|
@@ -54,7 +64,7 @@ export interface ParticipationPolicy {
|
|
|
54
64
|
allowAgentToAgent: boolean;
|
|
55
65
|
allowHumanToAgent: boolean;
|
|
56
66
|
allowLongRunningCollaboration: boolean;
|
|
57
|
-
|
|
67
|
+
requireMentionForGroupReplies: boolean;
|
|
58
68
|
maxConsecutiveAgentTurns?: number | null;
|
|
59
69
|
}
|
|
60
70
|
export interface WorkSession {
|
|
@@ -65,7 +75,7 @@ export interface WorkSession {
|
|
|
65
75
|
participationStyle?: ParticipationStyle;
|
|
66
76
|
activeParticipantIds: string[];
|
|
67
77
|
overlayIds?: string[];
|
|
68
|
-
status:
|
|
78
|
+
status: "active" | "paused" | "completed";
|
|
69
79
|
}
|
|
70
80
|
export interface ResolvedPolicy {
|
|
71
81
|
admission: AdmissionPolicy;
|
|
@@ -97,3 +107,50 @@ export interface ResolvedTurnEligibility {
|
|
|
97
107
|
mayInitiateExternalFirstContact: boolean;
|
|
98
108
|
reasonCodes: string[];
|
|
99
109
|
}
|
|
110
|
+
export interface ResolvedAgentBehaviorPolicy {
|
|
111
|
+
participation: ParticipationPolicy;
|
|
112
|
+
instructions: string[];
|
|
113
|
+
source: {
|
|
114
|
+
hasAgentDefault: boolean;
|
|
115
|
+
hasConversationOverride: boolean;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export interface ParticipationDecisionInput {
|
|
119
|
+
conversationType: "direct" | "group" | "unknown";
|
|
120
|
+
senderType: "human" | "ai_agent";
|
|
121
|
+
isOwner: boolean;
|
|
122
|
+
mentionedAgent: boolean;
|
|
123
|
+
recentHumanCount?: number;
|
|
124
|
+
consecutiveAgentTurns?: number;
|
|
125
|
+
currentAgentStreakStartedByHuman?: boolean;
|
|
126
|
+
}
|
|
127
|
+
export interface ParticipationDecision {
|
|
128
|
+
allow: boolean;
|
|
129
|
+
reasonCode: string;
|
|
130
|
+
reason: string;
|
|
131
|
+
}
|
|
132
|
+
export interface ParticipationHistoryMessage {
|
|
133
|
+
id?: string;
|
|
134
|
+
senderId: string;
|
|
135
|
+
senderType: "human" | "ai_agent";
|
|
136
|
+
metadata?: unknown;
|
|
137
|
+
}
|
|
138
|
+
export interface ParticipationHistorySnapshot {
|
|
139
|
+
recentSenderTypes: Array<"human" | "ai_agent">;
|
|
140
|
+
recentHumanCount: number;
|
|
141
|
+
recentAgentCount: number;
|
|
142
|
+
consecutiveAgentTurns: number;
|
|
143
|
+
currentAgentStreakStartedByHuman: boolean;
|
|
144
|
+
}
|
|
145
|
+
export declare const DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT = 50;
|
|
146
|
+
export declare function getDefaultParticipationPolicy(): ParticipationPolicy;
|
|
147
|
+
export declare function resolveAgentBehaviorPolicy(input?: {
|
|
148
|
+
agentDefault?: AgentBehaviorSettings | null;
|
|
149
|
+
conversationOverride?: AgentBehaviorSettings | null;
|
|
150
|
+
}): ResolvedAgentBehaviorPolicy;
|
|
151
|
+
export declare function evaluateParticipationPolicy(policy: ResolvedAgentBehaviorPolicy | null | undefined, input: ParticipationDecisionInput): ParticipationDecision;
|
|
152
|
+
export declare function buildBehaviorPolicyLines(policy: ResolvedAgentBehaviorPolicy | null | undefined): string[];
|
|
153
|
+
export declare function buildParticipationHistorySnapshot(messages: ParticipationHistoryMessage[], agentId: string): ParticipationHistorySnapshot;
|
|
154
|
+
export declare function buildParticipationHistorySnapshots<TMessage extends ParticipationHistoryMessage & {
|
|
155
|
+
id: string;
|
|
156
|
+
}>(messages: TMessage[], agentId: string): Map<string, ParticipationHistorySnapshot>;
|
package/dist/policy.js
CHANGED
|
@@ -1 +1,189 @@
|
|
|
1
|
-
|
|
1
|
+
import { resolveTurnMessageSemantics } from "./turn-protocol.js";
|
|
2
|
+
export const DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT = 50;
|
|
3
|
+
const DEFAULT_PARTICIPATION_POLICY = {
|
|
4
|
+
style: "natural",
|
|
5
|
+
allowAgentToAgent: true,
|
|
6
|
+
allowHumanToAgent: true,
|
|
7
|
+
allowLongRunningCollaboration: true,
|
|
8
|
+
requireMentionForGroupReplies: false,
|
|
9
|
+
maxConsecutiveAgentTurns: null,
|
|
10
|
+
};
|
|
11
|
+
function coalesceBoolean(overrideValue, fallbackValue, defaultValue = false) {
|
|
12
|
+
if (typeof overrideValue === "boolean")
|
|
13
|
+
return overrideValue;
|
|
14
|
+
if (typeof fallbackValue === "boolean")
|
|
15
|
+
return fallbackValue;
|
|
16
|
+
return defaultValue;
|
|
17
|
+
}
|
|
18
|
+
function coalesceStyle(overrideValue, fallbackValue) {
|
|
19
|
+
return overrideValue ?? fallbackValue ?? DEFAULT_PARTICIPATION_POLICY.style;
|
|
20
|
+
}
|
|
21
|
+
function resolveGroupMentionRequirement(value) {
|
|
22
|
+
if (typeof value?.requireMentionForGroupReplies === "boolean") {
|
|
23
|
+
return value.requireMentionForGroupReplies;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value?.requireMentionForGroupAgentReplies === "boolean") {
|
|
26
|
+
return value.requireMentionForGroupAgentReplies;
|
|
27
|
+
}
|
|
28
|
+
if (value?.requireMentionForGroupReplies === null ||
|
|
29
|
+
value?.requireMentionForGroupAgentReplies === null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function coalesceMaxConsecutiveAgentTurns(overrideValue, fallbackValue) {
|
|
35
|
+
if (overrideValue === null)
|
|
36
|
+
return null;
|
|
37
|
+
if (typeof overrideValue === "number")
|
|
38
|
+
return overrideValue;
|
|
39
|
+
if (fallbackValue === null)
|
|
40
|
+
return null;
|
|
41
|
+
if (typeof fallbackValue === "number")
|
|
42
|
+
return fallbackValue;
|
|
43
|
+
return DEFAULT_PARTICIPATION_POLICY.maxConsecutiveAgentTurns ?? null;
|
|
44
|
+
}
|
|
45
|
+
function normalizedInstructions(value) {
|
|
46
|
+
if (typeof value !== "string")
|
|
47
|
+
return null;
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
50
|
+
}
|
|
51
|
+
export function getDefaultParticipationPolicy() {
|
|
52
|
+
return { ...DEFAULT_PARTICIPATION_POLICY };
|
|
53
|
+
}
|
|
54
|
+
export function resolveAgentBehaviorPolicy(input) {
|
|
55
|
+
const agentDefault = input?.agentDefault ?? null;
|
|
56
|
+
const conversationOverride = input?.conversationOverride ?? null;
|
|
57
|
+
const defaultInstructions = normalizedInstructions(agentDefault?.instructions);
|
|
58
|
+
const overrideInstructions = normalizedInstructions(conversationOverride?.instructions);
|
|
59
|
+
return {
|
|
60
|
+
participation: {
|
|
61
|
+
style: coalesceStyle(conversationOverride?.participationStyle, agentDefault?.participationStyle),
|
|
62
|
+
allowAgentToAgent: coalesceBoolean(conversationOverride?.allowAgentToAgent, agentDefault?.allowAgentToAgent, DEFAULT_PARTICIPATION_POLICY.allowAgentToAgent),
|
|
63
|
+
allowHumanToAgent: DEFAULT_PARTICIPATION_POLICY.allowHumanToAgent,
|
|
64
|
+
allowLongRunningCollaboration: coalesceBoolean(conversationOverride?.allowLongRunningCollaboration, agentDefault?.allowLongRunningCollaboration, DEFAULT_PARTICIPATION_POLICY.allowLongRunningCollaboration),
|
|
65
|
+
requireMentionForGroupReplies: coalesceBoolean(resolveGroupMentionRequirement(conversationOverride), resolveGroupMentionRequirement(agentDefault), DEFAULT_PARTICIPATION_POLICY.requireMentionForGroupReplies),
|
|
66
|
+
maxConsecutiveAgentTurns: coalesceMaxConsecutiveAgentTurns(conversationOverride?.maxConsecutiveAgentTurns, agentDefault?.maxConsecutiveAgentTurns),
|
|
67
|
+
},
|
|
68
|
+
instructions: [
|
|
69
|
+
...(defaultInstructions ? [defaultInstructions] : []),
|
|
70
|
+
...(overrideInstructions ? [overrideInstructions] : []),
|
|
71
|
+
],
|
|
72
|
+
source: {
|
|
73
|
+
hasAgentDefault: agentDefault != null,
|
|
74
|
+
hasConversationOverride: conversationOverride != null,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function evaluateParticipationPolicy(policy, input) {
|
|
79
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy();
|
|
80
|
+
const participation = resolved.participation;
|
|
81
|
+
const consecutiveAgentTurns = Math.max(input.consecutiveAgentTurns ?? (input.senderType === "ai_agent" ? 1 : 0), input.senderType === "ai_agent" ? 1 : 0);
|
|
82
|
+
const currentAgentStreakStartedByHuman = input.currentAgentStreakStartedByHuman === true;
|
|
83
|
+
if (input.isOwner) {
|
|
84
|
+
return {
|
|
85
|
+
allow: true,
|
|
86
|
+
reasonCode: "owner_sender",
|
|
87
|
+
reason: "owner messages always pass through",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (input.conversationType === "group" &&
|
|
91
|
+
participation.requireMentionForGroupReplies &&
|
|
92
|
+
!input.mentionedAgent) {
|
|
93
|
+
return {
|
|
94
|
+
allow: false,
|
|
95
|
+
reasonCode: "group_mention_required",
|
|
96
|
+
reason: "group replies require a direct mention",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (input.senderType !== "ai_agent") {
|
|
100
|
+
return {
|
|
101
|
+
allow: true,
|
|
102
|
+
reasonCode: "human_sender",
|
|
103
|
+
reason: "latest sender is human",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (!participation.allowAgentToAgent) {
|
|
107
|
+
return {
|
|
108
|
+
allow: false,
|
|
109
|
+
reasonCode: "agent_to_agent_disabled",
|
|
110
|
+
reason: "agent-to-agent participation is disabled by policy",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!participation.allowLongRunningCollaboration &&
|
|
114
|
+
(consecutiveAgentTurns > 1 || !currentAgentStreakStartedByHuman)) {
|
|
115
|
+
return {
|
|
116
|
+
allow: false,
|
|
117
|
+
reasonCode: "human_reset_required",
|
|
118
|
+
reason: "a fresh human steer is required before continuing agent collaboration",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (typeof participation.maxConsecutiveAgentTurns === "number" &&
|
|
122
|
+
participation.maxConsecutiveAgentTurns >= 0 &&
|
|
123
|
+
consecutiveAgentTurns > participation.maxConsecutiveAgentTurns) {
|
|
124
|
+
return {
|
|
125
|
+
allow: false,
|
|
126
|
+
reasonCode: "agent_turn_limit_reached",
|
|
127
|
+
reason: "maximum consecutive agent turns reached",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
allow: true,
|
|
132
|
+
reasonCode: input.conversationType === "group"
|
|
133
|
+
? "group_agent_allowed"
|
|
134
|
+
: "direct_agent_allowed",
|
|
135
|
+
reason: participation.requireMentionForGroupReplies
|
|
136
|
+
? "policy allows this directly mentioned group reply"
|
|
137
|
+
: "agent-to-agent participation allowed by policy",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function buildBehaviorPolicyLines(policy) {
|
|
141
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy();
|
|
142
|
+
const maxTurns = resolved.participation.maxConsecutiveAgentTurns;
|
|
143
|
+
return [
|
|
144
|
+
`Canon guidance: participation posture is ${resolved.participation.style}.`,
|
|
145
|
+
`Canon rule: group replies require direct mention: ${resolved.participation.requireMentionForGroupReplies ? "yes" : "no"}`,
|
|
146
|
+
`Canon rule: agent-to-agent collaboration allowed: ${resolved.participation.allowAgentToAgent ? "yes" : "no"}`,
|
|
147
|
+
`Canon rule: long-running collaboration allowed: ${resolved.participation.allowLongRunningCollaboration ? "yes" : "no"}`,
|
|
148
|
+
`Canon rule: max consecutive agent turns without a human reset: ${typeof maxTurns === "number" ? String(maxTurns) : "unlimited"}`,
|
|
149
|
+
...resolved.instructions.map((instruction, index) => `Canon guidance ${index + 1}: ${instruction}`),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
function toHistorySenderType(message) {
|
|
153
|
+
if (message.senderType !== "ai_agent") {
|
|
154
|
+
return "human";
|
|
155
|
+
}
|
|
156
|
+
return resolveTurnMessageSemantics({
|
|
157
|
+
senderType: message.senderType,
|
|
158
|
+
metadata: message.metadata,
|
|
159
|
+
}) === "turn_complete"
|
|
160
|
+
? "ai_agent"
|
|
161
|
+
: null;
|
|
162
|
+
}
|
|
163
|
+
export function buildParticipationHistorySnapshot(messages, agentId) {
|
|
164
|
+
const recentSenderTypes = messages.flatMap((message) => {
|
|
165
|
+
const senderType = toHistorySenderType(message);
|
|
166
|
+
return senderType ? [senderType] : [];
|
|
167
|
+
});
|
|
168
|
+
let consecutiveAgentTurns = 0;
|
|
169
|
+
for (const senderType of recentSenderTypes) {
|
|
170
|
+
if (senderType !== "ai_agent")
|
|
171
|
+
break;
|
|
172
|
+
consecutiveAgentTurns += 1;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
recentSenderTypes,
|
|
176
|
+
recentHumanCount: recentSenderTypes.filter((senderType) => senderType === "human").length,
|
|
177
|
+
recentAgentCount: recentSenderTypes.filter((senderType) => senderType === "ai_agent").length,
|
|
178
|
+
consecutiveAgentTurns,
|
|
179
|
+
currentAgentStreakStartedByHuman: consecutiveAgentTurns > 0 &&
|
|
180
|
+
recentSenderTypes[consecutiveAgentTurns] === "human",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export function buildParticipationHistorySnapshots(messages, agentId) {
|
|
184
|
+
const snapshots = new Map();
|
|
185
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
186
|
+
snapshots.set(messages[index].id, buildParticipationHistorySnapshot(messages.slice(index + 1), agentId));
|
|
187
|
+
}
|
|
188
|
+
return snapshots;
|
|
189
|
+
}
|
package/dist/registration.js
CHANGED
|
@@ -17,6 +17,7 @@ export async function registerAndWaitForApproval(input, callbacks) {
|
|
|
17
17
|
developerInfo: input.developerInfo || 'Canon agent',
|
|
18
18
|
avatarUrl: input.avatarUrl,
|
|
19
19
|
clientType: input.clientType,
|
|
20
|
+
requestedAgentId: input.requestedAgentId,
|
|
20
21
|
});
|
|
21
22
|
callbacks?.onSubmitted?.(requestId);
|
|
22
23
|
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
package/dist/rtdb-rest.d.ts
CHANGED
package/dist/turn-protocol.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface RuntimeCapabilities {
|
|
|
9
9
|
supportsRequiresAction: boolean;
|
|
10
10
|
supportsNonFinalPermanentMessages: boolean;
|
|
11
11
|
}
|
|
12
|
+
export declare const FINAL_MESSAGE_HANDOFF_MS = 750;
|
|
12
13
|
export interface TurnState {
|
|
13
14
|
turnId?: string | null;
|
|
14
15
|
state: TurnLifecycleState;
|
package/dist/turn-protocol.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResolvedAgentBehaviorPolicy } from './policy.js';
|
|
1
2
|
export type MediaAttachmentKind = 'image' | 'audio' | 'file';
|
|
2
3
|
export interface MediaAttachment {
|
|
3
4
|
kind: MediaAttachmentKind;
|
|
@@ -36,6 +37,7 @@ export interface CanonConversation {
|
|
|
36
37
|
topic: string | null;
|
|
37
38
|
memberIds: string[];
|
|
38
39
|
isAgentChat: boolean;
|
|
40
|
+
behavior?: ResolvedAgentBehaviorPolicy;
|
|
39
41
|
hasUnread?: boolean;
|
|
40
42
|
lastMessage: {
|
|
41
43
|
text: string;
|
|
@@ -45,6 +47,10 @@ export interface CanonConversation {
|
|
|
45
47
|
} | null;
|
|
46
48
|
createdAt: string;
|
|
47
49
|
}
|
|
50
|
+
export interface CanonMessagesPage {
|
|
51
|
+
messages: CanonMessage[];
|
|
52
|
+
behavior?: ResolvedAgentBehaviorPolicy;
|
|
53
|
+
}
|
|
48
54
|
export type AgentClientType = 'claude-code' | 'openclaw' | 'codex' | 'generic';
|
|
49
55
|
/** Declares what session controls an agent type supports. */
|
|
50
56
|
export interface AgentCapabilities {
|
|
@@ -77,9 +83,11 @@ export interface AgentContext {
|
|
|
77
83
|
accessLevel: 'open' | 'owner-only';
|
|
78
84
|
/** Identifies the agent's client platform for UI feature detection */
|
|
79
85
|
clientType?: AgentClientType;
|
|
86
|
+
defaultBehavior?: ResolvedAgentBehaviorPolicy;
|
|
80
87
|
}
|
|
81
88
|
export interface MessageCreatedPayload {
|
|
82
89
|
conversationId: string;
|
|
90
|
+
behavior?: ResolvedAgentBehaviorPolicy;
|
|
83
91
|
message: {
|
|
84
92
|
id: string;
|
|
85
93
|
senderId: string;
|
|
@@ -152,6 +160,8 @@ export interface SessionState {
|
|
|
152
160
|
permissionMode?: string;
|
|
153
161
|
effort?: string;
|
|
154
162
|
cwd?: string;
|
|
163
|
+
executionMode?: 'worktree' | 'locked';
|
|
164
|
+
executionBranch?: string;
|
|
155
165
|
/** True when the agent is running under the host wrapper (host.ts) which can apply control signals */
|
|
156
166
|
hostMode?: boolean;
|
|
157
167
|
isActive: boolean;
|
|
@@ -192,6 +202,7 @@ export interface RegistrationInput {
|
|
|
192
202
|
avatarUrl?: string;
|
|
193
203
|
baseUrl?: string;
|
|
194
204
|
clientType?: AgentClientType;
|
|
205
|
+
requestedAgentId?: string;
|
|
195
206
|
}
|
|
196
207
|
export interface RegistrationResult {
|
|
197
208
|
status: 'approved' | 'rejected' | 'timeout';
|
package/dist/types.js
CHANGED