@canonmsg/agent-sdk 5.0.0 → 5.1.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/canon-agent.d.ts +19 -0
- package/dist/canon-agent.js +71 -131
- package/package.json +2 -2
package/dist/canon-agent.d.ts
CHANGED
|
@@ -53,6 +53,15 @@ export declare class CanonAgent {
|
|
|
53
53
|
private approvalManager;
|
|
54
54
|
private approvalManagerAgentId;
|
|
55
55
|
private approvalManagerOwnerId;
|
|
56
|
+
/**
|
|
57
|
+
* Shared poll/timeout/abort engine for interactive runtime input + card
|
|
58
|
+
* requests. The server request is still created inline (host-specific
|
|
59
|
+
* `native`/`responseUserId`), so the descriptors register a no-op `create`
|
|
60
|
+
* but keep the built-in `cancel`: passing the turn's abort `signal` lets the
|
|
61
|
+
* manager fire `consume({ cancel: true })` on interrupt, so the hosts no
|
|
62
|
+
* longer hand-roll a `cancelPendingRequest`.
|
|
63
|
+
*/
|
|
64
|
+
private runtimeRequestManager;
|
|
56
65
|
private cachedConversationIds;
|
|
57
66
|
private running;
|
|
58
67
|
private runtimeHeartbeatTimer;
|
|
@@ -66,6 +75,16 @@ export declare class CanonAgent {
|
|
|
66
75
|
private sseConnectedLogged;
|
|
67
76
|
constructor(options: CanonAgentOptions);
|
|
68
77
|
private ensureApprovalManager;
|
|
78
|
+
/**
|
|
79
|
+
* Shared engine for interactive runtime input + card polling. Unlike approval,
|
|
80
|
+
* these families do not need an owner (the request is created inline with the
|
|
81
|
+
* caller's own `responseUserId` policy), so the manager is always available.
|
|
82
|
+
* The registered descriptors keep the built-in poll/consume/timeout logic and
|
|
83
|
+
* the built-in `cancel` (best-effort `consume({ cancel: true })`), replacing
|
|
84
|
+
* only `create` with a no-op because the request is already created inline.
|
|
85
|
+
* On abort the manager runs that cancel via the passed `signal`.
|
|
86
|
+
*/
|
|
87
|
+
private ensureRuntimeRequestManager;
|
|
69
88
|
private filterApprovalReplyMessages;
|
|
70
89
|
on(event: 'message', handler: MessageHandler): void;
|
|
71
90
|
on(event: 'messageUpdated', handler: MessageUpdatedHandler): void;
|
package/dist/canon-agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApprovalManager, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, renderCanonHostInboundContent, sendMessageWithRetry, } from '@canonmsg/core';
|
|
1
|
+
import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, renderCanonHostInboundContent, sendMessageWithRetry, } from '@canonmsg/core';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { AuthManager } from './auth.js';
|
|
4
4
|
import { Debouncer } from './debouncer.js';
|
|
@@ -10,7 +10,6 @@ const RUNTIME_CONTROL_POLL_INTERVAL_MS = 2_000;
|
|
|
10
10
|
const RUNTIME_PRIMITIVE_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
11
11
|
const RUNTIME_PRIMITIVE_DEDUPE_MAX = 1_000;
|
|
12
12
|
const DEFAULT_RUNTIME_INPUT_TIMEOUT_MS = 5 * 60_000;
|
|
13
|
-
const RUNTIME_INPUT_POLL_MS = 1_000;
|
|
14
13
|
const RUNTIME_INPUT_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,160}$/;
|
|
15
14
|
const SDK_MESSAGE_ID_READABLE_MAX = 120;
|
|
16
15
|
const SDK_RUNTIME_CAPABILITIES = {
|
|
@@ -268,6 +267,15 @@ export class CanonAgent {
|
|
|
268
267
|
approvalManager = null;
|
|
269
268
|
approvalManagerAgentId = null;
|
|
270
269
|
approvalManagerOwnerId = null;
|
|
270
|
+
/**
|
|
271
|
+
* Shared poll/timeout/abort engine for interactive runtime input + card
|
|
272
|
+
* requests. The server request is still created inline (host-specific
|
|
273
|
+
* `native`/`responseUserId`), so the descriptors register a no-op `create`
|
|
274
|
+
* but keep the built-in `cancel`: passing the turn's abort `signal` lets the
|
|
275
|
+
* manager fire `consume({ cancel: true })` on interrupt, so the hosts no
|
|
276
|
+
* longer hand-roll a `cancelPendingRequest`.
|
|
277
|
+
*/
|
|
278
|
+
runtimeRequestManager = null;
|
|
271
279
|
cachedConversationIds = [];
|
|
272
280
|
running = false;
|
|
273
281
|
runtimeHeartbeatTimer = null;
|
|
@@ -354,6 +362,33 @@ export class CanonAgent {
|
|
|
354
362
|
this.approvalManagerOwnerId = ownerId;
|
|
355
363
|
return this.approvalManager;
|
|
356
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Shared engine for interactive runtime input + card polling. Unlike approval,
|
|
367
|
+
* these families do not need an owner (the request is created inline with the
|
|
368
|
+
* caller's own `responseUserId` policy), so the manager is always available.
|
|
369
|
+
* The registered descriptors keep the built-in poll/consume/timeout logic and
|
|
370
|
+
* the built-in `cancel` (best-effort `consume({ cancel: true })`), replacing
|
|
371
|
+
* only `create` with a no-op because the request is already created inline.
|
|
372
|
+
* On abort the manager runs that cancel via the passed `signal`.
|
|
373
|
+
*/
|
|
374
|
+
ensureRuntimeRequestManager() {
|
|
375
|
+
if (!this.runtimeRequestManager) {
|
|
376
|
+
const manager = new RuntimeRequestManager(this.apiClient, {
|
|
377
|
+
agentId: this.agentId ?? '',
|
|
378
|
+
ownerId: '',
|
|
379
|
+
});
|
|
380
|
+
manager.register('input', {
|
|
381
|
+
...runtimeInputDescriptor,
|
|
382
|
+
create: async ({ requestId }) => ({ requestId }),
|
|
383
|
+
});
|
|
384
|
+
manager.register('card', {
|
|
385
|
+
...runtimeCardDescriptor,
|
|
386
|
+
create: async ({ requestId }) => ({ requestId }),
|
|
387
|
+
});
|
|
388
|
+
this.runtimeRequestManager = manager;
|
|
389
|
+
}
|
|
390
|
+
return this.runtimeRequestManager;
|
|
391
|
+
}
|
|
357
392
|
filterApprovalReplyMessages(conversationId, messages) {
|
|
358
393
|
const manager = this.ensureApprovalManager();
|
|
359
394
|
if (!manager) {
|
|
@@ -1481,20 +1516,6 @@ export class CanonAgent {
|
|
|
1481
1516
|
const expiresAt = new Date(expiresAtMs).toISOString();
|
|
1482
1517
|
let result = { status: 'timeout', inputId };
|
|
1483
1518
|
let requestCreated = false;
|
|
1484
|
-
let requestResolved = false;
|
|
1485
|
-
const cancelPendingRequest = async () => {
|
|
1486
|
-
if (!requestCreated || requestResolved)
|
|
1487
|
-
return;
|
|
1488
|
-
try {
|
|
1489
|
-
await this.apiClient.consumeRuntimeInputResponse({
|
|
1490
|
-
conversationId,
|
|
1491
|
-
inputId,
|
|
1492
|
-
cancel: true,
|
|
1493
|
-
});
|
|
1494
|
-
requestResolved = true;
|
|
1495
|
-
}
|
|
1496
|
-
catch { }
|
|
1497
|
-
};
|
|
1498
1519
|
shouldPersistTurnState = true;
|
|
1499
1520
|
try {
|
|
1500
1521
|
await this.apiClient.createRuntimeInputRequest({
|
|
@@ -1529,56 +1550,15 @@ export class CanonAgent {
|
|
|
1529
1550
|
await this.typingSignals.clear(conversationId);
|
|
1530
1551
|
}
|
|
1531
1552
|
catch { }
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
result = {
|
|
1542
|
-
status: 'submitted',
|
|
1543
|
-
value: response.value,
|
|
1544
|
-
answers: response.answers,
|
|
1545
|
-
inputId,
|
|
1546
|
-
};
|
|
1547
|
-
break;
|
|
1548
|
-
}
|
|
1549
|
-
if (response.status === 'cancelled' || response.status === 'timeout') {
|
|
1550
|
-
requestResolved = true;
|
|
1551
|
-
result = { status: response.status, inputId };
|
|
1552
|
-
break;
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
catch {
|
|
1556
|
-
// Transient consume failures should not leak sensitive input or
|
|
1557
|
-
// break the runtime; keep waiting until the explicit timeout.
|
|
1558
|
-
}
|
|
1559
|
-
await sleepWithAbort(Math.min(RUNTIME_INPUT_POLL_MS, Math.max(1, expiresAtMs - Date.now())), abortController.signal);
|
|
1560
|
-
}
|
|
1561
|
-
if (!requestResolved) {
|
|
1562
|
-
try {
|
|
1563
|
-
const response = await this.apiClient.consumeRuntimeInputResponse({
|
|
1564
|
-
conversationId,
|
|
1565
|
-
inputId,
|
|
1566
|
-
});
|
|
1567
|
-
if (response.status === 'submitted') {
|
|
1568
|
-
result = {
|
|
1569
|
-
status: 'submitted',
|
|
1570
|
-
value: response.value,
|
|
1571
|
-
answers: response.answers,
|
|
1572
|
-
inputId,
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
else if (response.status === 'cancelled' || response.status === 'timeout') {
|
|
1576
|
-
result = { status: response.status, inputId };
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
catch { }
|
|
1580
|
-
requestResolved = true;
|
|
1581
|
-
}
|
|
1553
|
+
// The shared engine owns the poll-with-abort loop + post-deadline
|
|
1554
|
+
// consume; the request was already created above, so the descriptor's
|
|
1555
|
+
// create is a no-op. On abort the manager fires the descriptor's
|
|
1556
|
+
// built-in cancel (consume({ cancel: true })) via the passed signal.
|
|
1557
|
+
result = await this.ensureRuntimeRequestManager().request('input', conversationId, { kind: request.kind }, {
|
|
1558
|
+
requestId: inputId,
|
|
1559
|
+
expiresAt: expiresAtMs,
|
|
1560
|
+
signal: abortController.signal,
|
|
1561
|
+
});
|
|
1582
1562
|
const outcome = buildRuntimeInputOutcome(inputId, result.status, {
|
|
1583
1563
|
kind: request.kind,
|
|
1584
1564
|
reason: result.status,
|
|
@@ -1607,8 +1587,16 @@ export class CanonAgent {
|
|
|
1607
1587
|
return result;
|
|
1608
1588
|
}
|
|
1609
1589
|
catch (error) {
|
|
1610
|
-
await cancelPendingRequest();
|
|
1611
1590
|
if (abortController.signal.aborted || isAbortLikeError(error)) {
|
|
1591
|
+
if (requestCreated) {
|
|
1592
|
+
// Abort landing before the manager wired its cancel (e.g. during
|
|
1593
|
+
// the pre-request writeTurn/typing round-trips) leaves the
|
|
1594
|
+
// server-side request pending + responder-actionable; cancel it so
|
|
1595
|
+
// a late (possibly sensitive) answer can't resolve a dead request.
|
|
1596
|
+
await this.apiClient
|
|
1597
|
+
.consumeRuntimeInputResponse({ conversationId, inputId, cancel: true })
|
|
1598
|
+
.catch(() => { });
|
|
1599
|
+
}
|
|
1612
1600
|
throw error;
|
|
1613
1601
|
}
|
|
1614
1602
|
if (!requestCreated) {
|
|
@@ -1670,19 +1658,6 @@ export class CanonAgent {
|
|
|
1670
1658
|
let result = { status: 'timeout', cardId };
|
|
1671
1659
|
let requestCreated = false;
|
|
1672
1660
|
let requestResolved = false;
|
|
1673
|
-
const cancelPendingRequest = async () => {
|
|
1674
|
-
if (!requestCreated || requestResolved)
|
|
1675
|
-
return;
|
|
1676
|
-
try {
|
|
1677
|
-
await this.apiClient.consumeRuntimeCardResponse({
|
|
1678
|
-
conversationId,
|
|
1679
|
-
cardId,
|
|
1680
|
-
cancel: true,
|
|
1681
|
-
});
|
|
1682
|
-
requestResolved = true;
|
|
1683
|
-
}
|
|
1684
|
-
catch { }
|
|
1685
|
-
};
|
|
1686
1661
|
shouldPersistTurnState = true;
|
|
1687
1662
|
try {
|
|
1688
1663
|
await this.apiClient.createRuntimeCardRequest(buildRuntimeCardCreateArgs({ request, conversationId, cardId, fallbackTurnId: turnId, expiresAtMs }));
|
|
@@ -1703,56 +1678,16 @@ export class CanonAgent {
|
|
|
1703
1678
|
await this.typingSignals.clear(conversationId);
|
|
1704
1679
|
}
|
|
1705
1680
|
catch { }
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
status: 'submitted',
|
|
1717
|
-
cardId,
|
|
1718
|
-
...(response.actionId ? { actionId: response.actionId } : {}),
|
|
1719
|
-
...(response.values ? { values: response.values } : {}),
|
|
1720
|
-
};
|
|
1721
|
-
break;
|
|
1722
|
-
}
|
|
1723
|
-
if (response.status === 'cancelled' || response.status === 'timeout') {
|
|
1724
|
-
requestResolved = true;
|
|
1725
|
-
result = { status: response.status, cardId };
|
|
1726
|
-
break;
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
catch {
|
|
1730
|
-
// Keep waiting; the control path is the authoritative source and
|
|
1731
|
-
// transient consume failures should not leak response values.
|
|
1732
|
-
}
|
|
1733
|
-
await sleepWithAbort(Math.min(RUNTIME_INPUT_POLL_MS, Math.max(1, expiresAtMs - Date.now())), abortController.signal);
|
|
1734
|
-
}
|
|
1735
|
-
if (!requestResolved) {
|
|
1736
|
-
try {
|
|
1737
|
-
const response = await this.apiClient.consumeRuntimeCardResponse({
|
|
1738
|
-
conversationId,
|
|
1739
|
-
cardId,
|
|
1740
|
-
});
|
|
1741
|
-
if (response.status === 'submitted') {
|
|
1742
|
-
result = {
|
|
1743
|
-
status: 'submitted',
|
|
1744
|
-
cardId,
|
|
1745
|
-
...(response.actionId ? { actionId: response.actionId } : {}),
|
|
1746
|
-
...(response.values ? { values: response.values } : {}),
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
else if (response.status === 'cancelled' || response.status === 'timeout') {
|
|
1750
|
-
result = { status: response.status, cardId };
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
catch { }
|
|
1754
|
-
requestResolved = true;
|
|
1755
|
-
}
|
|
1681
|
+
// The shared engine owns the poll-with-abort loop + post-deadline
|
|
1682
|
+
// consume; the request was already created above, so the descriptor's
|
|
1683
|
+
// create is a no-op. On abort the manager fires the descriptor's
|
|
1684
|
+
// built-in cancel (consume({ cancel: true })) via the passed signal.
|
|
1685
|
+
result = await this.ensureRuntimeRequestManager().request('card', conversationId, { card: request.card }, {
|
|
1686
|
+
requestId: cardId,
|
|
1687
|
+
expiresAt: expiresAtMs,
|
|
1688
|
+
signal: abortController.signal,
|
|
1689
|
+
});
|
|
1690
|
+
requestResolved = true;
|
|
1756
1691
|
// The interactive path never yields 'displayed' (that returns early via
|
|
1757
1692
|
// sendCard); narrow for buildRuntimeCardOutcome's resolution status.
|
|
1758
1693
|
const resolutionStatus = result.status === 'displayed' ? 'timeout' : result.status;
|
|
@@ -1784,9 +1719,14 @@ export class CanonAgent {
|
|
|
1784
1719
|
}
|
|
1785
1720
|
catch (error) {
|
|
1786
1721
|
const shouldSendInterruptedOutcome = requestCreated && !requestResolved;
|
|
1787
|
-
await cancelPendingRequest();
|
|
1788
1722
|
if (abortController.signal.aborted || isAbortLikeError(error)) {
|
|
1789
1723
|
if (shouldSendInterruptedOutcome) {
|
|
1724
|
+
// Cancel the server-side card if the abort landed before the
|
|
1725
|
+
// manager wired its cancel, so a late action can't resolve a dead
|
|
1726
|
+
// request (mirrors the runtime-input path).
|
|
1727
|
+
await this.apiClient
|
|
1728
|
+
.consumeRuntimeCardResponse({ conversationId, cardId, cancel: true })
|
|
1729
|
+
.catch(() => { });
|
|
1790
1730
|
const outcome = buildRuntimeCardOutcome(cardId, 'cancelled', { reason: 'interrupted' });
|
|
1791
1731
|
await sendDurableMessage(outcome.text, {
|
|
1792
1732
|
metadata: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=18.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@canonmsg/core": "^4.
|
|
31
|
+
"@canonmsg/core": "^4.2.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|