@canonmsg/core 0.19.3 → 0.20.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/agent-profiles.js +85 -3
- package/dist/agent-resolver.js +2 -1
- package/dist/agent-session.js +6 -0
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/rtdb-rest.d.ts +10 -0
- package/dist/rtdb-rest.js +16 -0
- package/dist/stream.d.ts +16 -0
- package/dist/stream.js +47 -4
- package/dist/turn-protocol.d.ts +9 -4
- package/dist/turn-protocol.js +28 -1
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/dist/agent-profiles.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared agent profile management — loading, locking, and resolution.
|
|
3
3
|
* Used by both host.ts and server.ts.
|
|
4
4
|
*/
|
|
5
|
-
import { readFileSync, renameSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
6
6
|
import { closeSync, openSync } from 'node:fs';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { join, resolve } from 'node:path';
|
|
@@ -14,14 +14,96 @@ export const CANON_DIR = process.env.CANON_HOME
|
|
|
14
14
|
export const AGENTS_PATH = join(CANON_DIR, 'agents.json');
|
|
15
15
|
export const LOCKS_DIR = join(CANON_DIR, 'locks');
|
|
16
16
|
export const PENDING_REGISTRATIONS_PATH = join(CANON_DIR, 'pending-registrations.json');
|
|
17
|
+
const AGENTS_JSON_BOOTSTRAP_ENV = 'CANON_AGENTS_JSON_BOOTSTRAP';
|
|
17
18
|
// ── Profile loading ──────────────────────────────────────────────────
|
|
19
|
+
function normalizeBootstrapProfile(name, value) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
21
|
+
throw new Error(`${AGENTS_JSON_BOOTSTRAP_ENV} profile "${name}" must be an object`);
|
|
22
|
+
}
|
|
23
|
+
const profile = value;
|
|
24
|
+
const apiKey = profile.apiKey;
|
|
25
|
+
const agentId = profile.agentId;
|
|
26
|
+
const agentName = profile.agentName;
|
|
27
|
+
const registeredAt = profile.registeredAt;
|
|
28
|
+
if (typeof apiKey !== 'string'
|
|
29
|
+
|| typeof agentId !== 'string'
|
|
30
|
+
|| typeof agentName !== 'string'
|
|
31
|
+
|| typeof registeredAt !== 'string'
|
|
32
|
+
|| !apiKey
|
|
33
|
+
|| !agentId
|
|
34
|
+
|| !agentName
|
|
35
|
+
|| !registeredAt) {
|
|
36
|
+
throw new Error(`${AGENTS_JSON_BOOTSTRAP_ENV} profile "${name}" must include apiKey, agentId, agentName, and registeredAt`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
apiKey,
|
|
40
|
+
agentId,
|
|
41
|
+
agentName,
|
|
42
|
+
registeredAt,
|
|
43
|
+
...(typeof profile.clientType === 'string' ? { clientType: profile.clientType } : {}),
|
|
44
|
+
...(typeof profile.baseUrl === 'string' ? { baseUrl: profile.baseUrl } : {}),
|
|
45
|
+
...(typeof profile.streamUrl === 'string' ? { streamUrl: profile.streamUrl } : {}),
|
|
46
|
+
...(typeof profile.rtdbUrl === 'string' ? { rtdbUrl: profile.rtdbUrl } : {}),
|
|
47
|
+
...(typeof profile.runtimeId === 'string' ? { runtimeId: profile.runtimeId } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function parseBootstrapProfiles(raw) {
|
|
51
|
+
const trimmed = raw.trim();
|
|
52
|
+
if (!trimmed)
|
|
53
|
+
return {};
|
|
54
|
+
const parse = (text) => JSON.parse(text);
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = parse(trimmed);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
try {
|
|
61
|
+
parsed = parse(Buffer.from(trimmed, 'base64').toString('utf-8'));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error(`${AGENTS_JSON_BOOTSTRAP_ENV} must be valid agents.json JSON or base64-encoded JSON`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
68
|
+
throw new Error(`${AGENTS_JSON_BOOTSTRAP_ENV} must be a JSON object keyed by profile name`);
|
|
69
|
+
}
|
|
70
|
+
const profiles = {};
|
|
71
|
+
for (const [rawName, value] of Object.entries(parsed)) {
|
|
72
|
+
const name = rawName.trim();
|
|
73
|
+
if (!name) {
|
|
74
|
+
throw new Error(`${AGENTS_JSON_BOOTSTRAP_ENV} contains an empty profile name`);
|
|
75
|
+
}
|
|
76
|
+
profiles[name] = normalizeBootstrapProfile(name, value);
|
|
77
|
+
}
|
|
78
|
+
return profiles;
|
|
79
|
+
}
|
|
80
|
+
function applyBootstrapProfiles(existing) {
|
|
81
|
+
const raw = process.env[AGENTS_JSON_BOOTSTRAP_ENV];
|
|
82
|
+
if (!raw)
|
|
83
|
+
return existing;
|
|
84
|
+
const bootstrapProfiles = parseBootstrapProfiles(raw);
|
|
85
|
+
let changed = false;
|
|
86
|
+
const merged = { ...existing };
|
|
87
|
+
for (const [name, profile] of Object.entries(bootstrapProfiles)) {
|
|
88
|
+
if (merged[name])
|
|
89
|
+
continue;
|
|
90
|
+
merged[name] = profile;
|
|
91
|
+
changed = true;
|
|
92
|
+
}
|
|
93
|
+
if (changed || !existsSync(AGENTS_PATH)) {
|
|
94
|
+
saveProfiles(merged);
|
|
95
|
+
}
|
|
96
|
+
return merged;
|
|
97
|
+
}
|
|
18
98
|
export function loadProfiles() {
|
|
99
|
+
let profiles;
|
|
19
100
|
try {
|
|
20
|
-
|
|
101
|
+
profiles = JSON.parse(readFileSync(AGENTS_PATH, 'utf-8'));
|
|
21
102
|
}
|
|
22
103
|
catch {
|
|
23
|
-
|
|
104
|
+
profiles = {};
|
|
24
105
|
}
|
|
106
|
+
return applyBootstrapProfiles(profiles);
|
|
25
107
|
}
|
|
26
108
|
function writeJsonFile(path, value, mode = 0o600) {
|
|
27
109
|
mkdirSync(CANON_DIR, { recursive: true });
|
package/dist/agent-resolver.js
CHANGED
|
@@ -32,7 +32,8 @@ export function resolveCanonProfile(name, opts) {
|
|
|
32
32
|
const profiles = loadProfiles();
|
|
33
33
|
const profile = profiles[profileName];
|
|
34
34
|
if (!profile) {
|
|
35
|
-
throw new Error(`${prefix} Profile "${profileName}" not found in ~/.canon/agents.json`
|
|
35
|
+
throw new Error(`${prefix} Profile "${profileName}" not found in ~/.canon/agents.json. `
|
|
36
|
+
+ 'Set CANON_AGENTS_JSON_BOOTSTRAP, set CANON_API_KEY, or re-run registration against the mounted Canon profile volume.');
|
|
36
37
|
}
|
|
37
38
|
if (!profileMatches(profile, opts?.expectedClientType)) {
|
|
38
39
|
throw new Error(`${prefix} Profile "${profileName}" is registered for ${profile.clientType}, not ${opts?.expectedClientType}`);
|
package/dist/agent-session.js
CHANGED
|
@@ -132,6 +132,12 @@ export function buildAgentSessionSnapshot(input) {
|
|
|
132
132
|
worktreePath: input.sessionState?.worktreePath,
|
|
133
133
|
executionFallbackReason: input.sessionState?.executionFallbackReason,
|
|
134
134
|
turnState: input.turnState?.state,
|
|
135
|
+
turnId: input.turnState?.turnId ?? null,
|
|
136
|
+
turnOpenedAt: input.turnState?.openedAt ?? null,
|
|
137
|
+
turnUpdatedAt: input.turnState?.turnUpdatedAt
|
|
138
|
+
?? input.turnState?.updatedAt
|
|
139
|
+
?? input.turnState?.openedAt
|
|
140
|
+
?? null,
|
|
135
141
|
supportsQueue: input.turnState?.capabilities?.supportsQueue,
|
|
136
142
|
supportsInputInterrupt: input.turnState?.capabilities?.supportsInputInterrupt,
|
|
137
143
|
queueDepth: input.turnState?.queueDepth ?? 0,
|
package/dist/browser.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { buildAgentSessionSnapshot } from './agent-session.js';
|
|
|
12
12
|
export { CLAUDE_EFFORT_OPTIONS, EXECUTION_MODE_CONTROL_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildFirstPartyCodingRuntimeDescriptor, buildRuntimeEffortControl, buildRuntimeExecutionModeControl, buildRuntimeExecutionModeOptions, buildRuntimeModelControl, buildRuntimePermissionModeControl, buildRuntimeWorkspaceControl, buildRuntimeWorkspaceControlOptions, } from './runtime-descriptor.js';
|
|
13
13
|
export type { AgentBehaviorSettings, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationStyle, ResolvedAgentBehaviorPolicy, } from './policy.js';
|
|
14
14
|
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
15
|
-
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
15
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, ACTIVE_TURN_STALE_THRESHOLD_MS, FINAL_MESSAGE_HANDOFF_MS, WAITING_INPUT_STALE_THRESHOLD_MS, getTurnStateStaleThresholdMs, isTurnOpen, isTurnStateStale, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
16
16
|
export type { DeliveryIntent, InboundDisposition, RuntimeCapabilities, TriggerDecision, TurnLifecycleState, TurnMessageSemantics, TurnMetadata, TurnState, } from './turn-protocol.js';
|
|
17
17
|
export { buildApprovalReply, buildApprovalRequest, buildApprovalOutcome, generateApprovalId, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
|
|
18
18
|
export type { ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalNativeRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
|
package/dist/browser.js
CHANGED
|
@@ -8,7 +8,7 @@ export { buildSelfContextPromptLines, normalizeSelfContexts, } from './self-cont
|
|
|
8
8
|
export { buildAgentSessionSnapshot } from './agent-session.js';
|
|
9
9
|
export { CLAUDE_EFFORT_OPTIONS, EXECUTION_MODE_CONTROL_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildFirstPartyCodingRuntimeDescriptor, buildRuntimeEffortControl, buildRuntimeExecutionModeControl, buildRuntimeExecutionModeOptions, buildRuntimeModelControl, buildRuntimePermissionModeControl, buildRuntimeWorkspaceControl, buildRuntimeWorkspaceControlOptions, } from './runtime-descriptor.js';
|
|
10
10
|
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
11
|
-
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
11
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, ACTIVE_TURN_STALE_THRESHOLD_MS, FINAL_MESSAGE_HANDOFF_MS, WAITING_INPUT_STALE_THRESHOLD_MS, getTurnStateStaleThresholdMs, isTurnOpen, isTurnStateStale, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
12
12
|
export { buildApprovalReply, buildApprovalRequest, buildApprovalOutcome, generateApprovalId, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
|
|
13
13
|
export { DEFAULT_APPROVAL_CONFIG, parseApprovalRequestMetadata, parseApprovalReplyMetadata, parseSessionRule, } from './approval-types.js';
|
|
14
14
|
export { buildRuntimeInputOutcome, buildRuntimeInputReply, buildRuntimeInputRequest, buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, buildQuestionRequest, parseRuntimeInputOutcomeMetadata, parseRuntimeInputReplyMetadata, parseRuntimeInputRequestMetadata, } from './runtime-cards.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -9,11 +9,11 @@ export type { ConfiguredWorkspaceRoot, WorkspaceDiscoveryResult, } from './works
|
|
|
9
9
|
export { CanonClient, CanonApiError } from './client.js';
|
|
10
10
|
export { buildAgentSessionSnapshot } from './agent-session.js';
|
|
11
11
|
export { CLAUDE_EFFORT_OPTIONS, EXECUTION_MODE_CONTROL_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildFirstPartyCodingRuntimeDescriptor, buildRuntimeEffortControl, buildRuntimeExecutionModeControl, buildRuntimeExecutionModeOptions, buildRuntimeModelControl, buildRuntimePermissionModeControl, buildRuntimeWorkspaceControl, buildRuntimeWorkspaceControlOptions, } from './runtime-descriptor.js';
|
|
12
|
-
export { CanonStream } from './stream.js';
|
|
13
|
-
export type { StreamHandler } from './stream.js';
|
|
12
|
+
export { CanonStream, CanonStreamError } from './stream.js';
|
|
13
|
+
export type { CanonStreamErrorPayload, StreamHandler } from './stream.js';
|
|
14
14
|
export type { PolicyRole, ParticipationStyle, RepresentationMode, PermissionLevel, ConversationScope, AgentBehaviorSettings, Participant, Relationship, ContextOverlay, BehaviorProfile, AdmissionPolicy, RuntimeControlPolicy, ActionApprovalPolicy, ParticipationPolicy, ResolvedPolicy, ResolvedTurnEligibility, ResolvedAgentBehaviorPolicy, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationDecisionInput, ParticipationDecision, } from './policy.js';
|
|
15
15
|
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
16
|
-
export { DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
16
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, ACTIVE_TURN_STALE_THRESHOLD_MS, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, FINAL_MESSAGE_HANDOFF_MS, WAITING_INPUT_STALE_THRESHOLD_MS, getTurnStateStaleThresholdMs, isTurnOpen, isTurnStateStale, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
17
17
|
export type { DeliveryIntent, TurnMessageSemantics, InboundDisposition, TurnLifecycleState, RuntimeCapabilities, HostAdmissionActionCapabilities, TurnState, TurnMetadata, TriggerDecision, } from './turn-protocol.js';
|
|
18
18
|
export { ackRegistrationApproval, registerAndWaitForApproval, submitRegistrationRequest, waitForRegistrationApproval, } from './registration.js';
|
|
19
19
|
export { ApprovalManager } from './approval-manager.js';
|
package/dist/index.js
CHANGED
|
@@ -9,10 +9,10 @@ export { CanonClient, CanonApiError } from './client.js';
|
|
|
9
9
|
export { buildAgentSessionSnapshot } from './agent-session.js';
|
|
10
10
|
export { CLAUDE_EFFORT_OPTIONS, EXECUTION_MODE_CONTROL_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildFirstPartyCodingRuntimeDescriptor, buildRuntimeEffortControl, buildRuntimeExecutionModeControl, buildRuntimeExecutionModeOptions, buildRuntimeModelControl, buildRuntimePermissionModeControl, buildRuntimeWorkspaceControl, buildRuntimeWorkspaceControlOptions, } from './runtime-descriptor.js';
|
|
11
11
|
// Stream
|
|
12
|
-
export { CanonStream } from './stream.js';
|
|
12
|
+
export { CanonStream, CanonStreamError } from './stream.js';
|
|
13
13
|
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
14
14
|
// Turn protocol
|
|
15
|
-
export { DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
15
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, ACTIVE_TURN_STALE_THRESHOLD_MS, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, FINAL_MESSAGE_HANDOFF_MS, WAITING_INPUT_STALE_THRESHOLD_MS, getTurnStateStaleThresholdMs, isTurnOpen, isTurnStateStale, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
16
16
|
// Registration
|
|
17
17
|
export { ackRegistrationApproval, registerAndWaitForApproval, submitRegistrationRequest, waitForRegistrationApproval, } from './registration.js';
|
|
18
18
|
// Approval
|
package/dist/rtdb-rest.d.ts
CHANGED
|
@@ -45,6 +45,9 @@ export interface TurnStatePayload {
|
|
|
45
45
|
openedAt?: number | {
|
|
46
46
|
'.sv': 'timestamp';
|
|
47
47
|
};
|
|
48
|
+
turnUpdatedAt?: number | {
|
|
49
|
+
'.sv': 'timestamp';
|
|
50
|
+
} | null;
|
|
48
51
|
completedAt?: number | {
|
|
49
52
|
'.sv': 'timestamp';
|
|
50
53
|
} | null;
|
|
@@ -79,6 +82,13 @@ export interface AgentSessionSnapshotPatch {
|
|
|
79
82
|
executionFallbackReason?: string | null;
|
|
80
83
|
state?: null;
|
|
81
84
|
turnState?: TurnLifecycleState | null;
|
|
85
|
+
turnId?: string | null;
|
|
86
|
+
turnOpenedAt?: number | {
|
|
87
|
+
'.sv': 'timestamp';
|
|
88
|
+
} | null;
|
|
89
|
+
turnUpdatedAt?: number | {
|
|
90
|
+
'.sv': 'timestamp';
|
|
91
|
+
} | null;
|
|
82
92
|
supportsQueue?: boolean | null;
|
|
83
93
|
supportsInputInterrupt?: boolean | null;
|
|
84
94
|
queueDepth?: number;
|
package/dist/rtdb-rest.js
CHANGED
|
@@ -199,12 +199,25 @@ function createRTDBClientHandle(client, options) {
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
async function writeTurnStateImpl(conversationId, agentId, state) {
|
|
202
|
+
const isOpenTurn = state.state === 'thinking'
|
|
203
|
+
|| state.state === 'streaming'
|
|
204
|
+
|| state.state === 'tool'
|
|
205
|
+
|| state.state === 'waiting_input';
|
|
206
|
+
const turnOpenedAt = state.openedAt ?? null;
|
|
207
|
+
const turnUpdatedAt = state.turnUpdatedAt !== undefined
|
|
208
|
+
? state.turnUpdatedAt
|
|
209
|
+
: isOpenTurn
|
|
210
|
+
? turnOpenedAt ?? { '.sv': 'timestamp' }
|
|
211
|
+
: null;
|
|
202
212
|
await write(`/turn-state/${conversationId}/${agentId}`, {
|
|
203
213
|
...state,
|
|
204
214
|
updatedAt: { '.sv': 'timestamp' },
|
|
205
215
|
});
|
|
206
216
|
await patch(buildAgentSessionPath(conversationId, agentId), {
|
|
207
217
|
turnState: state.state,
|
|
218
|
+
turnId: state.turnId ?? null,
|
|
219
|
+
turnOpenedAt,
|
|
220
|
+
turnUpdatedAt,
|
|
208
221
|
...(state.capabilities?.supportsQueue !== undefined
|
|
209
222
|
? { supportsQueue: state.capabilities.supportsQueue }
|
|
210
223
|
: {}),
|
|
@@ -227,6 +240,9 @@ function createRTDBClientHandle(client, options) {
|
|
|
227
240
|
});
|
|
228
241
|
await patch(buildAgentSessionPath(conversationId, agentId), {
|
|
229
242
|
turnState: 'idle',
|
|
243
|
+
turnId: null,
|
|
244
|
+
turnOpenedAt: null,
|
|
245
|
+
turnUpdatedAt: null,
|
|
230
246
|
queueDepth: 0,
|
|
231
247
|
waitingForInput: false,
|
|
232
248
|
updatedAt: { '.sv': 'timestamp' },
|
package/dist/stream.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import type { AgentContext, ContactAddedPayload, ContactApprovedPayload, ContactRemovedPayload, ContactRequestPayload, ConversationUpdatedPayload, MessageCreatedPayload, TypingPayload, PresencePayload, RuntimeUpdatedPayload, TurnUpdatedPayload } from './types.js';
|
|
2
|
+
export interface CanonStreamErrorPayload {
|
|
3
|
+
code?: string;
|
|
4
|
+
message?: string;
|
|
5
|
+
retryAfterMs?: number;
|
|
6
|
+
activeConnections?: number;
|
|
7
|
+
maxConnections?: number;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export declare class CanonStreamError extends Error {
|
|
11
|
+
readonly code?: string;
|
|
12
|
+
readonly retryAfterMs?: number;
|
|
13
|
+
readonly payload: CanonStreamErrorPayload;
|
|
14
|
+
constructor(payload: CanonStreamErrorPayload, fallbackMessage: string);
|
|
15
|
+
}
|
|
2
16
|
export type StreamHandler = {
|
|
3
17
|
onMessage: (payload: MessageCreatedPayload) => void;
|
|
4
18
|
onAgentContext?: (ctx: AgentContext) => void;
|
|
@@ -35,6 +49,7 @@ export declare class CanonStream {
|
|
|
35
49
|
private lastEventId;
|
|
36
50
|
private reconnectAttempt;
|
|
37
51
|
private reconnectTimer;
|
|
52
|
+
private nextRetryAfterMs;
|
|
38
53
|
constructor(opts: {
|
|
39
54
|
apiKey: string;
|
|
40
55
|
agentId: string;
|
|
@@ -49,6 +64,7 @@ export declare class CanonStream {
|
|
|
49
64
|
private connect;
|
|
50
65
|
private readStream;
|
|
51
66
|
private processFrame;
|
|
67
|
+
private handleStreamError;
|
|
52
68
|
private handleAgentContext;
|
|
53
69
|
private handleMessageCreated;
|
|
54
70
|
private handleTyping;
|
package/dist/stream.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { DEFAULT_STREAM_URL } from './constants.js';
|
|
2
2
|
const MAX_BACKOFF_MS = 30_000;
|
|
3
|
+
export class CanonStreamError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
retryAfterMs;
|
|
6
|
+
payload;
|
|
7
|
+
constructor(payload, fallbackMessage) {
|
|
8
|
+
super(payload.message || fallbackMessage);
|
|
9
|
+
this.name = 'CanonStreamError';
|
|
10
|
+
this.code = typeof payload.code === 'string' ? payload.code : undefined;
|
|
11
|
+
this.retryAfterMs = typeof payload.retryAfterMs === 'number' && Number.isFinite(payload.retryAfterMs)
|
|
12
|
+
? Math.max(0, payload.retryAfterMs)
|
|
13
|
+
: undefined;
|
|
14
|
+
this.payload = payload;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
3
17
|
/**
|
|
4
18
|
* Manages a persistent SSE connection to Canon's stream service.
|
|
5
19
|
*
|
|
@@ -16,6 +30,7 @@ export class CanonStream {
|
|
|
16
30
|
lastEventId = null;
|
|
17
31
|
reconnectAttempt = 0;
|
|
18
32
|
reconnectTimer = null;
|
|
33
|
+
nextRetryAfterMs = null;
|
|
19
34
|
constructor(opts) {
|
|
20
35
|
this.apiKey = opts.apiKey;
|
|
21
36
|
this.agentId = opts.agentId;
|
|
@@ -83,7 +98,6 @@ export class CanonStream {
|
|
|
83
98
|
if (!res.ok) {
|
|
84
99
|
throw new Error(`SSE connect failed: ${res.status} ${res.statusText}`);
|
|
85
100
|
}
|
|
86
|
-
this.handler.onConnected?.();
|
|
87
101
|
await this.readStream(res);
|
|
88
102
|
}
|
|
89
103
|
catch (err) {
|
|
@@ -157,6 +171,10 @@ export class CanonStream {
|
|
|
157
171
|
this.reconnectAttempt = 0;
|
|
158
172
|
this.handleAgentContext(data);
|
|
159
173
|
break;
|
|
174
|
+
case 'connected':
|
|
175
|
+
this.reconnectAttempt = 0;
|
|
176
|
+
this.handler.onConnected?.();
|
|
177
|
+
break;
|
|
160
178
|
case 'message.created':
|
|
161
179
|
this.reconnectAttempt = 0;
|
|
162
180
|
this.handleMessageCreated(data);
|
|
@@ -211,12 +229,31 @@ export class CanonStream {
|
|
|
211
229
|
break;
|
|
212
230
|
case 'error':
|
|
213
231
|
// Don't reset backoff — error events mean something is wrong
|
|
214
|
-
this.
|
|
232
|
+
this.handleStreamError(data);
|
|
215
233
|
break;
|
|
216
234
|
default:
|
|
217
235
|
break;
|
|
218
236
|
}
|
|
219
237
|
}
|
|
238
|
+
handleStreamError(raw) {
|
|
239
|
+
let error;
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(raw);
|
|
242
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
243
|
+
error = new CanonStreamError(parsed, `Stream error: ${raw}`);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
error = new Error(`Stream error: ${raw}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
error = new Error(`Stream error: ${raw}`);
|
|
251
|
+
}
|
|
252
|
+
if (error instanceof CanonStreamError && typeof error.retryAfterMs === 'number') {
|
|
253
|
+
this.nextRetryAfterMs = error.retryAfterMs;
|
|
254
|
+
}
|
|
255
|
+
this.handler.onError?.(error);
|
|
256
|
+
}
|
|
220
257
|
handleAgentContext(raw) {
|
|
221
258
|
try {
|
|
222
259
|
const ctx = JSON.parse(raw);
|
|
@@ -332,10 +369,16 @@ export class CanonStream {
|
|
|
332
369
|
scheduleReconnect() {
|
|
333
370
|
if (!this.running)
|
|
334
371
|
return;
|
|
335
|
-
const
|
|
372
|
+
const retryAfterMs = this.nextRetryAfterMs;
|
|
373
|
+
this.nextRetryAfterMs = null;
|
|
374
|
+
const base = typeof retryAfterMs === 'number'
|
|
375
|
+
? retryAfterMs
|
|
376
|
+
: Math.min(1000 * Math.pow(2, this.reconnectAttempt), MAX_BACKOFF_MS);
|
|
336
377
|
const jitter = Math.random() * 0.25 * base;
|
|
337
378
|
const delay = base + jitter;
|
|
338
|
-
|
|
379
|
+
if (typeof retryAfterMs !== 'number') {
|
|
380
|
+
this.reconnectAttempt++;
|
|
381
|
+
}
|
|
339
382
|
this.reconnectTimer = setTimeout(() => {
|
|
340
383
|
this.reconnectTimer = null;
|
|
341
384
|
this.connect();
|
package/dist/turn-protocol.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface RuntimeCapabilities {
|
|
|
11
11
|
supportsNonFinalPermanentMessages: boolean;
|
|
12
12
|
}
|
|
13
13
|
export declare const FINAL_MESSAGE_HANDOFF_MS = 750;
|
|
14
|
+
export declare const ACTIVE_TURN_STALE_THRESHOLD_MS: number;
|
|
15
|
+
export declare const WAITING_INPUT_STALE_THRESHOLD_MS: number;
|
|
14
16
|
export interface TurnState {
|
|
15
17
|
turnId?: string | null;
|
|
16
18
|
state: TurnLifecycleState;
|
|
@@ -20,6 +22,7 @@ export interface TurnState {
|
|
|
20
22
|
activeMessageIds?: string[];
|
|
21
23
|
capabilities?: RuntimeCapabilities;
|
|
22
24
|
openedAt?: number;
|
|
25
|
+
turnUpdatedAt?: number | null;
|
|
23
26
|
updatedAt?: number;
|
|
24
27
|
completedAt?: number | null;
|
|
25
28
|
}
|
|
@@ -57,19 +60,21 @@ export declare const HOST_ADMISSION_ACTION_CAPABILITIES: HostAdmissionActionCapa
|
|
|
57
60
|
export declare const HOST_ADMISSION_ACTIONS_DISABLED: HostAdmissionActionCapabilities;
|
|
58
61
|
export declare function normalizeTurnMetadata(metadata: unknown): TurnMetadata | null;
|
|
59
62
|
export declare function normalizeTurnState(value: unknown): TurnState | null;
|
|
60
|
-
export declare function
|
|
63
|
+
export declare function getTurnStateStaleThresholdMs(state: TurnLifecycleState): number;
|
|
64
|
+
export declare function isTurnStateStale(turnState: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null | undefined): boolean;
|
|
65
|
+
export declare function isTurnOpen(turnState: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null | undefined): boolean;
|
|
61
66
|
export declare function resolveTurnMessageSemantics(input: {
|
|
62
67
|
senderType: 'human' | 'ai_agent';
|
|
63
68
|
metadata?: unknown;
|
|
64
|
-
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
69
|
+
senderTurnState?: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null;
|
|
65
70
|
}): TurnMessageSemantics;
|
|
66
71
|
export declare function shouldPromoteConversationMessage(input: {
|
|
67
72
|
senderType: 'human' | 'ai_agent';
|
|
68
73
|
metadata?: unknown;
|
|
69
|
-
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
74
|
+
senderTurnState?: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null;
|
|
70
75
|
}): boolean;
|
|
71
76
|
export declare function shouldTriggerAgentTurn(input: {
|
|
72
77
|
senderType: 'human' | 'ai_agent';
|
|
73
78
|
metadata?: unknown;
|
|
74
|
-
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
79
|
+
senderTurnState?: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null;
|
|
75
80
|
}): TriggerDecision;
|
package/dist/turn-protocol.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export const FINAL_MESSAGE_HANDOFF_MS = 750;
|
|
2
|
+
export const ACTIVE_TURN_STALE_THRESHOLD_MS = 30 * 60 * 1000;
|
|
3
|
+
export const WAITING_INPUT_STALE_THRESHOLD_MS = 12 * 60 * 60 * 1000;
|
|
2
4
|
const TURN_STATES = [
|
|
3
5
|
'idle',
|
|
4
6
|
'thinking',
|
|
@@ -120,16 +122,41 @@ export function normalizeTurnState(value) {
|
|
|
120
122
|
}
|
|
121
123
|
: undefined,
|
|
122
124
|
openedAt: typeof value.openedAt === 'number' ? value.openedAt : undefined,
|
|
125
|
+
turnUpdatedAt: typeof value.turnUpdatedAt === 'number' ? value.turnUpdatedAt : null,
|
|
123
126
|
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : undefined,
|
|
124
127
|
completedAt: typeof value.completedAt === 'number' ? value.completedAt : null,
|
|
125
128
|
};
|
|
126
129
|
}
|
|
130
|
+
export function getTurnStateStaleThresholdMs(state) {
|
|
131
|
+
return state === 'waiting_input'
|
|
132
|
+
? WAITING_INPUT_STALE_THRESHOLD_MS
|
|
133
|
+
: ACTIVE_TURN_STALE_THRESHOLD_MS;
|
|
134
|
+
}
|
|
135
|
+
export function isTurnStateStale(turnState) {
|
|
136
|
+
if (!turnState)
|
|
137
|
+
return false;
|
|
138
|
+
if (turnState.state !== 'thinking'
|
|
139
|
+
&& turnState.state !== 'streaming'
|
|
140
|
+
&& turnState.state !== 'tool'
|
|
141
|
+
&& turnState.state !== 'waiting_input') {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const updatedAt = turnState.turnUpdatedAt
|
|
145
|
+
?? turnState.updatedAt
|
|
146
|
+
?? turnState.openedAt;
|
|
147
|
+
if (updatedAt == null)
|
|
148
|
+
return false;
|
|
149
|
+
return Date.now() - updatedAt >= getTurnStateStaleThresholdMs(turnState.state);
|
|
150
|
+
}
|
|
127
151
|
export function isTurnOpen(turnState) {
|
|
128
152
|
if (!turnState)
|
|
129
153
|
return false;
|
|
130
|
-
|
|
154
|
+
const open = turnState.state !== 'idle'
|
|
131
155
|
&& turnState.state !== 'completed'
|
|
132
156
|
&& turnState.state !== 'interrupted';
|
|
157
|
+
if (!open)
|
|
158
|
+
return false;
|
|
159
|
+
return !isTurnStateStale(turnState);
|
|
133
160
|
}
|
|
134
161
|
export function resolveTurnMessageSemantics(input) {
|
|
135
162
|
const turnMetadata = normalizeTurnMetadata(input.metadata);
|
package/dist/types.d.ts
CHANGED
|
@@ -751,6 +751,9 @@ export interface AgentSessionSnapshot {
|
|
|
751
751
|
worktreePath?: string | null;
|
|
752
752
|
executionFallbackReason?: string | null;
|
|
753
753
|
turnState?: TurnLifecycleState;
|
|
754
|
+
turnId?: string | null;
|
|
755
|
+
turnOpenedAt?: number | null;
|
|
756
|
+
turnUpdatedAt?: number | null;
|
|
754
757
|
supportsQueue?: boolean;
|
|
755
758
|
supportsInputInterrupt?: boolean;
|
|
756
759
|
queueDepth: number;
|