@canonmsg/core 0.23.0 → 0.24.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/index.d.ts CHANGED
@@ -24,6 +24,8 @@ export { buildRuntimeInputOutcome, buildRuntimeInputReply, buildRuntimeInputRequ
24
24
  export type { ClaudeQuestionMetadata, ClaudeQuestionReplyMetadata, PlanApprovalMetadata, PlanApprovalReplyMetadata, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputOutcomeMetadata, 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
29
  export { clearPendingRegistration, getOrCreatePendingRegistration, loadPendingRegistrations, loadProfiles, savePendingRegistrations, saveProfiles, updatePendingRegistration, upsertAgentProfile, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
28
30
  export type { AgentProfile, PendingRegistration, ProfileLockHandle } from './agent-profiles.js';
29
31
  export { resolveCanonAgent, resolveCanonProfile, getActiveProfile, getActiveProfileLock } from './agent-resolver.js';
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ export { DEFAULT_APPROVAL_CONFIG, parseApprovalRequestMetadata, parseApprovalRep
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
26
  // Agent profiles (loading, locking, resolution)
26
27
  export { clearPendingRegistration, getOrCreatePendingRegistration, loadPendingRegistrations, loadProfiles, savePendingRegistrations, saveProfiles, updatePendingRegistration, upsertAgentProfile, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
27
28
  // Agent resolver
@@ -10,6 +10,7 @@ export interface RuntimeStreamingPayload {
10
10
  text: string;
11
11
  status: 'thinking' | 'streaming' | 'tool';
12
12
  messageId?: string;
13
+ turnId?: string | null;
13
14
  updatedAt?: number | {
14
15
  '.sv': 'timestamp';
15
16
  };
@@ -23,6 +23,7 @@ export interface StreamingNode {
23
23
  startedAt: unknown;
24
24
  updatedAt: unknown;
25
25
  messageId: string;
26
+ turnId?: string | null;
26
27
  }
27
28
  /**
28
29
  * Creates helpers that read/write the RTDB streaming node for a single
@@ -41,7 +42,7 @@ export interface StreamingNode {
41
42
  export declare function createStreamingHelper(opts: StreamingHelperOptions): {
42
43
  forConversation: (conversationId: string, agentId: string) => {
43
44
  /** Write the initial streaming node (status: "thinking", empty text). */
44
- start(messageId: string): Promise<void>;
45
+ start(messageId: string, turnId?: string | null): Promise<void>;
45
46
  /** Update the accumulated text and optionally change status. */
46
47
  update(text: string, status?: StreamingStatus): Promise<void>;
47
48
  /** Set status without changing text (e.g. switching to "tool"). */
package/dist/streaming.js CHANGED
@@ -20,13 +20,14 @@ export function createStreamingHelper(opts) {
20
20
  const nodeRef = db.ref(nodePath);
21
21
  return {
22
22
  /** Write the initial streaming node (status: "thinking", empty text). */
23
- async start(messageId) {
23
+ async start(messageId, turnId) {
24
24
  const node = {
25
25
  text: '',
26
26
  status: 'thinking',
27
27
  startedAt: serverTimestamp,
28
28
  updatedAt: serverTimestamp,
29
29
  messageId,
30
+ ...(turnId ? { turnId } : {}),
30
31
  };
31
32
  await nodeRef.set(node);
32
33
  },
@@ -0,0 +1,35 @@
1
+ import type { StreamingStatus } from './types.js';
2
+ export type TurnOutputMode = 'delta' | 'block' | 'snapshot' | 'status';
3
+ export interface TurnOutputSnapshot {
4
+ turnId: string;
5
+ messageId: string;
6
+ text: string;
7
+ status: StreamingStatus;
8
+ }
9
+ export interface TurnOutputControllerOptions {
10
+ turnId: string;
11
+ mode?: TurnOutputMode;
12
+ throttleMs?: number;
13
+ blockSeparator?: string;
14
+ writeSnapshot: (snapshot: TurnOutputSnapshot) => void | Promise<void>;
15
+ clearSnapshot: () => void | Promise<void>;
16
+ schedule?: typeof setTimeout;
17
+ cancel?: typeof clearTimeout;
18
+ }
19
+ export interface TurnOutputController {
20
+ readonly turnId: string;
21
+ readonly mode: TurnOutputMode;
22
+ getText(): string;
23
+ getStatus(): StreamingStatus;
24
+ startThinking(text?: string): Promise<void>;
25
+ startStreaming(text?: string): Promise<void>;
26
+ appendDelta(delta: string): void;
27
+ appendBlock(block: string): void;
28
+ replaceSnapshot(text: string, status?: StreamingStatus): Promise<void>;
29
+ setStatus(status: StreamingStatus, text?: string): Promise<void>;
30
+ flush(status?: StreamingStatus): Promise<void>;
31
+ waitingInput(): Promise<void>;
32
+ interrupt(): Promise<void>;
33
+ clear(): Promise<void>;
34
+ }
35
+ export declare function createTurnOutputController(options: TurnOutputControllerOptions): TurnOutputController;
@@ -0,0 +1,125 @@
1
+ const DEFAULT_THROTTLE_MS = 120;
2
+ const DEFAULT_BLOCK_SEPARATOR = '\n\n';
3
+ function normalizeStatus(status) {
4
+ return status === 'thinking' || status === 'tool' || status === 'streaming'
5
+ ? status
6
+ : 'streaming';
7
+ }
8
+ function appendBlockText(currentText, block, separator) {
9
+ if (!block)
10
+ return currentText;
11
+ const prefix = currentText ? separator : '';
12
+ const combined = `${currentText}${prefix}${block}`;
13
+ return separator === '\n\n' ? combined.replace(/\n{3,}/g, '\n\n') : combined;
14
+ }
15
+ export function createTurnOutputController(options) {
16
+ const schedule = options.schedule ?? setTimeout;
17
+ const cancel = options.cancel ?? clearTimeout;
18
+ const throttleMs = Math.max(0, options.throttleMs ?? DEFAULT_THROTTLE_MS);
19
+ const mode = options.mode ?? 'snapshot';
20
+ const blockSeparator = options.blockSeparator ?? DEFAULT_BLOCK_SEPARATOR;
21
+ let text = '';
22
+ let status = 'thinking';
23
+ let timer = null;
24
+ let pendingPromise = Promise.resolve();
25
+ const write = (nextStatus = status) => {
26
+ if (mode === 'status')
27
+ return;
28
+ const snapshot = {
29
+ turnId: options.turnId,
30
+ messageId: options.turnId,
31
+ text,
32
+ status: normalizeStatus(nextStatus),
33
+ };
34
+ pendingPromise = pendingPromise
35
+ .catch(() => { })
36
+ .then(() => Promise.resolve(options.writeSnapshot(snapshot)).catch(() => { }));
37
+ };
38
+ const cancelTimer = () => {
39
+ if (!timer)
40
+ return;
41
+ cancel(timer);
42
+ timer = null;
43
+ };
44
+ const scheduleWrite = () => {
45
+ if (mode === 'status')
46
+ return;
47
+ if (throttleMs <= 0) {
48
+ write();
49
+ return;
50
+ }
51
+ if (timer)
52
+ return;
53
+ timer = schedule(() => {
54
+ timer = null;
55
+ write();
56
+ }, throttleMs);
57
+ };
58
+ const flush = async (nextStatus = status) => {
59
+ cancelTimer();
60
+ write(nextStatus);
61
+ await pendingPromise;
62
+ };
63
+ const clear = async () => {
64
+ cancelTimer();
65
+ text = '';
66
+ status = 'thinking';
67
+ pendingPromise = pendingPromise
68
+ .catch(() => { })
69
+ .then(() => Promise.resolve(options.clearSnapshot()).catch(() => { }));
70
+ await pendingPromise;
71
+ };
72
+ return {
73
+ turnId: options.turnId,
74
+ mode,
75
+ getText() {
76
+ return text;
77
+ },
78
+ getStatus() {
79
+ return status;
80
+ },
81
+ async startThinking(nextText = '') {
82
+ text = nextText;
83
+ status = 'thinking';
84
+ await flush('thinking');
85
+ },
86
+ async startStreaming(nextText = text) {
87
+ text = nextText;
88
+ status = 'streaming';
89
+ await flush('streaming');
90
+ },
91
+ appendDelta(delta) {
92
+ if (!delta)
93
+ return;
94
+ if (status !== 'streaming')
95
+ text = '';
96
+ text += delta;
97
+ status = 'streaming';
98
+ scheduleWrite();
99
+ },
100
+ appendBlock(block) {
101
+ if (!block)
102
+ return;
103
+ if (status !== 'streaming')
104
+ text = '';
105
+ text = appendBlockText(text, block, blockSeparator);
106
+ status = 'streaming';
107
+ scheduleWrite();
108
+ },
109
+ async replaceSnapshot(nextText, nextStatus = 'streaming') {
110
+ text = nextText;
111
+ status = normalizeStatus(nextStatus);
112
+ await flush(status);
113
+ },
114
+ async setStatus(nextStatus, nextText) {
115
+ if (nextText !== undefined)
116
+ text = nextText;
117
+ status = normalizeStatus(nextStatus);
118
+ await flush(status);
119
+ },
120
+ flush,
121
+ waitingInput: clear,
122
+ interrupt: clear,
123
+ clear,
124
+ };
125
+ }
package/dist/types.d.ts CHANGED
@@ -613,6 +613,7 @@ export interface SetStreamingOptions {
613
613
  text: string;
614
614
  status: StreamingStatus;
615
615
  messageId: string;
616
+ turnId?: string | null;
616
617
  }
617
618
  export interface SetRuntimeTurnOptions {
618
619
  conversationId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.23.0",
3
+ "version": "0.24.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",