@canonmsg/core 0.24.0 → 1.0.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.
@@ -22,8 +22,3 @@ export declare function buildApprovalReply(approvalId: string, decision: 'allow'
22
22
  metadata: ApprovalReplyMetadata;
23
23
  };
24
24
  export declare function buildApprovalOutcome(approvalId: string, toolName: string, toolSummary: string, decision: 'allow' | 'deny', reason: 'replied' | 'timeout' | 'session-rule'): string;
25
- export declare function parseTextApprovalReply(text: string): {
26
- decision: 'allow' | 'deny';
27
- sessionRule?: SessionRule;
28
- targetApprovalId?: string;
29
- } | null;
@@ -104,68 +104,3 @@ export function buildApprovalOutcome(approvalId, toolName, toolSummary, decision
104
104
  }
105
105
  return `${icon} [${approvalId}] -- ${toolName}: ${short}`;
106
106
  }
107
- // ── Parse text-based approval reply (fallback for non-card UIs) ─────
108
- const ALLOW_WORDS = new Set([
109
- 'approve', 'approved', 'allow', 'yes', 'y', 'ok', 'go', 'do it', 'go ahead',
110
- ]);
111
- const DENY_WORDS = new Set([
112
- 'deny', 'denied', 'reject', 'no', 'n', 'nope', 'stop', "don't",
113
- ]);
114
- export function parseTextApprovalReply(text) {
115
- const trimmed = text.trim().toLowerCase();
116
- if (!trimmed)
117
- return null;
118
- // "approve apr_xxxxx" or "deny apr_xxxxx" — target specific approval
119
- const idMatch = trimmed.match(/^(approve|deny|allow|reject)\s+(apr_\w+)$/);
120
- if (idMatch) {
121
- const decision = idMatch[1] === 'deny' || idMatch[1] === 'reject' ? 'deny' : 'allow';
122
- return { decision, targetApprovalId: idMatch[2] };
123
- }
124
- // "approve all for Xm" — time-limited blanket
125
- const timeMatch = trimmed.match(/^approve\s+all\s+for\s+(\d+)\s*m(?:in(?:utes?)?)?$/);
126
- if (timeMatch) {
127
- const minutes = parseInt(timeMatch[1], 10);
128
- return {
129
- decision: 'allow',
130
- sessionRule: {
131
- type: 'approve-all',
132
- expiresAt: new Date(Date.now() + minutes * 60_000).toISOString(),
133
- },
134
- };
135
- }
136
- // "approve all <ToolName>" — per-tool blanket
137
- const toolMatch = trimmed.match(/^approve\s+all\s+(\w+)$/);
138
- if (toolMatch && toolMatch[1] !== 'for') {
139
- return {
140
- decision: 'allow',
141
- sessionRule: {
142
- type: 'approve-tool',
143
- toolPattern: toolMatch[1],
144
- },
145
- };
146
- }
147
- // "deny all <ToolName>"
148
- const denyToolMatch = trimmed.match(/^deny\s+all\s+(\w+)$/);
149
- if (denyToolMatch) {
150
- return {
151
- decision: 'deny',
152
- sessionRule: {
153
- type: 'deny-tool',
154
- toolPattern: denyToolMatch[1],
155
- },
156
- };
157
- }
158
- // "approve all" — session blanket
159
- if (trimmed === 'approve all' || trimmed === 'allow all') {
160
- return {
161
- decision: 'allow',
162
- sessionRule: { type: 'approve-all' },
163
- };
164
- }
165
- // Simple allow/deny words
166
- if (ALLOW_WORDS.has(trimmed))
167
- return { decision: 'allow' };
168
- if (DENY_WORDS.has(trimmed))
169
- return { decision: 'deny' };
170
- return null;
171
- }
@@ -43,7 +43,6 @@ export declare class ApprovalManager {
43
43
  */
44
44
  handleMessage(conversationId: string, message: {
45
45
  senderId: string;
46
- text?: string;
47
46
  metadata?: Record<string, unknown>;
48
47
  }): boolean;
49
48
  /** Check if a tool would be auto-resolved by a session rule */
@@ -53,7 +52,6 @@ export declare class ApprovalManager {
53
52
  get pendingCount(): number;
54
53
  dispose(): void;
55
54
  private resolveApproval;
56
- private findMostRecentPending;
57
55
  private pruneExpiredRules;
58
56
  private summarizeTool;
59
57
  private describeRule;
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_APPROVAL_CONFIG, parseApprovalReplyMetadata } from './approval-types.js';
2
- import { generateApprovalId, buildApprovalRequest, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
2
+ import { generateApprovalId, buildApprovalRequest, buildApprovalOutcome, redactSecrets, } from './approval-format.js';
3
3
  // ── ApprovalManager ─────────────────────────────────────────────────
4
4
  /**
5
5
  * Platform-agnostic approval protocol for Canon.
@@ -156,21 +156,6 @@ export class ApprovalManager {
156
156
  return false;
157
157
  return this.resolveApproval(parsed.approvalId, parsed.decision, parsed.sessionRule, conversationId);
158
158
  }
159
- // Fall back to text parsing
160
- if (message.text) {
161
- const parsed = parseTextApprovalReply(message.text);
162
- if (!parsed)
163
- return false;
164
- // If the user targeted a specific approval ID
165
- if (parsed.targetApprovalId) {
166
- return this.resolveApproval(parsed.targetApprovalId, parsed.decision, parsed.sessionRule, conversationId);
167
- }
168
- // Otherwise match the most recent pending in this conversation
169
- const pending = this.findMostRecentPending(conversationId);
170
- if (!pending)
171
- return false;
172
- return this.resolveApproval(pending.approvalId, parsed.decision, parsed.sessionRule, conversationId);
173
- }
174
159
  return false;
175
160
  }
176
161
  /** Check if a tool would be auto-resolved by a session rule */
@@ -258,15 +243,6 @@ export class ApprovalManager {
258
243
  entry.resolve({ decision, ...(acceptedSessionRule ? { sessionRule: acceptedSessionRule } : {}) });
259
244
  return true;
260
245
  }
261
- findMostRecentPending(conversationId) {
262
- let latest = null;
263
- for (const p of this.pending.values()) {
264
- if (p.conversationId === conversationId) {
265
- latest = p; // Map iterates in insertion order, so last = most recent
266
- }
267
- }
268
- return latest;
269
- }
270
246
  pruneExpiredRules() {
271
247
  const now = Date.now();
272
248
  this.rules = this.rules.filter(({ rule }) => {
package/dist/browser.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
2
2
  export { resolveCanonBaseUrl } from './base-url.js';
3
3
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
4
- export type { AddMemberResult, AgentCapabilities, AgentClientType, AgentSessionSnapshot, AgentRuntime, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonResolveAdmissionResult, ContactAddedPayload, ContactApprovedPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonStreamEvent, CreateContactRequestResult, MediaAttachment, MediaAttachmentKind, ModelOption, PermissionModeOption, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimePresentationField, CanonRuntimePresentationHint, CanonRuntimePresentationPolicy, CanonRuntimePresentationPreset, CanonRuntimeVisibility, CanonRuntimeProvenance, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, RuntimeUpdatedPayload, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, ResolvedAdmission, SessionConfig, TurnUpdatedPayload, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
4
+ export type { AddMemberResult, AgentCapabilities, AgentClientType, AgentSessionSnapshot, AgentRuntime, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonResolveAdmissionResult, ContactAddedPayload, ContactApprovedPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonStreamEvent, CreateContactRequestResult, MediaAttachment, MediaAttachmentKind, ModelOption, PermissionModeOption, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimePresentationField, CanonRuntimePresentationHint, CanonRuntimePresentationPolicy, CanonRuntimePresentationPreset, CanonRuntimeVisibility, CanonRuntimeProvenance, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, RuntimeUpdatedPayload, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, ResolvedAdmission, SessionConfig, TurnOutputBlock, TurnOutputBlockKind, TurnOutputBlockStatus, TurnUpdatedPayload, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
5
5
  export { DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, RUNTIME_PRESENTATION_FIELDS, buildRuntimePresentationPolicy, getRuntimePresentationHint, isRuntimePresentationField, isRuntimePresentationFieldHidden, redactAgentSessionSnapshotForConversation, redactExecutionMetadataForConversation, redactRuntimeDescriptorForConversation, redactRuntimeInfoForConversation, } from './runtime-presentation.js';
6
6
  export { buildRuntimeProvenance, resolveRuntimeProvenance, } from './provenance.js';
7
7
  export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
@@ -14,8 +14,10 @@ export type { AgentBehaviorSettings, ParticipationHistoryMessage, ParticipationH
14
14
  export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
15
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
- export { buildApprovalReply, buildApprovalRequest, buildApprovalOutcome, generateApprovalId, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
17
+ export { buildApprovalReply, buildApprovalRequest, buildApprovalOutcome, generateApprovalId, redactSecrets, } from './approval-format.js';
18
18
  export type { ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalNativeRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
19
19
  export { DEFAULT_APPROVAL_CONFIG, parseApprovalRequestMetadata, parseApprovalReplyMetadata, parseSessionRule, } from './approval-types.js';
20
20
  export { buildRuntimeInputOutcome, buildRuntimeInputReply, buildRuntimeInputRequest, buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, buildQuestionRequest, parseRuntimeInputOutcomeMetadata, parseRuntimeInputReplyMetadata, parseRuntimeInputRequestMetadata, } from './runtime-cards.js';
21
- export type { ClaudeQuestionMetadata, ClaudeQuestionReplyMetadata, PlanApprovalMetadata, PlanApprovalReplyMetadata, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputOutcomeMetadata, RuntimeInputReplyMetadata, RuntimeInputReplyStatus, RuntimeInputRequestMetadata, RuntimeInputResolutionStatus, RuntimeQuestionDefinition, RuntimeQuestionOption, } from './runtime-cards.js';
21
+ export type { ClaudeQuestionMetadata, ClaudeQuestionReplyMetadata, PlanApprovalMetadata, PlanApprovalReplyMetadata, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputOutcomeMetadata, RuntimeInputQuestion, RuntimeInputReplyMetadata, RuntimeInputReplyStatus, RuntimeInputRequestMetadata, RuntimeInputResolutionStatus, RuntimeQuestionDefinition, RuntimeQuestionOption, } from './runtime-cards.js';
22
+ export { DEFAULT_TURN_TRAIL_MAX_BLOCKS, DEFAULT_TURN_TRAIL_METADATA_BUDGET_BYTES, buildBoundedTurnTrail, } from './turn-output-controller.js';
23
+ export type { BuildBoundedTurnTrailOptions, TurnOutputBlockInput, } from './turn-output-controller.js';
package/dist/browser.js CHANGED
@@ -9,6 +9,7 @@ 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
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
- export { buildApprovalReply, buildApprovalRequest, buildApprovalOutcome, generateApprovalId, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
12
+ export { buildApprovalReply, buildApprovalRequest, buildApprovalOutcome, generateApprovalId, 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';
15
+ export { DEFAULT_TURN_TRAIL_MAX_BLOCKS, DEFAULT_TURN_TRAIL_METADATA_BUDGET_BYTES, buildBoundedTurnTrail, } from './turn-output-controller.js';
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type CanonMessage, type CanonConversation, type CanonContact, type CanonContactRequest, type CanonMessagesPage, type CanonResolveAdmissionResult, type AgentContext, type AddMemberResult, type CreateContactRequestResult, type MediaAttachment, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetRuntimeTurnOptions, type SetStreamingOptions } from './types.js';
2
2
  import type { RuntimeInputKind } from './runtime-cards.js';
3
3
  import type { ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, SessionRule } from './approval-types.js';
4
- import type { RuntimeInputChoice, RuntimeInputNativeMetadata } from './runtime-cards.js';
4
+ import type { RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputNativeMetadata, RuntimeInputQuestion } from './runtime-cards.js';
5
5
  import type { SendContextualMessageOptions, SendContextualMessageResult } from './self-context.js';
6
6
  import type { InboundDisposition } from './turn-protocol.js';
7
7
  /**
@@ -65,6 +65,7 @@ export declare class CanonClient {
65
65
  title?: string;
66
66
  prompt?: string;
67
67
  choices?: RuntimeInputChoice[];
68
+ questions?: RuntimeInputQuestion[];
68
69
  secretName?: string;
69
70
  native?: RuntimeInputNativeMetadata;
70
71
  sensitive?: boolean;
@@ -89,6 +90,7 @@ export declare class CanonClient {
89
90
  inputId: string;
90
91
  kind: RuntimeInputKind;
91
92
  value: string;
93
+ answers?: RuntimeInputAnswers;
92
94
  } | {
93
95
  status: 'cancelled';
94
96
  inputId: string;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
2
- export type { AddMemberResult, AgentCapabilities, AgentClientType, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonGroupContext, CanonGroupContextMode, CanonKnownRecentParticipant, CanonMembershipChange, CanonResolveAdmissionResult, ConversationUpdatedPayload, ContactAddedPayload, ContactApprovedPayload, ContactCardPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonMessage, CanonRuntimeProvenance, CanonConversation, CanonMessagesPage, CreateContactRequestResult, AgentContext, CanonStreamEvent, AgentSessionSnapshot, ResolvedAdmission, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, RuntimeUpdatedPayload, TurnUpdatedPayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, SetRuntimeTurnOptions, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimePresentationField, CanonRuntimePresentationHint, CanonRuntimePresentationPolicy, CanonRuntimePresentationPreset, CanonRuntimeVisibility, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, ModelOption, PermissionModeOption, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
2
+ export type { AddMemberResult, AgentCapabilities, AgentClientType, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContact, CanonContactRequest, CanonContactRequestStatus, CanonGroupContext, CanonGroupContextMode, CanonKnownRecentParticipant, CanonMembershipChange, CanonResolveAdmissionResult, ConversationUpdatedPayload, ContactAddedPayload, ContactApprovedPayload, ContactCardPayload, ContactRemovedPayload, ContactRequestPayload, ContactSource, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonMessage, CanonRuntimeProvenance, CanonConversation, CanonMessagesPage, CreateContactRequestResult, AgentContext, CanonStreamEvent, AgentSessionSnapshot, ResolvedAdmission, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, RuntimeUpdatedPayload, TurnUpdatedPayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, SetRuntimeTurnOptions, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeCommandArgumentChoice, CanonRuntimeCommandArgumentDescriptor, CanonRuntimeCommandArgumentKind, CanonRuntimeCommandDescriptor, CanonRuntimeDetailTier, CanonRuntimeExecutionMetadata, CanonRuntimePresentationField, CanonRuntimePresentationHint, CanonRuntimePresentationPolicy, CanonRuntimePresentationPreset, CanonRuntimeVisibility, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimePrimitiveId, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, ModelOption, PermissionModeOption, RuntimeInfoPayload, RuntimeControlError, RuntimeControlState, RuntimeControlValueSource, TurnOutputBlock, TurnOutputBlockKind, TurnOutputBlockStatus, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
3
3
  export { DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, RUNTIME_PRESENTATION_FIELDS, buildRuntimePresentationPolicy, getRuntimePresentationHint, isRuntimePresentationField, isRuntimePresentationFieldHidden, redactAgentRuntimeForConversation, redactAgentSessionSnapshotForConversation, redactExecutionMetadataForConversation, redactRuntimeActivityItemForConversation, redactRuntimeDescriptorForConversation, redactRuntimeInfoForConversation, redactSessionStateForConversation, } from './runtime-presentation.js';
4
4
  export { buildRuntimeProvenance, resolveRuntimeProvenance, } from './provenance.js';
5
5
  export type { CanonSelfContext, CanonSelfContextType, SelfContextPromptRenderOptions, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, } from './self-context.js';
@@ -17,15 +17,15 @@ export { DEFAULT_RUNTIME_CAPABILITIES, ACTIVE_TURN_STALE_THRESHOLD_MS, HOST_ADMI
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';
20
- export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
20
+ export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, redactSecrets, } from './approval-format.js';
21
21
  export { DEFAULT_APPROVAL_CONFIG, parseApprovalRequestMetadata, parseApprovalReplyMetadata, parseSessionRule, } from './approval-types.js';
22
22
  export type { ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalNativeRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
23
23
  export { buildRuntimeInputOutcome, buildRuntimeInputReply, buildRuntimeInputRequest, buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, buildQuestionRequest, parseRuntimeInputOutcomeMetadata, parseRuntimeInputReplyMetadata, parseRuntimeInputRequestMetadata, } from './runtime-cards.js';
24
- export type { ClaudeQuestionMetadata, ClaudeQuestionReplyMetadata, PlanApprovalMetadata, PlanApprovalReplyMetadata, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputOutcomeMetadata, RuntimeInputReplyMetadata, RuntimeInputReplyStatus, RuntimeInputRequestMetadata, RuntimeInputResolutionStatus, RuntimeQuestionDefinition, RuntimeQuestionOption, } from './runtime-cards.js';
24
+ export type { ClaudeQuestionMetadata, ClaudeQuestionReplyMetadata, PlanApprovalMetadata, PlanApprovalReplyMetadata, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputOutcomeMetadata, RuntimeInputQuestion, RuntimeInputReplyMetadata, RuntimeInputReplyStatus, RuntimeInputRequestMetadata, RuntimeInputResolutionStatus, RuntimeQuestionDefinition, RuntimeQuestionOption, } from './runtime-cards.js';
25
25
  export { createStreamingHelper } from './streaming.js';
26
26
  export type { RTDBHandle, RTDBRef, ServerTimestamp, StreamingHelperOptions, StreamingNode } from './streaming.js';
27
- export { createTurnOutputController } from './turn-output-controller.js';
28
- export type { TurnOutputController, TurnOutputControllerOptions, TurnOutputMode, TurnOutputSnapshot, } from './turn-output-controller.js';
27
+ export { DEFAULT_TURN_TRAIL_MAX_BLOCKS, DEFAULT_TURN_TRAIL_METADATA_BUDGET_BYTES, buildBoundedTurnTrail, createTurnOutputController, } from './turn-output-controller.js';
28
+ export type { BuildBoundedTurnTrailOptions, TurnOutputController, TurnOutputBlockInput, TurnOutputControllerOptions, TurnOutputMode, TurnOutputSnapshot, } from './turn-output-controller.js';
29
29
  export { clearPendingRegistration, getOrCreatePendingRegistration, loadPendingRegistrations, loadProfiles, savePendingRegistrations, saveProfiles, updatePendingRegistration, upsertAgentProfile, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
30
30
  export type { AgentProfile, PendingRegistration, ProfileLockHandle } from './agent-profiles.js';
31
31
  export { resolveCanonAgent, resolveCanonProfile, getActiveProfile, getActiveProfileLock } from './agent-resolver.js';
package/dist/index.js CHANGED
@@ -17,12 +17,12 @@ export { DEFAULT_RUNTIME_CAPABILITIES, ACTIVE_TURN_STALE_THRESHOLD_MS, HOST_ADMI
17
17
  export { ackRegistrationApproval, registerAndWaitForApproval, submitRegistrationRequest, waitForRegistrationApproval, } from './registration.js';
18
18
  // Approval
19
19
  export { ApprovalManager } from './approval-manager.js';
20
- export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
20
+ export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, redactSecrets, } from './approval-format.js';
21
21
  export { DEFAULT_APPROVAL_CONFIG, parseApprovalRequestMetadata, parseApprovalReplyMetadata, parseSessionRule, } from './approval-types.js';
22
22
  export { buildRuntimeInputOutcome, buildRuntimeInputReply, buildRuntimeInputRequest, buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, buildQuestionRequest, parseRuntimeInputOutcomeMetadata, parseRuntimeInputReplyMetadata, parseRuntimeInputRequestMetadata, } from './runtime-cards.js';
23
23
  // Streaming (RTDB helpers)
24
24
  export { createStreamingHelper } from './streaming.js';
25
- export { createTurnOutputController } from './turn-output-controller.js';
25
+ export { DEFAULT_TURN_TRAIL_MAX_BLOCKS, DEFAULT_TURN_TRAIL_METADATA_BUDGET_BYTES, buildBoundedTurnTrail, createTurnOutputController, } from './turn-output-controller.js';
26
26
  // Agent profiles (loading, locking, resolution)
27
27
  export { clearPendingRegistration, getOrCreatePendingRegistration, loadPendingRegistrations, loadProfiles, savePendingRegistrations, saveProfiles, updatePendingRegistration, upsertAgentProfile, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
28
28
  // Agent resolver
@@ -47,6 +47,18 @@ export interface RuntimeInputChoice {
47
47
  value?: string;
48
48
  description?: string;
49
49
  }
50
+ export interface RuntimeInputQuestion {
51
+ id: string;
52
+ question: string;
53
+ header?: string;
54
+ choices?: RuntimeInputChoice[];
55
+ allowOther?: boolean;
56
+ isSecret?: boolean;
57
+ multiSelect?: boolean;
58
+ }
59
+ export type RuntimeInputAnswers = Record<string, {
60
+ answers: string[];
61
+ }>;
50
62
  export interface RuntimeInputNativeMetadata {
51
63
  runtime?: string;
52
64
  method?: string;
@@ -64,6 +76,7 @@ export interface RuntimeInputRequestMetadata {
64
76
  prompt: string;
65
77
  title?: string;
66
78
  choices?: RuntimeInputChoice[];
79
+ questions?: RuntimeInputQuestion[];
67
80
  secretName?: string;
68
81
  native?: RuntimeInputNativeMetadata;
69
82
  expiresAt: string;
@@ -75,6 +88,7 @@ export interface RuntimeInputReplyMetadata {
75
88
  kind?: RuntimeInputKind;
76
89
  status: RuntimeInputReplyStatus;
77
90
  answerSummary?: string;
91
+ answersSummary?: string;
78
92
  sensitive?: boolean;
79
93
  }
80
94
  export interface RuntimeInputOutcomeMetadata {
@@ -109,6 +123,7 @@ export declare function buildRuntimeInputRequest(inputId: string, input: Omit<Ru
109
123
  export declare function buildRuntimeInputReply(inputId: string, status: RuntimeInputReplyStatus, details?: {
110
124
  kind?: RuntimeInputKind;
111
125
  answerSummary?: string;
126
+ answersSummary?: string;
112
127
  sensitive?: boolean;
113
128
  }): {
114
129
  text: string;
@@ -38,6 +38,30 @@ function normalizeRuntimeInputChoices(value) {
38
38
  });
39
39
  return choices.length > 0 ? choices : undefined;
40
40
  }
41
+ function normalizeRuntimeInputQuestions(value) {
42
+ if (!Array.isArray(value))
43
+ return undefined;
44
+ const questions = value.slice(0, 12).flatMap((entry) => {
45
+ if (!isRecord(entry))
46
+ return [];
47
+ const id = normalizeString(entry.id, 120);
48
+ const question = normalizeString(entry.question, 1000);
49
+ if (!id || !/^[a-zA-Z0-9_.:-]{1,120}$/.test(id) || !question)
50
+ return [];
51
+ const header = normalizeString(entry.header, 120) ?? undefined;
52
+ const choices = normalizeRuntimeInputChoices(entry.choices);
53
+ return [{
54
+ id,
55
+ question,
56
+ ...(header ? { header } : {}),
57
+ ...(choices ? { choices } : {}),
58
+ ...(entry.allowOther === true ? { allowOther: true } : {}),
59
+ ...(entry.isSecret === true ? { isSecret: true } : {}),
60
+ ...(entry.multiSelect === true ? { multiSelect: true } : {}),
61
+ }];
62
+ });
63
+ return questions.length > 0 ? questions : undefined;
64
+ }
41
65
  function normalizeRuntimeInputNative(value) {
42
66
  if (!isRecord(value))
43
67
  return undefined;
@@ -130,6 +154,7 @@ export function buildRuntimeInputRequest(inputId, input) {
130
154
  prompt: input.prompt.slice(0, 1000),
131
155
  title: title.slice(0, 120),
132
156
  ...(input.choices?.length ? { choices: input.choices.slice(0, 12) } : {}),
157
+ ...(input.questions?.length ? { questions: input.questions.slice(0, 12) } : {}),
133
158
  ...(input.secretName ? { secretName: input.secretName.slice(0, 120) } : {}),
134
159
  ...(input.native ? { native: input.native } : {}),
135
160
  expiresAt: input.expiresAt,
@@ -139,6 +164,7 @@ export function buildRuntimeInputRequest(inputId, input) {
139
164
  }
140
165
  export function buildRuntimeInputReply(inputId, status, details) {
141
166
  const answerSummary = details?.sensitive ? undefined : details?.answerSummary?.trim().slice(0, 500);
167
+ const answersSummary = details?.sensitive ? undefined : details?.answersSummary?.trim().slice(0, 500);
142
168
  return {
143
169
  text: status === 'submitted' ? 'Submitted' : 'Cancelled',
144
170
  metadata: {
@@ -147,6 +173,7 @@ export function buildRuntimeInputReply(inputId, status, details) {
147
173
  ...(details?.kind ? { kind: details.kind } : {}),
148
174
  status,
149
175
  ...(answerSummary ? { answerSummary } : {}),
176
+ ...(answersSummary ? { answersSummary } : {}),
150
177
  ...(details?.sensitive ? { sensitive: true } : {}),
151
178
  },
152
179
  };
@@ -184,6 +211,7 @@ export function parseRuntimeInputRequestMetadata(value) {
184
211
  return null;
185
212
  const title = normalizeString(value.title, 120) ?? undefined;
186
213
  const choices = normalizeRuntimeInputChoices(value.choices);
214
+ const questions = normalizeRuntimeInputQuestions(value.questions);
187
215
  const secretName = normalizeString(value.secretName, 120) ?? undefined;
188
216
  const native = normalizeRuntimeInputNative(value.native);
189
217
  return {
@@ -194,6 +222,7 @@ export function parseRuntimeInputRequestMetadata(value) {
194
222
  prompt,
195
223
  ...(title ? { title } : {}),
196
224
  ...(choices ? { choices } : {}),
225
+ ...(questions ? { questions } : {}),
197
226
  ...(secretName ? { secretName } : {}),
198
227
  ...(native ? { native } : {}),
199
228
  expiresAt: new Date(expiresMs).toISOString(),
@@ -211,12 +240,14 @@ export function parseRuntimeInputReplyMetadata(value) {
211
240
  return null;
212
241
  const kind = normalizeRuntimeInputKind(value.kind) ?? undefined;
213
242
  const answerSummary = value.sensitive === true ? undefined : normalizeString(value.answerSummary, 500) ?? undefined;
243
+ const answersSummary = value.sensitive === true ? undefined : normalizeString(value.answersSummary, 500) ?? undefined;
214
244
  return {
215
245
  type: 'runtime_input_reply',
216
246
  inputId,
217
247
  ...(kind ? { kind } : {}),
218
248
  status,
219
249
  ...(answerSummary ? { answerSummary } : {}),
250
+ ...(answersSummary ? { answersSummary } : {}),
220
251
  ...(value.sensitive === true ? { sensitive: true } : {}),
221
252
  };
222
253
  }
@@ -1,5 +1,5 @@
1
1
  import type { ExecutionEnvironmentMode } from './execution-environment-mode.js';
2
- import { type AgentClientType, type CanonControlDescriptor, type CanonRuntimeActionDescriptor, type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimeStreamingMode, type CanonWorkspaceRootMetadata, type ModelOption, type PermissionModeOption, type WorkspaceOption } from './types.js';
2
+ import { type AgentClientType, type CanonControlDescriptor, type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimeStreamingMode, type CanonWorkspaceRootMetadata, type ModelOption, type PermissionModeOption, type WorkspaceOption } from './types.js';
3
3
  import type { CanonRuntimePresentationPolicy } from './types.js';
4
4
  import type { HostAdmissionActionCapabilities } from './turn-protocol.js';
5
5
  export declare const CLAUDE_EFFORT_OPTIONS: readonly [{
@@ -37,9 +37,9 @@ export declare function buildRuntimePermissionModeControl(input: {
37
37
  defaultValue?: string | null;
38
38
  }): CanonControlDescriptor;
39
39
  export declare function buildRuntimeEffortControl(): CanonControlDescriptor;
40
- export declare const RUNTIME_STOP_ACTION: CanonRuntimeActionDescriptor;
41
- export declare const RUNTIME_STOP_AND_DROP_ACTION: CanonRuntimeActionDescriptor;
42
- export declare const RUNTIME_NEW_SESSION_ACTION: CanonRuntimeActionDescriptor;
40
+ export declare const RUNTIME_STOP_ACTION: CanonRuntimeCommandDescriptor;
41
+ export declare const RUNTIME_STOP_AND_DROP_ACTION: CanonRuntimeCommandDescriptor;
42
+ export declare const RUNTIME_NEW_SESSION_ACTION: CanonRuntimeCommandDescriptor;
43
43
  export declare function buildFirstPartyCodingRuntimeDescriptor(input: {
44
44
  clientType: Extract<AgentClientType, 'claude-code' | 'codex'>;
45
45
  models: ReadonlyArray<ModelOption>;
@@ -48,7 +48,6 @@ export declare function buildFirstPartyCodingRuntimeDescriptor(input: {
48
48
  executionModes: ReadonlyArray<ExecutionEnvironmentMode>;
49
49
  permissionModes?: ReadonlyArray<PermissionModeOption>;
50
50
  defaultPermissionMode?: string | null;
51
- actions?: ReadonlyArray<CanonRuntimeActionDescriptor>;
52
51
  commands?: ReadonlyArray<CanonRuntimeCommandDescriptor>;
53
52
  streamingTextMode: CanonRuntimeStreamingMode;
54
53
  admissionActions?: HostAdmissionActionCapabilities;
@@ -151,7 +151,6 @@ export function buildFirstPartyCodingRuntimeDescriptor(input) {
151
151
  ...(input.clientType === 'claude-code' ? [buildRuntimeEffortControl()] : []),
152
152
  ],
153
153
  commands: input.commands,
154
- actions: input.actions,
155
154
  workspaceRoots: input.workspaceRoots,
156
155
  supportsInterrupt: true,
157
156
  supportsInputInterrupt: true,
@@ -177,14 +177,11 @@ export function redactRuntimeDescriptorForConversation(descriptor) {
177
177
  .filter((control) => Boolean(control));
178
178
  const commands = descriptor.commands?.map((command) => redactAction(command))
179
179
  .filter((command) => Boolean(command));
180
- const actions = descriptor.actions?.map((action) => redactAction(action))
181
- .filter((action) => Boolean(action));
182
180
  return cloneDefined({
183
181
  ...descriptor,
184
182
  coreControls,
185
183
  runtimeControls,
186
184
  commands,
187
- actions,
188
185
  workspaceRoots: isRuntimePresentationFieldHidden(descriptor, 'workspaceRoot')
189
186
  ? []
190
187
  : descriptor.workspaceRoots,
@@ -1,4 +1,4 @@
1
- import type { AgentClientType, AgentRuntime, CanonRuntimeActivityItem, RuntimeInfoPayload } from './types.js';
1
+ import type { AgentClientType, AgentRuntime, CanonRuntimeActivityItem, RuntimeInfoPayload, TurnOutputBlock } from './types.js';
2
2
  import { type AgentSessionSnapshotPatch, type RTDBClientHandle, type SessionStatePayload, type TurnStatePayload } from './rtdb-rest.js';
3
3
  export interface RuntimeStatePublisherOptions {
4
4
  agentId: string;
@@ -8,9 +8,10 @@ export interface RuntimeStatePublisherOptions {
8
8
  }
9
9
  export interface RuntimeStreamingPayload {
10
10
  text: string;
11
- status: 'thinking' | 'streaming' | 'tool';
11
+ status: 'thinking' | 'streaming' | 'tool' | 'waiting_input';
12
12
  messageId?: string;
13
13
  turnId?: string | null;
14
+ blocks?: TurnOutputBlock[];
14
15
  updatedAt?: number | {
15
16
  '.sv': 'timestamp';
16
17
  };
@@ -25,7 +25,6 @@ function hasPreservableRuntimeState(value) {
25
25
  return hasNonEmptyArray(runtimeDescriptor.coreControls)
26
26
  || hasNonEmptyArray(runtimeDescriptor.runtimeControls)
27
27
  || hasNonEmptyArray(runtimeDescriptor.commands)
28
- || hasNonEmptyArray(runtimeDescriptor.actions)
29
28
  || hasNonEmptyArray(runtimeDescriptor.workspaceRoots)
30
29
  || hasNonEmptyArray(runtimeDescriptor.writableRoots)
31
30
  || hasEnabledAdmissionActions(runtimeDescriptor.admissionActions)
@@ -1,4 +1,4 @@
1
- import type { StreamingStatus } from './types.js';
1
+ import type { StreamingStatus, TurnOutputBlock } from './types.js';
2
2
  /** Minimal subset of firebase-admin Database to avoid a hard dependency. */
3
3
  export interface RTDBHandle {
4
4
  ref(path: string): RTDBRef;
@@ -24,6 +24,7 @@ export interface StreamingNode {
24
24
  updatedAt: unknown;
25
25
  messageId: string;
26
26
  turnId?: string | null;
27
+ blocks?: TurnOutputBlock[];
27
28
  }
28
29
  /**
29
30
  * Creates helpers that read/write the RTDB streaming node for a single
@@ -44,7 +45,7 @@ export declare function createStreamingHelper(opts: StreamingHelperOptions): {
44
45
  /** Write the initial streaming node (status: "thinking", empty text). */
45
46
  start(messageId: string, turnId?: string | null): Promise<void>;
46
47
  /** Update the accumulated text and optionally change status. */
47
- update(text: string, status?: StreamingStatus): Promise<void>;
48
+ update(text: string, status?: StreamingStatus, blocks?: TurnOutputBlock[]): Promise<void>;
48
49
  /** Set status without changing text (e.g. switching to "tool"). */
49
50
  setStatus(status: StreamingStatus): Promise<void>;
50
51
  /** Remove the streaming node (call after final Firestore message is written). */
package/dist/streaming.js CHANGED
@@ -32,12 +32,15 @@ export function createStreamingHelper(opts) {
32
32
  await nodeRef.set(node);
33
33
  },
34
34
  /** Update the accumulated text and optionally change status. */
35
- async update(text, status = 'streaming') {
36
- await nodeRef.update({
35
+ async update(text, status = 'streaming', blocks) {
36
+ const update = {
37
37
  text,
38
38
  status,
39
39
  updatedAt: serverTimestamp,
40
- });
40
+ };
41
+ if (blocks)
42
+ update.blocks = blocks;
43
+ await nodeRef.update(update);
41
44
  },
42
45
  /** Set status without changing text (e.g. switching to "tool"). */
43
46
  async setStatus(status) {
@@ -1,10 +1,25 @@
1
- import type { StreamingStatus } from './types.js';
1
+ import type { StreamingStatus, TurnOutputBlock, TurnOutputBlockKind, TurnOutputBlockStatus } from './types.js';
2
2
  export type TurnOutputMode = 'delta' | 'block' | 'snapshot' | 'status';
3
3
  export interface TurnOutputSnapshot {
4
4
  turnId: string;
5
5
  messageId: string;
6
6
  text: string;
7
7
  status: StreamingStatus;
8
+ blocks?: TurnOutputBlock[];
9
+ }
10
+ export interface TurnOutputBlockInput {
11
+ id: string;
12
+ kind: TurnOutputBlockKind;
13
+ status?: TurnOutputBlockStatus;
14
+ sequence?: number;
15
+ title?: string;
16
+ text?: string;
17
+ summary?: string;
18
+ detail?: string;
19
+ }
20
+ export interface BuildBoundedTurnTrailOptions {
21
+ maxBlocks?: number;
22
+ maxMetadataBytes?: number;
8
23
  }
9
24
  export interface TurnOutputControllerOptions {
10
25
  turnId: string;
@@ -21,15 +36,24 @@ export interface TurnOutputController {
21
36
  readonly mode: TurnOutputMode;
22
37
  getText(): string;
23
38
  getStatus(): StreamingStatus;
39
+ getBlocks(): TurnOutputBlock[];
40
+ getFinalTrail(): TurnOutputBlock[];
24
41
  startThinking(text?: string): Promise<void>;
25
42
  startStreaming(text?: string): Promise<void>;
26
43
  appendDelta(delta: string): void;
27
44
  appendBlock(block: string): void;
28
45
  replaceSnapshot(text: string, status?: StreamingStatus): Promise<void>;
29
46
  setStatus(status: StreamingStatus, text?: string): Promise<void>;
47
+ addBlock(block: TurnOutputBlockInput): Promise<TurnOutputBlock>;
48
+ updateBlock(id: string, patch: Partial<Omit<TurnOutputBlockInput, 'id' | 'sequence'>>): Promise<TurnOutputBlock | null>;
49
+ completeBlock(id: string, patch?: Partial<Omit<TurnOutputBlockInput, 'id' | 'sequence' | 'status'>>): Promise<TurnOutputBlock | null>;
50
+ failBlock(id: string, patch?: Partial<Omit<TurnOutputBlockInput, 'id' | 'sequence' | 'status'>>): Promise<TurnOutputBlock | null>;
30
51
  flush(status?: StreamingStatus): Promise<void>;
31
52
  waitingInput(): Promise<void>;
32
53
  interrupt(): Promise<void>;
33
54
  clear(): Promise<void>;
34
55
  }
56
+ export declare const DEFAULT_TURN_TRAIL_MAX_BLOCKS = 20;
57
+ export declare const DEFAULT_TURN_TRAIL_METADATA_BUDGET_BYTES = 2500;
58
+ export declare function buildBoundedTurnTrail(blocks: readonly TurnOutputBlock[], options?: BuildBoundedTurnTrailOptions): TurnOutputBlock[];
35
59
  export declare function createTurnOutputController(options: TurnOutputControllerOptions): TurnOutputController;
@@ -1,7 +1,12 @@
1
1
  const DEFAULT_THROTTLE_MS = 120;
2
2
  const DEFAULT_BLOCK_SEPARATOR = '\n\n';
3
+ export const DEFAULT_TURN_TRAIL_MAX_BLOCKS = 20;
4
+ export const DEFAULT_TURN_TRAIL_METADATA_BUDGET_BYTES = 2_500;
3
5
  function normalizeStatus(status) {
4
- return status === 'thinking' || status === 'tool' || status === 'streaming'
6
+ return status === 'thinking'
7
+ || status === 'tool'
8
+ || status === 'streaming'
9
+ || status === 'waiting_input'
5
10
  ? status
6
11
  : 'streaming';
7
12
  }
@@ -12,6 +17,46 @@ function appendBlockText(currentText, block, separator) {
12
17
  const combined = `${currentText}${prefix}${block}`;
13
18
  return separator === '\n\n' ? combined.replace(/\n{3,}/g, '\n\n') : combined;
14
19
  }
20
+ function trim(value, max) {
21
+ if (!value)
22
+ return undefined;
23
+ return value.length > max ? `${value.slice(0, max - 3)}...` : value;
24
+ }
25
+ function jsonByteLength(value) {
26
+ const serialized = JSON.stringify(value);
27
+ if (typeof TextEncoder !== 'undefined') {
28
+ return new TextEncoder().encode(serialized).length;
29
+ }
30
+ return serialized.length;
31
+ }
32
+ function compactTrailBlock(block) {
33
+ return {
34
+ id: trim(block.id, 120) ?? block.id,
35
+ turnId: trim(block.turnId, 120) ?? block.turnId,
36
+ kind: block.kind,
37
+ status: block.status,
38
+ sequence: block.sequence,
39
+ ...(trim(block.title, 80) ? { title: trim(block.title, 80) } : {}),
40
+ ...(trim(block.text, 280) ? { text: trim(block.text, 280) } : {}),
41
+ ...(trim(block.summary, 120) ? { summary: trim(block.summary, 120) } : {}),
42
+ };
43
+ }
44
+ export function buildBoundedTurnTrail(blocks, options = {}) {
45
+ const maxBlocks = options.maxBlocks ?? DEFAULT_TURN_TRAIL_MAX_BLOCKS;
46
+ const maxMetadataBytes = options.maxMetadataBytes ?? DEFAULT_TURN_TRAIL_METADATA_BUDGET_BYTES;
47
+ const out = [];
48
+ for (const block of [...blocks].sort((left, right) => left.sequence - right.sequence)) {
49
+ if (out.length >= maxBlocks)
50
+ break;
51
+ const candidate = compactTrailBlock(block);
52
+ const next = [...out, candidate];
53
+ if (jsonByteLength({ turnTrail: next }) > maxMetadataBytes) {
54
+ break;
55
+ }
56
+ out.push(candidate);
57
+ }
58
+ return out;
59
+ }
15
60
  export function createTurnOutputController(options) {
16
61
  const schedule = options.schedule ?? setTimeout;
17
62
  const cancel = options.cancel ?? clearTimeout;
@@ -20,6 +65,8 @@ export function createTurnOutputController(options) {
20
65
  const blockSeparator = options.blockSeparator ?? DEFAULT_BLOCK_SEPARATOR;
21
66
  let text = '';
22
67
  let status = 'thinking';
68
+ let blocks = [];
69
+ let nextSequence = 1;
23
70
  let timer = null;
24
71
  let pendingPromise = Promise.resolve();
25
72
  const write = (nextStatus = status) => {
@@ -30,6 +77,7 @@ export function createTurnOutputController(options) {
30
77
  messageId: options.turnId,
31
78
  text,
32
79
  status: normalizeStatus(nextStatus),
80
+ ...(blocks.length > 0 ? { blocks: [...blocks] } : {}),
33
81
  };
34
82
  pendingPromise = pendingPromise
35
83
  .catch(() => { })
@@ -64,11 +112,60 @@ export function createTurnOutputController(options) {
64
112
  cancelTimer();
65
113
  text = '';
66
114
  status = 'thinking';
115
+ blocks = [];
116
+ nextSequence = 1;
67
117
  pendingPromise = pendingPromise
68
118
  .catch(() => { })
69
119
  .then(() => Promise.resolve(options.clearSnapshot()).catch(() => { }));
70
120
  await pendingPromise;
71
121
  };
122
+ const normalizeBlock = (block, existing) => {
123
+ const now = Date.now();
124
+ const sequence = existing?.sequence ?? block.sequence ?? nextSequence++;
125
+ if (sequence >= nextSequence)
126
+ nextSequence = sequence + 1;
127
+ return {
128
+ id: block.id,
129
+ turnId: options.turnId,
130
+ kind: block.kind,
131
+ status: block.status ?? existing?.status ?? 'running',
132
+ sequence,
133
+ ...(block.title ?? existing?.title ? { title: block.title ?? existing?.title } : {}),
134
+ ...(block.text ?? existing?.text ? { text: block.text ?? existing?.text } : {}),
135
+ ...(block.summary ?? existing?.summary ? { summary: block.summary ?? existing?.summary } : {}),
136
+ ...(block.detail ?? existing?.detail ? { detail: block.detail ?? existing?.detail } : {}),
137
+ createdAt: existing?.createdAt ?? now,
138
+ updatedAt: now,
139
+ };
140
+ };
141
+ const upsertBlock = async (id, patch, fallback) => {
142
+ const index = blocks.findIndex((block) => block.id === id);
143
+ if (index < 0 && !fallback)
144
+ return null;
145
+ const existing = index >= 0 ? blocks[index] : null;
146
+ const next = normalizeBlock({
147
+ id,
148
+ kind: patch.kind ?? existing?.kind ?? fallback.kind,
149
+ status: patch.status ?? existing?.status,
150
+ title: patch.title ?? existing?.title,
151
+ text: patch.text ?? existing?.text,
152
+ summary: patch.summary ?? existing?.summary,
153
+ detail: patch.detail ?? existing?.detail,
154
+ sequence: existing?.sequence,
155
+ }, existing);
156
+ if (index >= 0) {
157
+ blocks = [
158
+ ...blocks.slice(0, index),
159
+ next,
160
+ ...blocks.slice(index + 1),
161
+ ];
162
+ }
163
+ else {
164
+ blocks = [...blocks, next].sort((left, right) => left.sequence - right.sequence);
165
+ }
166
+ await flush(status);
167
+ return next;
168
+ };
72
169
  return {
73
170
  turnId: options.turnId,
74
171
  mode,
@@ -78,6 +175,12 @@ export function createTurnOutputController(options) {
78
175
  getStatus() {
79
176
  return status;
80
177
  },
178
+ getBlocks() {
179
+ return [...blocks];
180
+ },
181
+ getFinalTrail() {
182
+ return buildBoundedTurnTrail(blocks);
183
+ },
81
184
  async startThinking(nextText = '') {
82
185
  text = nextText;
83
186
  status = 'thinking';
@@ -91,7 +194,7 @@ export function createTurnOutputController(options) {
91
194
  appendDelta(delta) {
92
195
  if (!delta)
93
196
  return;
94
- if (status !== 'streaming')
197
+ if (status === 'thinking' && (!text || text === 'Thinking...'))
95
198
  text = '';
96
199
  text += delta;
97
200
  status = 'streaming';
@@ -100,7 +203,7 @@ export function createTurnOutputController(options) {
100
203
  appendBlock(block) {
101
204
  if (!block)
102
205
  return;
103
- if (status !== 'streaming')
206
+ if (status === 'thinking' && (!text || text === 'Thinking...'))
104
207
  text = '';
105
208
  text = appendBlockText(text, block, blockSeparator);
106
209
  status = 'streaming';
@@ -117,8 +220,23 @@ export function createTurnOutputController(options) {
117
220
  status = normalizeStatus(nextStatus);
118
221
  await flush(status);
119
222
  },
223
+ async addBlock(block) {
224
+ return upsertBlock(block.id, block, { kind: block.kind });
225
+ },
226
+ async updateBlock(id, patch) {
227
+ return upsertBlock(id, patch);
228
+ },
229
+ async completeBlock(id, patch) {
230
+ return upsertBlock(id, { ...(patch ?? {}), status: 'completed' });
231
+ },
232
+ async failBlock(id, patch) {
233
+ return upsertBlock(id, { ...(patch ?? {}), status: 'failed' });
234
+ },
120
235
  flush,
121
- waitingInput: clear,
236
+ async waitingInput() {
237
+ status = 'waiting_input';
238
+ await flush('waiting_input');
239
+ },
122
240
  interrupt: clear,
123
241
  clear,
124
242
  };
@@ -30,7 +30,6 @@ export interface TurnMetadata {
30
30
  turnId?: string | null;
31
31
  turnSemantics?: TurnMessageSemantics;
32
32
  deliveryIntent?: DeliveryIntent;
33
- turnComplete?: boolean;
34
33
  replyBehavior?: 'allow_auto_reply' | 'suppress_auto_reply';
35
34
  inboundDisposition?: InboundDisposition;
36
35
  }
@@ -66,15 +65,12 @@ export declare function isTurnOpen(turnState: Pick<TurnState, 'state' | 'turnUpd
66
65
  export declare function resolveTurnMessageSemantics(input: {
67
66
  senderType: 'human' | 'ai_agent';
68
67
  metadata?: unknown;
69
- senderTurnState?: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null;
70
68
  }): TurnMessageSemantics;
71
69
  export declare function shouldPromoteConversationMessage(input: {
72
70
  senderType: 'human' | 'ai_agent';
73
71
  metadata?: unknown;
74
- senderTurnState?: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null;
75
72
  }): boolean;
76
73
  export declare function shouldTriggerAgentTurn(input: {
77
74
  senderType: 'human' | 'ai_agent';
78
75
  metadata?: unknown;
79
- senderTurnState?: Pick<TurnState, 'state' | 'turnUpdatedAt' | 'updatedAt' | 'openedAt'> | null;
80
76
  }): TriggerDecision;
@@ -79,15 +79,13 @@ export function normalizeTurnMetadata(metadata) {
79
79
  || metadata.inboundDisposition === 'cancelled'
80
80
  ? metadata.inboundDisposition
81
81
  : undefined;
82
- if (!turnId && !turnSemantics && !deliveryIntent && typeof metadata.turnComplete !== 'boolean'
83
- && !replyBehavior && !inboundDisposition) {
82
+ if (!turnId && !turnSemantics && !deliveryIntent && !replyBehavior && !inboundDisposition) {
84
83
  return null;
85
84
  }
86
85
  return {
87
86
  ...(turnId ? { turnId } : {}),
88
87
  ...(turnSemantics ? { turnSemantics } : {}),
89
88
  ...(deliveryIntent ? { deliveryIntent } : {}),
90
- ...(typeof metadata.turnComplete === 'boolean' ? { turnComplete: metadata.turnComplete } : {}),
91
89
  ...(replyBehavior ? { replyBehavior } : {}),
92
90
  ...(inboundDisposition ? { inboundDisposition } : {}),
93
91
  };
@@ -163,13 +161,10 @@ export function resolveTurnMessageSemantics(input) {
163
161
  if (turnMetadata?.turnSemantics) {
164
162
  return turnMetadata.turnSemantics;
165
163
  }
166
- if (turnMetadata?.turnComplete === true) {
167
- return 'turn_complete';
168
- }
169
164
  if (input.senderType === 'human') {
170
165
  return 'turn_complete';
171
166
  }
172
- return isTurnOpen(input.senderTurnState) ? 'progress' : 'turn_complete';
167
+ return 'progress';
173
168
  }
174
169
  export function shouldPromoteConversationMessage(input) {
175
170
  if (isHiddenRuntimeCardMetadata(input.metadata)) {
@@ -198,7 +193,9 @@ export function shouldTriggerAgentTurn(input) {
198
193
  return {
199
194
  allow: false,
200
195
  semantics,
201
- reason: 'non-final agent progress does not trigger other agents',
196
+ reason: turnMetadata?.turnSemantics === 'progress'
197
+ ? 'non-final agent progress does not trigger other agents'
198
+ : 'agent messages require explicit turn_complete semantics',
202
199
  };
203
200
  }
204
201
  return {
package/dist/types.d.ts CHANGED
@@ -69,8 +69,10 @@ export interface CanonConversation {
69
69
  hasUnread?: boolean;
70
70
  lastMessage: {
71
71
  text: string;
72
+ messageId: string;
72
73
  senderId: string;
73
- senderType?: 'human' | 'ai_agent';
74
+ senderType: 'human' | 'ai_agent';
75
+ contentType: CanonMessage['contentType'];
74
76
  timestamp: string;
75
77
  } | null;
76
78
  createdAt: string;
@@ -106,9 +108,9 @@ export interface CanonContactRequest {
106
108
  requesterName: string;
107
109
  requesterAvatarUrl: string | null;
108
110
  targetId: string;
109
- targetName?: string | null;
110
- targetAvatarUrl?: string | null;
111
- targetUserType?: 'human' | 'ai_agent' | null;
111
+ targetName: string;
112
+ targetAvatarUrl: string | null;
113
+ targetUserType: 'human' | 'ai_agent';
112
114
  targetOwnerId?: string | null;
113
115
  approverId: string;
114
116
  message: string | null;
@@ -342,7 +344,6 @@ export interface CanonRuntimeDescriptor {
342
344
  coreControls: ReadonlyArray<CanonControlDescriptor>;
343
345
  runtimeControls?: ReadonlyArray<CanonControlDescriptor>;
344
346
  commands?: ReadonlyArray<CanonRuntimeCommandDescriptor>;
345
- actions?: ReadonlyArray<CanonRuntimeActionDescriptor>;
346
347
  /**
347
348
  * Optional setup-time local roots advertised by a runtime. These are
348
349
  * metadata only for now; existing session config still selects concrete
@@ -424,6 +425,21 @@ export interface CanonRuntimeActivityItem {
424
425
  sensitive?: boolean;
425
426
  actions?: ReadonlyArray<CanonRuntimeCommandDescriptor>;
426
427
  }
428
+ export type TurnOutputBlockKind = 'text' | 'tool' | 'plan' | 'approval' | 'input' | 'status';
429
+ export type TurnOutputBlockStatus = 'running' | 'completed' | 'failed' | 'pending' | 'blocked';
430
+ export interface TurnOutputBlock {
431
+ id: string;
432
+ turnId: string;
433
+ kind: TurnOutputBlockKind;
434
+ status: TurnOutputBlockStatus;
435
+ sequence: number;
436
+ title?: string;
437
+ text?: string;
438
+ summary?: string;
439
+ detail?: string;
440
+ createdAt?: number;
441
+ updatedAt?: number;
442
+ }
427
443
  export interface CanonRuntimeInventoryEntry {
428
444
  id: string;
429
445
  label: string;
@@ -607,13 +623,14 @@ export interface CreateConversationOptions {
607
623
  /** Required when creating a direct conversation with a first-party coding agent. */
608
624
  sessionConfig?: SessionConfig | null;
609
625
  }
610
- export type StreamingStatus = 'thinking' | 'streaming' | 'tool';
626
+ export type StreamingStatus = 'thinking' | 'streaming' | 'tool' | 'waiting_input';
611
627
  export interface SetStreamingOptions {
612
628
  conversationId: string;
613
629
  text: string;
614
630
  status: StreamingStatus;
615
631
  messageId: string;
616
632
  turnId?: string | null;
633
+ blocks?: TurnOutputBlock[];
617
634
  }
618
635
  export interface SetRuntimeTurnOptions {
619
636
  conversationId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.24.0",
3
+ "version": "1.0.0",
4
4
  "description": "Canon core — shared types, REST client, SSE stream, and registration for Canon messaging",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",