@canonmsg/core 0.19.2 → 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 +7 -1
- 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 +18 -2
- package/dist/stream.d.ts +16 -0
- package/dist/stream.js +47 -4
- package/dist/turn-protocol.d.ts +11 -5
- package/dist/turn-protocol.js +31 -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,8 +132,14 @@ 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
|
-
supportsInputInterrupt: input.turnState?.capabilities?.
|
|
142
|
+
supportsInputInterrupt: input.turnState?.capabilities?.supportsInputInterrupt,
|
|
137
143
|
queueDepth: input.turnState?.queueDepth ?? 0,
|
|
138
144
|
waitingForInput: input.turnState?.state === 'waiting_input',
|
|
139
145
|
contextUsage: input.sessionState?.contextUsage,
|
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,17 +199,30 @@ 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
|
: {}),
|
|
211
|
-
...(state.capabilities?.
|
|
212
|
-
? { supportsInputInterrupt: state.capabilities.
|
|
224
|
+
...(state.capabilities?.supportsInputInterrupt !== undefined
|
|
225
|
+
? { supportsInputInterrupt: state.capabilities.supportsInputInterrupt }
|
|
213
226
|
: {}),
|
|
214
227
|
queueDepth: state.queueDepth,
|
|
215
228
|
waitingForInput: state.state === 'waiting_input',
|
|
@@ -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
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
export type DeliveryIntent = 'queue' | 'interrupt' | 'interleave' | 'stop';
|
|
2
2
|
export type TurnMessageSemantics = 'progress' | 'turn_complete' | 'control';
|
|
3
|
-
export type InboundDisposition = 'queued' | 'accepted_now' | 'interleaved' | 'trigger_suppressed' | 'rejected';
|
|
3
|
+
export type InboundDisposition = 'queued' | 'accepted_now' | 'interleaved' | 'trigger_suppressed' | 'rejected' | 'cancelled';
|
|
4
4
|
export type TurnLifecycleState = 'idle' | 'thinking' | 'streaming' | 'tool' | 'waiting_input' | 'completed' | 'interrupted';
|
|
5
5
|
export interface RuntimeCapabilities {
|
|
6
6
|
supportsInterrupt: boolean;
|
|
7
|
+
supportsInputInterrupt: boolean;
|
|
7
8
|
supportsQueue: boolean;
|
|
8
9
|
supportsInterleave: boolean;
|
|
9
10
|
supportsRequiresAction: boolean;
|
|
10
11
|
supportsNonFinalPermanentMessages: boolean;
|
|
11
12
|
}
|
|
12
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;
|
|
13
16
|
export interface TurnState {
|
|
14
17
|
turnId?: string | null;
|
|
15
18
|
state: TurnLifecycleState;
|
|
@@ -19,6 +22,7 @@ export interface TurnState {
|
|
|
19
22
|
activeMessageIds?: string[];
|
|
20
23
|
capabilities?: RuntimeCapabilities;
|
|
21
24
|
openedAt?: number;
|
|
25
|
+
turnUpdatedAt?: number | null;
|
|
22
26
|
updatedAt?: number;
|
|
23
27
|
completedAt?: number | null;
|
|
24
28
|
}
|
|
@@ -56,19 +60,21 @@ export declare const HOST_ADMISSION_ACTION_CAPABILITIES: HostAdmissionActionCapa
|
|
|
56
60
|
export declare const HOST_ADMISSION_ACTIONS_DISABLED: HostAdmissionActionCapabilities;
|
|
57
61
|
export declare function normalizeTurnMetadata(metadata: unknown): TurnMetadata | null;
|
|
58
62
|
export declare function normalizeTurnState(value: unknown): TurnState | null;
|
|
59
|
-
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;
|
|
60
66
|
export declare function resolveTurnMessageSemantics(input: {
|
|
61
67
|
senderType: 'human' | 'ai_agent';
|
|
62
68
|
metadata?: unknown;
|
|
63
|
-
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
69
|
+
senderTurnState?: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null;
|
|
64
70
|
}): TurnMessageSemantics;
|
|
65
71
|
export declare function shouldPromoteConversationMessage(input: {
|
|
66
72
|
senderType: 'human' | 'ai_agent';
|
|
67
73
|
metadata?: unknown;
|
|
68
|
-
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
74
|
+
senderTurnState?: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null;
|
|
69
75
|
}): boolean;
|
|
70
76
|
export declare function shouldTriggerAgentTurn(input: {
|
|
71
77
|
senderType: 'human' | 'ai_agent';
|
|
72
78
|
metadata?: unknown;
|
|
73
|
-
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
79
|
+
senderTurnState?: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null;
|
|
74
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',
|
|
@@ -21,6 +23,7 @@ const DELIVERY_INTENTS = [
|
|
|
21
23
|
];
|
|
22
24
|
export const DEFAULT_RUNTIME_CAPABILITIES = {
|
|
23
25
|
supportsInterrupt: false,
|
|
26
|
+
supportsInputInterrupt: false,
|
|
24
27
|
supportsQueue: true,
|
|
25
28
|
supportsInterleave: false,
|
|
26
29
|
supportsRequiresAction: false,
|
|
@@ -73,6 +76,7 @@ export function normalizeTurnMetadata(metadata) {
|
|
|
73
76
|
|| metadata.inboundDisposition === 'interleaved'
|
|
74
77
|
|| metadata.inboundDisposition === 'trigger_suppressed'
|
|
75
78
|
|| metadata.inboundDisposition === 'rejected'
|
|
79
|
+
|| metadata.inboundDisposition === 'cancelled'
|
|
76
80
|
? metadata.inboundDisposition
|
|
77
81
|
: undefined;
|
|
78
82
|
if (!turnId && !turnSemantics && !deliveryIntent && typeof metadata.turnComplete !== 'boolean'
|
|
@@ -110,6 +114,7 @@ export function normalizeTurnState(value) {
|
|
|
110
114
|
capabilities: isRecord(value.capabilities)
|
|
111
115
|
? {
|
|
112
116
|
supportsInterrupt: Boolean(value.capabilities.supportsInterrupt),
|
|
117
|
+
supportsInputInterrupt: Boolean(value.capabilities.supportsInputInterrupt),
|
|
113
118
|
supportsQueue: value.capabilities.supportsQueue !== false,
|
|
114
119
|
supportsInterleave: Boolean(value.capabilities.supportsInterleave),
|
|
115
120
|
supportsRequiresAction: Boolean(value.capabilities.supportsRequiresAction),
|
|
@@ -117,16 +122,41 @@ export function normalizeTurnState(value) {
|
|
|
117
122
|
}
|
|
118
123
|
: undefined,
|
|
119
124
|
openedAt: typeof value.openedAt === 'number' ? value.openedAt : undefined,
|
|
125
|
+
turnUpdatedAt: typeof value.turnUpdatedAt === 'number' ? value.turnUpdatedAt : null,
|
|
120
126
|
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : undefined,
|
|
121
127
|
completedAt: typeof value.completedAt === 'number' ? value.completedAt : null,
|
|
122
128
|
};
|
|
123
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
|
+
}
|
|
124
151
|
export function isTurnOpen(turnState) {
|
|
125
152
|
if (!turnState)
|
|
126
153
|
return false;
|
|
127
|
-
|
|
154
|
+
const open = turnState.state !== 'idle'
|
|
128
155
|
&& turnState.state !== 'completed'
|
|
129
156
|
&& turnState.state !== 'interrupted';
|
|
157
|
+
if (!open)
|
|
158
|
+
return false;
|
|
159
|
+
return !isTurnStateStale(turnState);
|
|
130
160
|
}
|
|
131
161
|
export function resolveTurnMessageSemantics(input) {
|
|
132
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;
|