agents 0.25.0 → 0.27.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/README.md +8 -9
- package/dist/{agent-routing-0DR0Tk2v.d.ts → agent-routing-Bhdbvf7s.d.ts} +13 -8
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +1 -1
- package/dist/{agent-tools-YrATZJu2.d.ts → agent-tools-bJZwBMfq.d.ts} +2 -2
- package/dist/agent-tools.d.ts +1 -1
- package/dist/ai-sdk-OoGkFy3s.js +2444 -0
- package/dist/ai-sdk-OoGkFy3s.js.map +1 -0
- package/dist/apply-chunks-1FheRb9W.js +99 -0
- package/dist/apply-chunks-1FheRb9W.js.map +1 -0
- package/dist/broadcast-state-CEyI4uAK.js +69 -0
- package/dist/broadcast-state-CEyI4uAK.js.map +1 -0
- package/dist/browser/ai-sdk.d.ts +1 -1
- package/dist/browser/ai-sdk.js +2 -2
- package/dist/browser/ai-sdk.js.map +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/tanstack-ai.d.ts +1 -1
- package/dist/browser/tanstack-ai.js +1 -1
- package/dist/{browser-DE3d-RLk.d.ts → browser-tmkcAAI3.d.ts} +2 -2
- package/dist/{browser-tool-DuNdlwZ9.d.ts → browser-tool-Bn-nzJQI.d.ts} +3 -3
- package/dist/{browser-tool-CQ10e6Kj.js → browser-tool-CBqs6hqa.js} +58 -18
- package/dist/browser-tool-CBqs6hqa.js.map +1 -0
- package/dist/capability-B4WbF81e.js.map +1 -1
- package/dist/{capability-runner-DBAnGr6L.d.ts → capability-runner-O5VbQzqg.d.ts} +9 -1
- package/dist/channel-Bak_6zDP.d.ts +333 -0
- package/dist/chat/index.d.ts +11 -9
- package/dist/chat/index.js +5 -194
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +2 -4
- package/dist/chat/react.js +3 -1
- package/dist/chat/react.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +1 -1
- package/dist/chat-sdk/index.js +1 -1
- package/dist/cli.js +144 -0
- package/dist/{client-fyQrLGkD.js → client-1tEQ1Oqz.js} +27 -2
- package/dist/{client-fyQrLGkD.js.map → client-1tEQ1Oqz.js.map} +1 -1
- package/dist/{client-je3koD9Z.d.ts → client-DS3Dz9l0.d.ts} +2 -2
- package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
- package/dist/client.d.ts +1 -1
- package/dist/conversations-DH0ut_8n.d.ts +192 -0
- package/dist/{current-agent-vAk_5JMt.d.ts → current-agent-C07C5o5Y.d.ts} +3 -3
- package/dist/daemon-core-Buubj-y1.js +481 -0
- package/dist/daemon-core-Buubj-y1.js.map +1 -0
- package/dist/daemon-core-CbohVPAu.d.ts +132 -0
- package/dist/{do-oauth-client-provider-Tmf1vgKz.d.ts → do-oauth-client-provider-DfsUcpc3.d.ts} +7 -1
- package/dist/{durable-object-lifecycle-HhgOMWq9.d.ts → durable-object-lifecycle-DRqp475_.d.ts} +2 -2
- package/dist/{channels/email.d.ts → experimental/channels/email/index.d.ts} +25 -12
- package/dist/{channels/email.js → experimental/channels/email/index.js} +13 -35
- package/dist/experimental/channels/email/index.js.map +1 -0
- package/dist/experimental/channels/index.d.ts +270 -0
- package/dist/experimental/channels/index.js +1186 -0
- package/dist/experimental/channels/index.js.map +1 -0
- package/dist/{channels/slack.d.ts → experimental/channels/slack/index.d.ts} +19 -9
- package/dist/experimental/channels/slack/index.js +267 -0
- package/dist/experimental/channels/slack/index.js.map +1 -0
- package/dist/{channels/telegram.d.ts → experimental/channels/telegram/index.d.ts} +19 -9
- package/dist/experimental/channels/telegram/index.js +198 -0
- package/dist/experimental/channels/telegram/index.js.map +1 -0
- package/dist/experimental/channels/web/ai-sdk.d.ts +55 -0
- package/dist/experimental/channels/web/ai-sdk.js +266 -0
- package/dist/experimental/channels/web/ai-sdk.js.map +1 -0
- package/dist/experimental/channels/web/client.d.ts +97 -0
- package/dist/experimental/channels/web/client.js +349 -0
- package/dist/experimental/channels/web/client.js.map +1 -0
- package/dist/experimental/channels/web/index.d.ts +189 -0
- package/dist/experimental/channels/web/index.js +332 -0
- package/dist/experimental/channels/web/index.js.map +1 -0
- package/dist/gateway-BDC07gYO.d.ts +100 -0
- package/dist/handle-zAzh0UW2.d.ts +460 -0
- package/dist/harness/ai-sdk/index.d.ts +175 -0
- package/dist/harness/ai-sdk/index.js +605 -0
- package/dist/harness/ai-sdk/index.js.map +1 -0
- package/dist/harness/container/daemon/index.d.ts +76 -0
- package/dist/harness/container/daemon/index.js +3 -0
- package/dist/harness/container/index.d.ts +740 -0
- package/dist/harness/container/index.js +2152 -0
- package/dist/harness/container/index.js.map +1 -0
- package/dist/harness/container/runtime/index.d.ts +144 -0
- package/dist/harness/container/runtime/index.js +1082 -0
- package/dist/harness/container/runtime/index.js.map +1 -0
- package/dist/harness/opencode/index.d.ts +369 -0
- package/dist/harness/opencode/index.js +1072 -0
- package/dist/harness/opencode/index.js.map +1 -0
- package/dist/{harnesses → harness}/pi/index.d.ts +85 -88
- package/dist/{harnesses → harness}/pi/index.js +196 -77
- package/dist/harness/pi/index.js.map +1 -0
- package/dist/harness/store/index.d.ts +24 -0
- package/dist/harness/store/index.js +2 -0
- package/dist/harness/think/index.d.ts +821 -0
- package/dist/harness/think/index.js +2529 -0
- package/dist/harness/think/index.js.map +1 -0
- package/dist/harness-B6B9S3rb.d.ts +203 -0
- package/dist/{images-iiTDSyRu.js → images-Ce9SCu3X.js} +2 -248
- package/dist/images-Ce9SCu3X.js.map +1 -0
- package/dist/{index-BreL0YrQ.d.ts → index-D0A5tbWu.d.ts} +2 -2
- package/dist/index-Ib_4dwOo.d.ts +200 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/ingress-EFbrS547.js +9 -0
- package/dist/ingress-EFbrS547.js.map +1 -0
- package/dist/input-schema-DNrpTu49.js +23 -0
- package/dist/input-schema-DNrpTu49.js.map +1 -0
- package/dist/internal-kmw70xwR.js +46 -0
- package/dist/internal-kmw70xwR.js.map +1 -0
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +3 -3
- package/dist/mcp/client/do-oauth-client-provider.d.ts +1 -1
- package/dist/mcp/client/do-oauth-client-provider.js +9 -0
- package/dist/mcp/client/do-oauth-client-provider.js.map +1 -1
- package/dist/mcp/client/index.d.ts +1 -1
- package/dist/mcp/client/index.js +1 -1
- package/dist/mcp/client/x402.d.ts +35 -4
- package/dist/mcp/client/x402.js +24 -3
- package/dist/mcp/client/x402.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +2 -2
- package/dist/models/ai-sdk/index.d.ts +16 -14
- package/dist/models/ai-sdk/index.js +1 -2442
- package/dist/models/opencode/index.d.ts +65 -0
- package/dist/models/opencode/index.js +285 -0
- package/dist/models/opencode/index.js.map +1 -0
- package/dist/models/pi-ai/index.d.ts +23 -21
- package/dist/models/pi-ai/index.js +7 -6
- package/dist/models/pi-ai/index.js.map +1 -1
- package/dist/options-pwTNq9z6.d.ts +134 -0
- package/dist/protocol-BEgrJYC5.d.ts +331 -0
- package/dist/protocol-CukuI73M.d.ts +386 -0
- package/dist/protocol-cOklVBBD.js +227 -0
- package/dist/protocol-cOklVBBD.js.map +1 -0
- package/dist/{protocol-kHsr-f0X.js → protocol-eXeJxhgu.js} +2 -67
- package/dist/protocol-eXeJxhgu.js.map +1 -0
- package/dist/protocol-y2sSY584.js +148 -0
- package/dist/protocol-y2sSY584.js.map +1 -0
- package/dist/queue/index.d.ts +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/routing/index.d.ts +2 -2
- package/dist/{scheduler-RdACNXDP.d.ts → scheduler-CrFCnv_J.d.ts} +2 -2
- package/dist/schedules/index.d.ts +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +11 -434
- package/dist/sessions/index.js +1 -2271
- package/dist/sessions-B9pNuxdT.js +2272 -0
- package/dist/sessions-B9pNuxdT.js.map +1 -0
- package/dist/settings-C45hDliX.js +253 -0
- package/dist/settings-C45hDliX.js.map +1 -0
- package/dist/{transport-PfFnt-Ld.d.ts → settings-NgGwF7Qt.d.ts} +14 -208
- package/dist/{src-BZeu-SCD.js → src-DgCbZtIR.js} +4 -4
- package/dist/{src-BZeu-SCD.js.map → src-DgCbZtIR.js.map} +1 -1
- package/dist/stall-watchdog-BpRF2uwG.js +195 -0
- package/dist/stall-watchdog-BpRF2uwG.js.map +1 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/store-DLeqKuj3.d.ts +310 -0
- package/dist/store-vgcPYo8K.js +432 -0
- package/dist/store-vgcPYo8K.js.map +1 -0
- package/dist/streams/index.d.ts +1 -1
- package/dist/streams/index.js +1 -1
- package/dist/{streams-DW-UCOUc.js → streams-CITClVer.js} +16 -3
- package/dist/streams-CITClVer.js.map +1 -0
- package/dist/{streams-Cmkj9QfW.d.ts → streams-fbG61qM8.d.ts} +2 -2
- package/dist/sub-routing.d.ts +1 -1
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.js +1 -1
- package/dist/{tasks-68DESxf0.js → tasks-CkTZNaV1.js} +2 -1
- package/dist/tasks-CkTZNaV1.js.map +1 -0
- package/dist/{tasks-9YxYtByh.d.ts → tasks-PtlB4Uyn.d.ts} +2 -2
- package/dist/tool-3PA8xjB_.js +421 -0
- package/dist/tool-3PA8xjB_.js.map +1 -0
- package/dist/tool-BPJEbwaX.d.ts +278 -0
- package/dist/transport-k2mFrtL8.d.ts +200 -0
- package/dist/turns-DCV3ycDl.js +317 -0
- package/dist/turns-DCV3ycDl.js.map +1 -0
- package/dist/voice/client.js +1 -1
- package/dist/voice/index.d.ts +2 -2
- package/dist/voice/index.js +1 -1
- package/dist/websearch/index.d.ts +58 -0
- package/dist/websearch/index.js +2 -0
- package/dist/websearch/tools/ai-sdk.d.ts +77 -0
- package/dist/websearch/tools/ai-sdk.js +46 -0
- package/dist/websearch/tools/ai-sdk.js.map +1 -0
- package/dist/websearch/tools/pi.d.ts +82 -0
- package/dist/websearch/tools/pi.js +89 -0
- package/dist/websearch/tools/pi.js.map +1 -0
- package/dist/websearch/tools/tanstack-ai.d.ts +71 -0
- package/dist/websearch/tools/tanstack-ai.js +46 -0
- package/dist/websearch/tools/tanstack-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +12 -296
- package/dist/websockets/index.js +1 -1
- package/dist/{websockets-Ci-LZFHH.js → websockets-CmENGb-Z.js} +18 -4
- package/dist/websockets-CmENGb-Z.js.map +1 -0
- package/dist/{wire-types-CrjKvDyi.d.ts → wire-types-Cv0iwjk5.d.ts} +3 -51
- package/dist/workflow-types.d.ts +1 -1
- package/dist/workflows.d.ts +1 -1
- package/dist/workflows.js +1 -1
- package/docs/browse-the-web.md +3 -0
- package/docs/harnesses/pi.md +114 -32
- package/docs/harnesses/think.md +215 -0
- package/docs/index.md +4 -3
- package/docs/lifecycle.md +40 -0
- package/docs/models-pi-ai.md +5 -5
- package/docs/search-the-web.md +133 -0
- package/package.json +126 -47
- package/dist/browser-tool-CQ10e6Kj.js.map +0 -1
- package/dist/channel-Bnm4S7T2.d.ts +0 -491
- package/dist/channels/ai-sdk.d.ts +0 -46
- package/dist/channels/ai-sdk.js +0 -120
- package/dist/channels/ai-sdk.js.map +0 -1
- package/dist/channels/email.js.map +0 -1
- package/dist/channels/index.d.ts +0 -233
- package/dist/channels/index.js +0 -608
- package/dist/channels/index.js.map +0 -1
- package/dist/channels/slack.js +0 -614
- package/dist/channels/slack.js.map +0 -1
- package/dist/channels/tanstack-ai.d.ts +0 -39
- package/dist/channels/tanstack-ai.js +0 -17
- package/dist/channels/tanstack-ai.js.map +0 -1
- package/dist/channels/telegram.js +0 -427
- package/dist/channels/telegram.js.map +0 -1
- package/dist/channels/voice.d.ts +0 -45
- package/dist/channels/voice.js +0 -122
- package/dist/channels/voice.js.map +0 -1
- package/dist/harnesses/pi/index.js.map +0 -1
- package/dist/images-iiTDSyRu.js.map +0 -1
- package/dist/index-BB0kqhIz.d.ts +0 -101
- package/dist/ingress-BfetZbMO.js +0 -83
- package/dist/ingress-BfetZbMO.js.map +0 -1
- package/dist/internal-CYlgHl1l.js +0 -59
- package/dist/internal-CYlgHl1l.js.map +0 -1
- package/dist/models/ai-sdk/index.js.map +0 -1
- package/dist/protocol-kHsr-f0X.js.map +0 -1
- package/dist/sessions/index.js.map +0 -1
- package/dist/streams-DW-UCOUc.js.map +0 -1
- package/dist/surface-bZZJqBka.js +0 -17
- package/dist/surface-bZZJqBka.js.map +0 -1
- package/dist/tasks-68DESxf0.js.map +0 -1
- package/dist/tool-schema-CBjGPrsQ.js +0 -31
- package/dist/tool-schema-CBjGPrsQ.js.map +0 -1
- package/dist/websockets-Ci-LZFHH.js.map +0 -1
- package/docs/channels.md +0 -323
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "../../classPrivateFieldGet2-DZBYAB34.js";
|
|
2
|
+
import { t as _classPrivateMethodInitSpec } from "../../classPrivateMethodInitSpec-qMjJ6sHQ.js";
|
|
3
|
+
import { t as LifecycleCapability } from "../../capability-B4WbF81e.js";
|
|
4
|
+
import { t as TextSegmentJoiner } from "../../text-segment-joiner-BtAFQSA_.js";
|
|
5
|
+
import { n as Streams } from "../../streams-CITClVer.js";
|
|
6
|
+
import { t as applyChunks } from "../../apply-chunks-1FheRb9W.js";
|
|
7
|
+
import { a as toParticipant, i as participantRoute, o as unsupported } from "../../internal-kmw70xwR.js";
|
|
8
|
+
import { t as WEB_IDENTITY_HEADER } from "../../protocol-y2sSY584.js";
|
|
9
|
+
import { t as matchesPath } from "../../ingress-EFbrS547.js";
|
|
10
|
+
//#region src/experimental/channels/harness-conversations.ts
|
|
11
|
+
function isTurnEvent(event) {
|
|
12
|
+
return event.type !== "conversation-create" && event.type !== "conversation-fork" && event.type !== "conversation-reset";
|
|
13
|
+
}
|
|
14
|
+
const PREFIX = "channels:harness:";
|
|
15
|
+
var _harness$1 = /* @__PURE__ */ new WeakMap();
|
|
16
|
+
var _host$1 = /* @__PURE__ */ new WeakMap();
|
|
17
|
+
var _links = /* @__PURE__ */ new WeakMap();
|
|
18
|
+
var _HarnessConversations_brand = /* @__PURE__ */ new WeakSet();
|
|
19
|
+
/**
|
|
20
|
+
* Serves harness sessions as Channels conversations: the conversation id is
|
|
21
|
+
* the session id, a turn is one operation (its id is the inbound message's
|
|
22
|
+
* event id), and a run is one response. Operations that join a running run
|
|
23
|
+
* (steers) share its response.
|
|
24
|
+
*
|
|
25
|
+
* The harness owns the transcript. Turn records live in storage; each
|
|
26
|
+
* watched session's transcript is held in memory, from the watch's state
|
|
27
|
+
* and the messages that follow it.
|
|
28
|
+
*/
|
|
29
|
+
var HarnessConversations = class {
|
|
30
|
+
constructor(harness, host) {
|
|
31
|
+
_classPrivateMethodInitSpec(this, _HarnessConversations_brand);
|
|
32
|
+
_classPrivateFieldInitSpec(this, _harness$1, void 0);
|
|
33
|
+
_classPrivateFieldInitSpec(this, _host$1, void 0);
|
|
34
|
+
_classPrivateFieldInitSpec(this, _links, /* @__PURE__ */ new Map());
|
|
35
|
+
_classPrivateFieldSet2(_harness$1, this, harness);
|
|
36
|
+
_classPrivateFieldSet2(_host$1, this, host);
|
|
37
|
+
}
|
|
38
|
+
/** The conversation a surface joins when it names none. */
|
|
39
|
+
defaultConversation() {
|
|
40
|
+
return _classPrivateFieldGet2(_harness$1, this).session().id;
|
|
41
|
+
}
|
|
42
|
+
/** Follow every session a surface has joined. Call from `onStart`. */
|
|
43
|
+
async attachAll() {
|
|
44
|
+
const sessions = _classPrivateFieldGet2(_host$1, this).kv().list({ prefix: `${PREFIX}session:` });
|
|
45
|
+
await Promise.all([...sessions].map(([key]) => _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, key.slice(`${PREFIX}session:`.length)).catch((error) => console.error("Failed to follow a harness session", error))));
|
|
46
|
+
}
|
|
47
|
+
async snapshot(conversationId) {
|
|
48
|
+
return {
|
|
49
|
+
messages: [...(await _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, conversationId)).messages],
|
|
50
|
+
turns: _assertClassBrand(_HarnessConversations_brand, this, _turns).call(this, conversationId).filter(isOpen)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async onEvent(event, origin) {
|
|
54
|
+
const conversationId = origin.conversationId;
|
|
55
|
+
const link = await _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, conversationId);
|
|
56
|
+
switch (event.type) {
|
|
57
|
+
case "message":
|
|
58
|
+
if (!_assertClassBrand(_HarnessConversations_brand, this, _turnRecord).call(this, conversationId, event.eventId)) await _assertClassBrand(_HarnessConversations_brand, this, _save).call(this, conversationId, {
|
|
59
|
+
turnId: event.eventId,
|
|
60
|
+
startedBy: event.eventId,
|
|
61
|
+
messageId: event.message.id,
|
|
62
|
+
status: "queued"
|
|
63
|
+
});
|
|
64
|
+
await link.session.submit({
|
|
65
|
+
parts: inputParts(event.message),
|
|
66
|
+
messageId: event.message.id,
|
|
67
|
+
from: { participantId: origin.participant.id }
|
|
68
|
+
}, { operationId: event.eventId });
|
|
69
|
+
return;
|
|
70
|
+
case "tool-result":
|
|
71
|
+
case "approval-response": {
|
|
72
|
+
const turn = _assertClassBrand(_HarnessConversations_brand, this, _turnRecord).call(this, conversationId, event.turnId);
|
|
73
|
+
if (!turn) return;
|
|
74
|
+
const operationId = event.eventId;
|
|
75
|
+
_classPrivateFieldGet2(_host$1, this).kv().put(aliasKey(conversationId, operationId), turn.turnId);
|
|
76
|
+
await link.session.submit({
|
|
77
|
+
...event.type === "tool-result" ? {
|
|
78
|
+
type: "tool-result",
|
|
79
|
+
toolCallId: event.toolCallId,
|
|
80
|
+
result: event.result
|
|
81
|
+
} : {
|
|
82
|
+
type: "approval",
|
|
83
|
+
approvalId: event.approvalId,
|
|
84
|
+
approved: event.approved,
|
|
85
|
+
...event.reason !== void 0 && { reason: event.reason }
|
|
86
|
+
},
|
|
87
|
+
from: { participantId: origin.participant.id }
|
|
88
|
+
}, { operationId });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
case "cancel": {
|
|
92
|
+
const turn = _assertClassBrand(_HarnessConversations_brand, this, _turnRecord).call(this, conversationId, event.turnId);
|
|
93
|
+
await link.session.abort(turn?.operation ?? event.turnId);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Every session, as conversations. */
|
|
99
|
+
async list() {
|
|
100
|
+
return (await _classPrivateFieldGet2(_harness$1, this).sessions.list()).map(({ id, parent, busy }) => ({
|
|
101
|
+
id,
|
|
102
|
+
busy,
|
|
103
|
+
...parent !== void 0 && { parent }
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
/** Create a session; a conversation operation. */
|
|
107
|
+
async create() {
|
|
108
|
+
const session = await _classPrivateFieldGet2(_harness$1, this).sessions.create();
|
|
109
|
+
await _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, session.id);
|
|
110
|
+
return session.id;
|
|
111
|
+
}
|
|
112
|
+
/** Fork a session; a conversation operation. */
|
|
113
|
+
async fork(from) {
|
|
114
|
+
const session = await _classPrivateFieldGet2(_harness$1, this).sessions.fork(from);
|
|
115
|
+
await _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, session.id);
|
|
116
|
+
return session.id;
|
|
117
|
+
}
|
|
118
|
+
/** Reset a session's context; the watch reports it as a `reset` event. */
|
|
119
|
+
async reset(conversationId, handoff) {
|
|
120
|
+
await (await _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, conversationId)).session.reset(handoff);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function _link(conversationId) {
|
|
124
|
+
let link = _classPrivateFieldGet2(_links, this).get(conversationId);
|
|
125
|
+
if (!link) {
|
|
126
|
+
const attached = _assertClassBrand(_HarnessConversations_brand, this, _attach).call(this, conversationId).then((linked) => {
|
|
127
|
+
linked.watch.closed.then(() => {
|
|
128
|
+
if (_classPrivateFieldGet2(_links, this).get(conversationId) === attached) _classPrivateFieldGet2(_links, this).delete(conversationId);
|
|
129
|
+
});
|
|
130
|
+
return linked;
|
|
131
|
+
}, (error) => {
|
|
132
|
+
_classPrivateFieldGet2(_links, this).delete(conversationId);
|
|
133
|
+
throw error;
|
|
134
|
+
});
|
|
135
|
+
link = attached;
|
|
136
|
+
_classPrivateFieldGet2(_links, this).set(conversationId, link);
|
|
137
|
+
}
|
|
138
|
+
return link;
|
|
139
|
+
}
|
|
140
|
+
async function _attach(conversationId) {
|
|
141
|
+
const session = _classPrivateFieldGet2(_harness$1, this).session(conversationId);
|
|
142
|
+
const watch = await session.watch();
|
|
143
|
+
_classPrivateFieldGet2(_host$1, this).kv().put(`${PREFIX}session:${conversationId}`, true);
|
|
144
|
+
const link = new SessionLink(session, watch);
|
|
145
|
+
await _assertClassBrand(_HarnessConversations_brand, this, _resume).call(this, conversationId, link);
|
|
146
|
+
watch.start((events) => _assertClassBrand(_HarnessConversations_brand, this, _onEvents).call(this, conversationId, link, events));
|
|
147
|
+
return link;
|
|
148
|
+
}
|
|
149
|
+
/** Catch up from the watch's state, after an attach or a restart. */
|
|
150
|
+
async function _resume(conversationId, link) {
|
|
151
|
+
const { state } = link.watch;
|
|
152
|
+
const live = new Set(state.pending.map((p) => p.operationId));
|
|
153
|
+
for (const status of state.pending) await _assertClassBrand(_HarnessConversations_brand, this, _operation).call(this, conversationId, link, status);
|
|
154
|
+
if (state.run) {
|
|
155
|
+
for (const id of state.run.operations) live.add(id);
|
|
156
|
+
await _assertClassBrand(_HarnessConversations_brand, this, _runStart).call(this, conversationId, link, state.run.operations);
|
|
157
|
+
for (const chunk of state.run.partial ?? []) link.append(chunk);
|
|
158
|
+
}
|
|
159
|
+
for (const turn of _assertClassBrand(_HarnessConversations_brand, this, _turns).call(this, conversationId)) {
|
|
160
|
+
const operationId = turn.operation ?? turn.turnId;
|
|
161
|
+
if (turn.status === "settled" || live.has(operationId)) continue;
|
|
162
|
+
link.session.wait(operationId).then((result) => _assertClassBrand(_HarnessConversations_brand, this, _operation).call(this, conversationId, link, result).catch((error) => console.error("Failed to settle a turn", error)), (error) => console.error("Failed to settle a turn", error));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function _onEvents(conversationId, link, events) {
|
|
166
|
+
for (const event of events) try {
|
|
167
|
+
await _assertClassBrand(_HarnessConversations_brand, this, _onEvent).call(this, conversationId, link, event);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error("Harness event failed", event.type, error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function _onEvent(conversationId, link, event) {
|
|
173
|
+
switch (event.type) {
|
|
174
|
+
case "operation": return _assertClassBrand(_HarnessConversations_brand, this, _operation).call(this, conversationId, link, event.status);
|
|
175
|
+
case "run-start": return _assertClassBrand(_HarnessConversations_brand, this, _runStart).call(this, conversationId, link, event.operations);
|
|
176
|
+
case "chunk":
|
|
177
|
+
link.append(event.chunk);
|
|
178
|
+
return;
|
|
179
|
+
case "message":
|
|
180
|
+
link.upsert(event.message);
|
|
181
|
+
await _classPrivateFieldGet2(_host$1, this).publishMessages(conversationId, [event.message]);
|
|
182
|
+
return;
|
|
183
|
+
case "run-end":
|
|
184
|
+
link.endResponse();
|
|
185
|
+
return;
|
|
186
|
+
case "reset":
|
|
187
|
+
link.endResponse();
|
|
188
|
+
link.messages = [];
|
|
189
|
+
await _classPrivateFieldGet2(_host$1, this).reset(conversationId);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function _runStart(conversationId, link, operations) {
|
|
194
|
+
link.endResponse();
|
|
195
|
+
const queued = _assertClassBrand(_HarnessConversations_brand, this, _turns).call(this, conversationId).find((t) => t.status === "queued");
|
|
196
|
+
const first = operations[0] ?? queued?.operation ?? queued?.turnId;
|
|
197
|
+
if (first === void 0) return;
|
|
198
|
+
const turn = _assertClassBrand(_HarnessConversations_brand, this, _turn).call(this, conversationId, first);
|
|
199
|
+
if (!turn) return;
|
|
200
|
+
link.response = await _classPrivateFieldGet2(_host$1, this).openResponse(conversationId, turn.turnId);
|
|
201
|
+
const last = link.messages.at(-1);
|
|
202
|
+
link.extends = turn.turnId !== first && last?.role === "assistant" ? last.id : void 0;
|
|
203
|
+
for (const id of operations.length ? operations : [first]) await _assertClassBrand(_HarnessConversations_brand, this, _join).call(this, conversationId, link, id);
|
|
204
|
+
}
|
|
205
|
+
async function _operation(conversationId, link, status) {
|
|
206
|
+
const { operationId } = status;
|
|
207
|
+
const turn = _assertClassBrand(_HarnessConversations_brand, this, _turn).call(this, conversationId, operationId);
|
|
208
|
+
switch (status.status) {
|
|
209
|
+
case "queued":
|
|
210
|
+
if (!turn) await _assertClassBrand(_HarnessConversations_brand, this, _save).call(this, conversationId, {
|
|
211
|
+
turnId: operationId,
|
|
212
|
+
startedBy: operationId,
|
|
213
|
+
messageId: operationId,
|
|
214
|
+
status: "queued"
|
|
215
|
+
});
|
|
216
|
+
else if (turn.turnId !== operationId && turn.status === "settled") await _assertClassBrand(_HarnessConversations_brand, this, _save).call(this, conversationId, {
|
|
217
|
+
turnId: turn.turnId,
|
|
218
|
+
startedBy: turn.startedBy,
|
|
219
|
+
messageId: turn.messageId,
|
|
220
|
+
operation: operationId,
|
|
221
|
+
status: "queued"
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
case "placed": return _assertClassBrand(_HarnessConversations_brand, this, _join).call(this, conversationId, link, operationId);
|
|
225
|
+
case "done": return _assertClassBrand(_HarnessConversations_brand, this, _settle).call(this, conversationId, link, operationId, { outcome: "completed" });
|
|
226
|
+
case "unanswered": {
|
|
227
|
+
const stopped = status.reason === "aborted" || status.reason === "withdrawn";
|
|
228
|
+
const refusedAnswer = turn !== void 0 && turn.turnId !== operationId && !stopped;
|
|
229
|
+
return _assertClassBrand(_HarnessConversations_brand, this, _settle).call(this, conversationId, link, operationId, stopped ? { outcome: "aborted" } : refusedAnswer ? { outcome: "completed" } : {
|
|
230
|
+
outcome: "failed",
|
|
231
|
+
error: `Not answered${status.reason ? `: ${status.reason}` : ""}`
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function _join(conversationId, link, operationId) {
|
|
237
|
+
const turn = _assertClassBrand(_HarnessConversations_brand, this, _turn).call(this, conversationId, operationId);
|
|
238
|
+
if (!turn || !link.response) return;
|
|
239
|
+
const answer = turn.turnId !== operationId;
|
|
240
|
+
if (turn.status === "settled" && !answer) return;
|
|
241
|
+
if (turn.status === "running" && turn.responseId === link.response.id) return;
|
|
242
|
+
await _assertClassBrand(_HarnessConversations_brand, this, _save).call(this, conversationId, {
|
|
243
|
+
turnId: turn.turnId,
|
|
244
|
+
startedBy: turn.startedBy,
|
|
245
|
+
messageId: turn.messageId,
|
|
246
|
+
...answer && { operation: operationId },
|
|
247
|
+
status: "running",
|
|
248
|
+
responseId: link.response.id,
|
|
249
|
+
...link.extends !== void 0 && { extends: link.extends }
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async function _settle(conversationId, link, operationId, end) {
|
|
253
|
+
const turn = _assertClassBrand(_HarnessConversations_brand, this, _turn).call(this, conversationId, operationId);
|
|
254
|
+
if (!turn || turn.status === "settled") return;
|
|
255
|
+
if ((turn.operation ?? turn.turnId) !== operationId) return;
|
|
256
|
+
const start = link.messages.findIndex((m) => m.id === turn.messageId);
|
|
257
|
+
const messageIds = [];
|
|
258
|
+
for (const message of start === -1 ? [] : link.messages.slice(start + 1)) {
|
|
259
|
+
if (message.role === "user") break;
|
|
260
|
+
messageIds.push(message.id);
|
|
261
|
+
}
|
|
262
|
+
const waiting = end.outcome === "completed" && link.messages.some((message) => messageIds.includes(message.id) && awaitsInput(message));
|
|
263
|
+
await _assertClassBrand(_HarnessConversations_brand, this, _save).call(this, conversationId, {
|
|
264
|
+
turnId: turn.turnId,
|
|
265
|
+
startedBy: turn.startedBy,
|
|
266
|
+
messageId: turn.messageId,
|
|
267
|
+
...turn.operation !== void 0 && { operation: turn.operation },
|
|
268
|
+
status: "settled",
|
|
269
|
+
messageIds,
|
|
270
|
+
...waiting ? { outcome: "awaiting-input" } : end
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
async function _save(conversationId, turn) {
|
|
274
|
+
_classPrivateFieldGet2(_host$1, this).kv().put(turnKey(conversationId, turn.turnId), turn);
|
|
275
|
+
await _classPrivateFieldGet2(_host$1, this).publishTurn(conversationId, turn);
|
|
276
|
+
}
|
|
277
|
+
function _turnRecord(conversationId, turnId) {
|
|
278
|
+
return _classPrivateFieldGet2(_host$1, this).kv().get(turnKey(conversationId, turnId));
|
|
279
|
+
}
|
|
280
|
+
/** The turn an operation belongs to: its own, or the one it answers. */
|
|
281
|
+
function _turn(conversationId, operationId) {
|
|
282
|
+
const turnId = _classPrivateFieldGet2(_host$1, this).kv().get(aliasKey(conversationId, operationId)) ?? operationId;
|
|
283
|
+
return _assertClassBrand(_HarnessConversations_brand, this, _turnRecord).call(this, conversationId, turnId);
|
|
284
|
+
}
|
|
285
|
+
function _turns(conversationId) {
|
|
286
|
+
const prefix = turnKey(conversationId, "");
|
|
287
|
+
return [..._classPrivateFieldGet2(_host$1, this).kv().list({ prefix })].map(([, turn]) => turn);
|
|
288
|
+
}
|
|
289
|
+
/** One watched session: its transcript and the response of its run. */
|
|
290
|
+
var SessionLink = class {
|
|
291
|
+
constructor(session, watch) {
|
|
292
|
+
this.session = session;
|
|
293
|
+
this.watch = watch;
|
|
294
|
+
this.messages = [...watch.state.messages];
|
|
295
|
+
}
|
|
296
|
+
upsert(message) {
|
|
297
|
+
const index = this.messages.findIndex((m) => m.id === message.id);
|
|
298
|
+
if (index === -1) this.messages.push(message);
|
|
299
|
+
else this.messages[index] = message;
|
|
300
|
+
}
|
|
301
|
+
append(chunk) {
|
|
302
|
+
try {
|
|
303
|
+
this.response?.append(chunk);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.warn("Dropped a harness chunk", chunk.type, error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
endResponse() {
|
|
309
|
+
this.response?.end();
|
|
310
|
+
this.response = void 0;
|
|
311
|
+
this.extends = void 0;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
function turnKey(conversationId, turnId) {
|
|
315
|
+
return `${PREFIX}turn:${JSON.stringify(conversationId)}:${turnId}`;
|
|
316
|
+
}
|
|
317
|
+
function aliasKey(conversationId, operationId) {
|
|
318
|
+
return `${PREFIX}alias:${JSON.stringify(conversationId)}:${operationId}`;
|
|
319
|
+
}
|
|
320
|
+
/** Whether the message has a tool call waiting for a result or approval. */
|
|
321
|
+
function awaitsInput(message) {
|
|
322
|
+
return message.parts.some((part) => part.type === "tool" && (part.state === "input-available" || part.state === "approval-requested"));
|
|
323
|
+
}
|
|
324
|
+
function isOpen(turn) {
|
|
325
|
+
return turn.status !== "settled" || turn.outcome === "awaiting-input";
|
|
326
|
+
}
|
|
327
|
+
function inputParts(message) {
|
|
328
|
+
return message.parts.flatMap((part) => {
|
|
329
|
+
if (part.type === "text") return [{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: part.text
|
|
332
|
+
}];
|
|
333
|
+
if (part.type === "file") return [{
|
|
334
|
+
type: "file",
|
|
335
|
+
mediaType: part.mediaType,
|
|
336
|
+
url: part.url,
|
|
337
|
+
...part.filename !== void 0 && { filename: part.filename }
|
|
338
|
+
}];
|
|
339
|
+
return [];
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/experimental/channels/response.ts
|
|
344
|
+
/** A chunk that breaks the response grammar. Nothing was recorded. */
|
|
345
|
+
var ResponseGrammarError = class extends Error {
|
|
346
|
+
constructor(message) {
|
|
347
|
+
super(message);
|
|
348
|
+
this.name = "ResponseGrammarError";
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
function responseWriter(writer) {
|
|
352
|
+
const open = /* @__PURE__ */ new Set();
|
|
353
|
+
function check(chunk) {
|
|
354
|
+
const [kind, id, step] = partOf(chunk);
|
|
355
|
+
if (!kind) return void 0;
|
|
356
|
+
const key = `${kind}:${id}`;
|
|
357
|
+
if (step === "start") {
|
|
358
|
+
if (open.has(key)) fail(`${kind} "${id}" is already open`);
|
|
359
|
+
return () => open.add(key);
|
|
360
|
+
}
|
|
361
|
+
if (step !== "close" && !open.has(key)) fail(`${kind} "${id}" is not open`);
|
|
362
|
+
return step === "delta" ? void 0 : () => open.delete(key);
|
|
363
|
+
}
|
|
364
|
+
const response = {
|
|
365
|
+
id: writer.streamId,
|
|
366
|
+
append(chunk) {
|
|
367
|
+
const apply = check(chunk);
|
|
368
|
+
writer.append(chunk);
|
|
369
|
+
apply?.();
|
|
370
|
+
},
|
|
371
|
+
end() {
|
|
372
|
+
writer.close();
|
|
373
|
+
},
|
|
374
|
+
async pipe(chunks) {
|
|
375
|
+
try {
|
|
376
|
+
for await (const chunk of chunks) response.append(chunk);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
writer.error("interrupted");
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
response.end();
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
return response;
|
|
385
|
+
}
|
|
386
|
+
function partOf(chunk) {
|
|
387
|
+
switch (chunk.type) {
|
|
388
|
+
case "text-start": return [
|
|
389
|
+
"text",
|
|
390
|
+
chunk.id,
|
|
391
|
+
"start"
|
|
392
|
+
];
|
|
393
|
+
case "text-delta": return [
|
|
394
|
+
"text",
|
|
395
|
+
chunk.id,
|
|
396
|
+
"delta"
|
|
397
|
+
];
|
|
398
|
+
case "text-end": return [
|
|
399
|
+
"text",
|
|
400
|
+
chunk.id,
|
|
401
|
+
"end"
|
|
402
|
+
];
|
|
403
|
+
case "reasoning-start": return [
|
|
404
|
+
"reasoning",
|
|
405
|
+
chunk.id,
|
|
406
|
+
"start"
|
|
407
|
+
];
|
|
408
|
+
case "reasoning-delta": return [
|
|
409
|
+
"reasoning",
|
|
410
|
+
chunk.id,
|
|
411
|
+
"delta"
|
|
412
|
+
];
|
|
413
|
+
case "reasoning-end": return [
|
|
414
|
+
"reasoning",
|
|
415
|
+
chunk.id,
|
|
416
|
+
"end"
|
|
417
|
+
];
|
|
418
|
+
case "tool-input-start": return [
|
|
419
|
+
"tool input",
|
|
420
|
+
chunk.toolCallId,
|
|
421
|
+
"start"
|
|
422
|
+
];
|
|
423
|
+
case "tool-input-delta": return [
|
|
424
|
+
"tool input",
|
|
425
|
+
chunk.toolCallId,
|
|
426
|
+
"delta"
|
|
427
|
+
];
|
|
428
|
+
case "tool-input-available":
|
|
429
|
+
case "tool-input-error": return [
|
|
430
|
+
"tool input",
|
|
431
|
+
chunk.toolCallId,
|
|
432
|
+
"close"
|
|
433
|
+
];
|
|
434
|
+
default: return [void 0];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function fail(message) {
|
|
438
|
+
throw new ResponseGrammarError(message);
|
|
439
|
+
}
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/experimental/channels/conversations.ts
|
|
442
|
+
const TAG_PREFIX = "channels:";
|
|
443
|
+
const GENERIC_FAILURE = "The turn failed.";
|
|
444
|
+
const SEEN_EVENTS_LIMIT = 1e3;
|
|
445
|
+
var _websockets = /* @__PURE__ */ new WeakMap();
|
|
446
|
+
var _harness = /* @__PURE__ */ new WeakMap();
|
|
447
|
+
var _channels$1 = /* @__PURE__ */ new WeakMap();
|
|
448
|
+
var _queues = /* @__PURE__ */ new WeakMap();
|
|
449
|
+
var _seen = /* @__PURE__ */ new WeakMap();
|
|
450
|
+
var _Channels_brand = /* @__PURE__ */ new WeakSet();
|
|
451
|
+
/**
|
|
452
|
+
* Serves an agent harness's sessions as conversations: carries inbound
|
|
453
|
+
* events to the harness, and responses, turn statuses and transcript
|
|
454
|
+
* updates back to every channel showing the conversation.
|
|
455
|
+
*/
|
|
456
|
+
var Channels = class Channels extends LifecycleCapability {
|
|
457
|
+
/**
|
|
458
|
+
* Channels for any shared-interface harness, with their own Streams and
|
|
459
|
+
* (when a channel needs them) WebSockets. Lifecycle does not install one
|
|
460
|
+
* capability from another yet, so install them alongside:
|
|
461
|
+
*
|
|
462
|
+
* ```ts
|
|
463
|
+
* readonly channels = Channels.forHarness(harness, {
|
|
464
|
+
* channels: { web: new WebChannel() }
|
|
465
|
+
* });
|
|
466
|
+
* readonly lifecycle = Lifecycle.install(this)
|
|
467
|
+
* .use(this.harness)
|
|
468
|
+
* .use(this.channels.streams)
|
|
469
|
+
* .use(this.channels)
|
|
470
|
+
* .use(this.channels.websockets);
|
|
471
|
+
*
|
|
472
|
+
* receive(event: GatewayEvent, origin: GatewayOrigin) {
|
|
473
|
+
* return this.channels.receive(event, origin);
|
|
474
|
+
* }
|
|
475
|
+
* ```
|
|
476
|
+
*/
|
|
477
|
+
static forHarness(harness, options) {
|
|
478
|
+
return new Channels({
|
|
479
|
+
harness,
|
|
480
|
+
channels: options.channels,
|
|
481
|
+
streams: options.streams ?? new Streams(),
|
|
482
|
+
...options.websockets && { websockets: options.websockets }
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
constructor(options) {
|
|
486
|
+
super("channels");
|
|
487
|
+
_classPrivateMethodInitSpec(this, _Channels_brand);
|
|
488
|
+
_classPrivateFieldInitSpec(this, _websockets, void 0);
|
|
489
|
+
_classPrivateFieldInitSpec(this, _harness, void 0);
|
|
490
|
+
_classPrivateFieldInitSpec(this, _channels$1, void 0);
|
|
491
|
+
_classPrivateFieldInitSpec(this, _queues, /* @__PURE__ */ new Map());
|
|
492
|
+
_classPrivateFieldInitSpec(this, _seen, /* @__PURE__ */ new Map());
|
|
493
|
+
this.streams = options.streams;
|
|
494
|
+
_classPrivateFieldSet2(_websockets, this, options.websockets);
|
|
495
|
+
_classPrivateFieldSet2(_harness, this, new HarnessConversations(options.harness, {
|
|
496
|
+
kv: () => this.lifecycle.storage.kv,
|
|
497
|
+
openResponse: (conversationId, turnId) => _assertClassBrand(_Channels_brand, this, _openResponse).call(this, conversationId, turnId),
|
|
498
|
+
publishTurn: (conversationId, turn) => _assertClassBrand(_Channels_brand, this, _publish).call(this, conversationId, {
|
|
499
|
+
type: "turn",
|
|
500
|
+
turn: statusOf(turn)
|
|
501
|
+
}),
|
|
502
|
+
publishMessages: (conversationId, messages) => _assertClassBrand(_Channels_brand, this, _publish).call(this, conversationId, {
|
|
503
|
+
type: "messages",
|
|
504
|
+
messages
|
|
505
|
+
}),
|
|
506
|
+
reset: (conversationId) => _assertClassBrand(_Channels_brand, this, _reset).call(this, conversationId)
|
|
507
|
+
}));
|
|
508
|
+
_classPrivateFieldSet2(_channels$1, this, Object.entries(options.channels));
|
|
509
|
+
for (const [channelKey, channel] of _classPrivateFieldGet2(_channels$1, this)) channel.mount(_assertClassBrand(_Channels_brand, this, _host).call(this, channelKey));
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* The WebSockets the channels share. Install it with Lifecycle. Throws
|
|
513
|
+
* when no channel uses WebSockets.
|
|
514
|
+
*/
|
|
515
|
+
get websockets() {
|
|
516
|
+
if (!_classPrivateFieldGet2(_websockets, this)) throw new Error("No channel uses WebSockets");
|
|
517
|
+
return _classPrivateFieldGet2(_websockets, this);
|
|
518
|
+
}
|
|
519
|
+
/** Mark responses left streaming by an earlier instance as interrupted. */
|
|
520
|
+
async onStart() {
|
|
521
|
+
const streaming = await this.streams.list({
|
|
522
|
+
state: "streaming",
|
|
523
|
+
limit: 1e3
|
|
524
|
+
});
|
|
525
|
+
for (const status of streaming) {
|
|
526
|
+
const conversationId = conversationOf(status.tag);
|
|
527
|
+
if (conversationId === void 0) continue;
|
|
528
|
+
(await this.streams.open(status.streamId)).error("interrupted");
|
|
529
|
+
await _assertClassBrand(_Channels_brand, this, _publish).call(this, conversationId, {
|
|
530
|
+
type: "response-end",
|
|
531
|
+
responseId: status.streamId,
|
|
532
|
+
ending: "interrupted"
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
await _classPrivateFieldGet2(_harness, this).attachAll();
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Deliver an inbound event from the gateway, through the same path as
|
|
539
|
+
* the agent's own channels. The agent calls this from its RPC method.
|
|
540
|
+
*/
|
|
541
|
+
async receive(event, from) {
|
|
542
|
+
const origin = {
|
|
543
|
+
conversationId: from.conversationId ?? _classPrivateFieldGet2(_harness, this).defaultConversation(),
|
|
544
|
+
participant: from.participant,
|
|
545
|
+
surface: from.surface
|
|
546
|
+
};
|
|
547
|
+
if (isConversationOperation(event.type)) throw new Error(`${event.type} is not supported on this surface`);
|
|
548
|
+
const { surface } = origin;
|
|
549
|
+
this.lifecycle.storage.kv.put(surfaceKey(origin.conversationId, surface.channelKey, surface.address), surface);
|
|
550
|
+
await _assertClassBrand(_Channels_brand, this, _dispatch$1).call(this, event.type !== "approval-response" || "turnId" in event ? event : await _assertClassBrand(_Channels_brand, this, _withTurn).call(this, event, origin), origin);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
/** Open a new response for a turn. Each run opens one. */
|
|
554
|
+
async function _openResponse(conversationId, turnId) {
|
|
555
|
+
return responseWriter(await this.streams.open(crypto.randomUUID(), {
|
|
556
|
+
tag: TAG_PREFIX + conversationId,
|
|
557
|
+
metadata: { turnId }
|
|
558
|
+
}));
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* After the harness reset a session: delete the conversation's responses,
|
|
562
|
+
* stopping live producers, then push an empty snapshot.
|
|
563
|
+
*/
|
|
564
|
+
async function _reset(conversationId) {
|
|
565
|
+
const streams = this.streams;
|
|
566
|
+
const tag = TAG_PREFIX + conversationId;
|
|
567
|
+
for (;;) {
|
|
568
|
+
const batch = await streams.list({ tag });
|
|
569
|
+
if (batch.length === 0) break;
|
|
570
|
+
for (const { streamId, state } of batch) {
|
|
571
|
+
if (state === "streaming") (await streams.open(streamId)).close();
|
|
572
|
+
await streams.delete(streamId);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const prefix = JSON.stringify([conversationId]).slice(0, -1) + ",";
|
|
576
|
+
for (const key of _classPrivateFieldGet2(_seen, this).keys()) if (key.startsWith(prefix)) _classPrivateFieldGet2(_seen, this).delete(key);
|
|
577
|
+
await _assertClassBrand(_Channels_brand, this, _publish).call(this, conversationId, { type: "reset" });
|
|
578
|
+
}
|
|
579
|
+
async function _publish(conversationId, update) {
|
|
580
|
+
await Promise.all(_classPrivateFieldGet2(_channels$1, this).map(async ([channelKey, channel]) => {
|
|
581
|
+
try {
|
|
582
|
+
await channel.publish(conversationId, update);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error(`Channel "${channelKey}" failed to publish`, error);
|
|
585
|
+
}
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
588
|
+
function _host(channelKey) {
|
|
589
|
+
const kv = () => this.lifecycle.storage.kv;
|
|
590
|
+
const stateKey = (key) => `${TAG_PREFIX}state:${JSON.stringify([channelKey, key])}`;
|
|
591
|
+
return {
|
|
592
|
+
channelKey,
|
|
593
|
+
websockets: (create) => _classPrivateFieldGet2(_websockets, this) ?? _classPrivateFieldSet2(_websockets, this, create()),
|
|
594
|
+
surfaces: async (conversationId) => {
|
|
595
|
+
const prefix = surfaceKey(conversationId, channelKey);
|
|
596
|
+
return [...kv().list({ prefix })].map(([, surface]) => surface);
|
|
597
|
+
},
|
|
598
|
+
state: {
|
|
599
|
+
get: (key) => kv().get(stateKey(key)),
|
|
600
|
+
put: (key, value) => kv().put(stateKey(key), value),
|
|
601
|
+
delete: (key) => void kv().delete(stateKey(key))
|
|
602
|
+
},
|
|
603
|
+
snapshot: async (conversationId) => {
|
|
604
|
+
const snapshot = await _assertClassBrand(_Channels_brand, this, _runInHost).call(this, () => _classPrivateFieldGet2(_harness, this).snapshot(conversationId));
|
|
605
|
+
return {
|
|
606
|
+
...snapshot,
|
|
607
|
+
turns: snapshot.turns.map(statusOf)
|
|
608
|
+
};
|
|
609
|
+
},
|
|
610
|
+
defaultConversation: () => _classPrivateFieldGet2(_harness, this).defaultConversation(),
|
|
611
|
+
operations: CONVERSATION_OPERATIONS,
|
|
612
|
+
listConversations: () => _assertClassBrand(_Channels_brand, this, _runInHost).call(this, () => _classPrivateFieldGet2(_harness, this).list()),
|
|
613
|
+
dispatch: (event, origin) => _assertClassBrand(_Channels_brand, this, _dispatch$1).call(this, event, origin),
|
|
614
|
+
readResponse: (conversationId, responseId, options) => _assertClassBrand(_Channels_brand, this, _readResponse).call(this, conversationId, responseId, options)
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
/** Events take effect in arrival order, one at a time per conversation. */
|
|
618
|
+
function _dispatch$1(event, origin) {
|
|
619
|
+
const key = JSON.stringify([origin.conversationId, event.eventId]);
|
|
620
|
+
const seen = _classPrivateFieldGet2(_seen, this).get(key);
|
|
621
|
+
if (seen) return seen;
|
|
622
|
+
const previous = _classPrivateFieldGet2(_queues, this).get(origin.conversationId);
|
|
623
|
+
const run = async () => {
|
|
624
|
+
let result = {};
|
|
625
|
+
try {
|
|
626
|
+
result = await _assertClassBrand(_Channels_brand, this, _runInHost).call(this, async () => {
|
|
627
|
+
if (isTurnEvent(event)) {
|
|
628
|
+
await _classPrivateFieldGet2(_harness, this).onEvent(event, origin);
|
|
629
|
+
return {};
|
|
630
|
+
}
|
|
631
|
+
return _assertClassBrand(_Channels_brand, this, _operate).call(this, event, origin);
|
|
632
|
+
});
|
|
633
|
+
} catch (error) {
|
|
634
|
+
_classPrivateFieldGet2(_seen, this).delete(key);
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
if (event.type === "message") await _assertClassBrand(_Channels_brand, this, _publish).call(this, origin.conversationId, {
|
|
638
|
+
type: "messages",
|
|
639
|
+
messages: [event.message],
|
|
640
|
+
origin,
|
|
641
|
+
eventId: event.eventId
|
|
642
|
+
});
|
|
643
|
+
return result;
|
|
644
|
+
};
|
|
645
|
+
const result = previous ? previous.then(run, run) : run();
|
|
646
|
+
_classPrivateFieldGet2(_seen, this).set(key, result);
|
|
647
|
+
if (_classPrivateFieldGet2(_seen, this).size > SEEN_EVENTS_LIMIT) _classPrivateFieldGet2(_seen, this).delete(_classPrivateFieldGet2(_seen, this).keys().next().value);
|
|
648
|
+
const settled = result.catch(() => {});
|
|
649
|
+
_classPrivateFieldGet2(_queues, this).set(origin.conversationId, settled);
|
|
650
|
+
settled.then(() => {
|
|
651
|
+
if (_classPrivateFieldGet2(_queues, this).get(origin.conversationId) === settled) _classPrivateFieldGet2(_queues, this).delete(origin.conversationId);
|
|
652
|
+
});
|
|
653
|
+
return result;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Act on the conversation itself. Any participant may create, fork or
|
|
657
|
+
* reset: the agent object is the authorization boundary, and the gateway
|
|
658
|
+
* decides who reaches it.
|
|
659
|
+
*/
|
|
660
|
+
async function _operate(event, origin) {
|
|
661
|
+
const harness = _classPrivateFieldGet2(_harness, this);
|
|
662
|
+
switch (event.type) {
|
|
663
|
+
case "conversation-create": return { conversationId: await harness.create() };
|
|
664
|
+
case "conversation-fork": return { conversationId: await harness.fork(origin.conversationId) };
|
|
665
|
+
case "conversation-reset":
|
|
666
|
+
await harness.reset(origin.conversationId, event.handoff);
|
|
667
|
+
return {};
|
|
668
|
+
default: throw new Error(`${event.type} is not a conversation operation`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/** Name the turn awaiting an approval that arrived without one. */
|
|
672
|
+
async function _withTurn(event, origin) {
|
|
673
|
+
const { messages, turns } = await _assertClassBrand(_Channels_brand, this, _runInHost).call(this, () => _classPrivateFieldGet2(_harness, this).snapshot(origin.conversationId));
|
|
674
|
+
const message = messages.find((m) => m.parts.some((part) => part.type === "tool" && part.state === "approval-requested" && part.approval?.id === event.approvalId));
|
|
675
|
+
const turn = turns.find((t) => t.status === "settled" && message !== void 0 && t.messageIds.includes(message.id));
|
|
676
|
+
if (!turn) throw new Error("No turn is waiting for this approval");
|
|
677
|
+
return {
|
|
678
|
+
...event,
|
|
679
|
+
turnId: turn.turnId
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async function _readResponse(conversationId, responseId, options) {
|
|
683
|
+
const streams = this.streams;
|
|
684
|
+
if ((await streams.status(responseId))?.tag !== TAG_PREFIX + conversationId) return "not-found";
|
|
685
|
+
let cursor = options.from ?? 0;
|
|
686
|
+
for await (const batch of streams.readBatches(responseId, {
|
|
687
|
+
from: cursor,
|
|
688
|
+
signal: options.signal,
|
|
689
|
+
onUpToDate: () => options.onCaughtUp(cursor)
|
|
690
|
+
})) {
|
|
691
|
+
options.onChunks(batch[0].seq, batch.map((item) => item.chunk));
|
|
692
|
+
cursor = batch[batch.length - 1].seq + 1;
|
|
693
|
+
}
|
|
694
|
+
return (await streams.status(responseId))?.state === "errored" ? "interrupted" : "ended";
|
|
695
|
+
}
|
|
696
|
+
async function _runInHost(fn) {
|
|
697
|
+
return await this.lifecycle.runInHostContext(fn);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Where a surface is recorded. Without an address, the prefix for all of a
|
|
701
|
+
* channel's surfaces in the conversation.
|
|
702
|
+
*/
|
|
703
|
+
function surfaceKey(conversationId, channelKey, address) {
|
|
704
|
+
const scope = `${TAG_PREFIX}surface:${JSON.stringify([conversationId, channelKey])}:`;
|
|
705
|
+
return address === void 0 ? scope : scope + JSON.stringify(address);
|
|
706
|
+
}
|
|
707
|
+
const CONVERSATION_OPERATIONS = [
|
|
708
|
+
"conversation-create",
|
|
709
|
+
"conversation-fork",
|
|
710
|
+
"conversation-reset"
|
|
711
|
+
];
|
|
712
|
+
function isConversationOperation(type) {
|
|
713
|
+
return CONVERSATION_OPERATIONS.includes(type);
|
|
714
|
+
}
|
|
715
|
+
/** A turn status with only its protocol fields. */
|
|
716
|
+
function statusOf(turn) {
|
|
717
|
+
const { turnId, startedBy } = turn;
|
|
718
|
+
switch (turn.status) {
|
|
719
|
+
case "queued": return {
|
|
720
|
+
turnId,
|
|
721
|
+
startedBy,
|
|
722
|
+
status: "queued"
|
|
723
|
+
};
|
|
724
|
+
case "running": {
|
|
725
|
+
const { responseId, extends: extended } = turn;
|
|
726
|
+
return {
|
|
727
|
+
turnId,
|
|
728
|
+
startedBy,
|
|
729
|
+
status: "running",
|
|
730
|
+
responseId,
|
|
731
|
+
...extended !== void 0 && { extends: extended }
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
case "settled": {
|
|
735
|
+
const { messageIds } = turn;
|
|
736
|
+
return turn.outcome === "failed" ? {
|
|
737
|
+
turnId,
|
|
738
|
+
startedBy,
|
|
739
|
+
status: "settled",
|
|
740
|
+
outcome: "failed",
|
|
741
|
+
messageIds,
|
|
742
|
+
error: turn.error ?? GENERIC_FAILURE
|
|
743
|
+
} : {
|
|
744
|
+
turnId,
|
|
745
|
+
startedBy,
|
|
746
|
+
status: "settled",
|
|
747
|
+
outcome: turn.outcome,
|
|
748
|
+
messageIds
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function conversationOf(tag) {
|
|
754
|
+
return tag?.startsWith(TAG_PREFIX) ? tag.slice(9) : void 0;
|
|
755
|
+
}
|
|
756
|
+
//#endregion
|
|
757
|
+
//#region src/experimental/channels/stream.ts
|
|
758
|
+
/**
|
|
759
|
+
* Read a stream to completion, then finalize exactly once.
|
|
760
|
+
*
|
|
761
|
+
* `onFinish` runs whether the stream closed normally, errored because the
|
|
762
|
+
* generation failed, or stopped because `onChunk` threw. A Channel that
|
|
763
|
+
* finalizes here cannot lose a terminal provider call to an early ending.
|
|
764
|
+
*/
|
|
765
|
+
async function consumeChunks(chunks, consumer) {
|
|
766
|
+
const reader = chunks.getReader();
|
|
767
|
+
let outcome = { interrupted: false };
|
|
768
|
+
let cancellation;
|
|
769
|
+
try {
|
|
770
|
+
while (true) {
|
|
771
|
+
const { done, value } = await reader.read();
|
|
772
|
+
if (done) break;
|
|
773
|
+
await consumer.onChunk(value);
|
|
774
|
+
}
|
|
775
|
+
} catch (error) {
|
|
776
|
+
outcome = {
|
|
777
|
+
interrupted: true,
|
|
778
|
+
error
|
|
779
|
+
};
|
|
780
|
+
cancellation = reader.cancel().catch(() => {});
|
|
781
|
+
} finally {
|
|
782
|
+
reader.releaseLock();
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
return await consumer.onFinish(outcome);
|
|
786
|
+
} finally {
|
|
787
|
+
await cancellation;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Turn a response's chunks into answer text, one chunk at a time. Deltas of
|
|
792
|
+
* one part join directly; a new text part or tool activity in between adds a
|
|
793
|
+
* space. Metadata and data never split text. Returns the text each chunk adds.
|
|
794
|
+
*/
|
|
795
|
+
function createTextCollector() {
|
|
796
|
+
const joiner = new TextSegmentJoiner();
|
|
797
|
+
return (chunk) => {
|
|
798
|
+
if (chunk.type === "metadata" || chunk.type === "data" || chunk.type === "text-end") return "";
|
|
799
|
+
let added = "";
|
|
800
|
+
for (const event of joiner.pushChunk(chunk.type === "text-delta" ? {
|
|
801
|
+
type: "text-delta",
|
|
802
|
+
text: chunk.delta
|
|
803
|
+
} : { type: chunk.type })) if (event.type === "text") added += event.text;
|
|
804
|
+
return added;
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Collect a response's text into one Markdown answer.
|
|
809
|
+
*
|
|
810
|
+
* The result reports interruption instead of throwing, because a Channel that
|
|
811
|
+
* has partial text still has to decide what to deliver.
|
|
812
|
+
*/
|
|
813
|
+
function collectText(chunks) {
|
|
814
|
+
let text = "";
|
|
815
|
+
const collect = createTextCollector();
|
|
816
|
+
return consumeChunks(chunks, {
|
|
817
|
+
onChunk(chunk) {
|
|
818
|
+
text += collect(chunk);
|
|
819
|
+
},
|
|
820
|
+
onFinish: (outcome) => ({
|
|
821
|
+
text,
|
|
822
|
+
interrupted: outcome.interrupted
|
|
823
|
+
})
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
/** A finished message as a response's chunks. */
|
|
827
|
+
function messageChunks(markdown) {
|
|
828
|
+
return new ReadableStream({ start(controller) {
|
|
829
|
+
controller.enqueue({
|
|
830
|
+
type: "text-start",
|
|
831
|
+
id: "message"
|
|
832
|
+
});
|
|
833
|
+
controller.enqueue({
|
|
834
|
+
type: "text-delta",
|
|
835
|
+
id: "message",
|
|
836
|
+
delta: markdown
|
|
837
|
+
});
|
|
838
|
+
controller.enqueue({
|
|
839
|
+
type: "text-end",
|
|
840
|
+
id: "message"
|
|
841
|
+
});
|
|
842
|
+
controller.close();
|
|
843
|
+
} });
|
|
844
|
+
}
|
|
845
|
+
//#endregion
|
|
846
|
+
//#region src/experimental/channels/surface.ts
|
|
847
|
+
function isSurfaceValue(value) {
|
|
848
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") return true;
|
|
849
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
850
|
+
if (Array.isArray(value)) return value.every(isSurfaceValue);
|
|
851
|
+
return typeof value === "object" && Object.values(value).every(isSurfaceValue);
|
|
852
|
+
}
|
|
853
|
+
/** Validate the common envelope before provider-specific address parsing. */
|
|
854
|
+
function isChannelMessageSurface(value) {
|
|
855
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
856
|
+
const surface = value;
|
|
857
|
+
return typeof surface.channelKey === "string" && surface.channelKey.length > 0 && surface.version === 1 && isSurfaceValue(surface.address) && typeof surface.label === "string" && surface.label.trim().length > 0;
|
|
858
|
+
}
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region src/experimental/channels/gateway.ts
|
|
861
|
+
var _channels = /* @__PURE__ */ new WeakMap();
|
|
862
|
+
var _agent = /* @__PURE__ */ new WeakMap();
|
|
863
|
+
var _onRoute = /* @__PURE__ */ new WeakMap();
|
|
864
|
+
var _ChannelGateway_brand = /* @__PURE__ */ new WeakSet();
|
|
865
|
+
/**
|
|
866
|
+
* The Worker's entry point for Channels. Authenticates and normalizes
|
|
867
|
+
* webhooks, resolves WebSocket upgrades, routes each to the agent object
|
|
868
|
+
* its route names, and sends to surfaces.
|
|
869
|
+
*
|
|
870
|
+
* The gateway is the trust boundary. It alone decides who a sender is and
|
|
871
|
+
* tells the agent, so an agent serving Channels must be reachable only
|
|
872
|
+
* through it: an app that forwards an upgrade to the agent some other way
|
|
873
|
+
* (another fetch handler, `routeAgentRequest`, RPC) lets the caller pick
|
|
874
|
+
* its own participant and conversation.
|
|
875
|
+
*
|
|
876
|
+
* The agent object is the authorization boundary. Whoever a Channel routes
|
|
877
|
+
* to an object may list, join, create, fork and reset every conversation in
|
|
878
|
+
* it. To keep people apart, route them to different objects; by default
|
|
879
|
+
* each participant gets an object of their own.
|
|
880
|
+
*/
|
|
881
|
+
var ChannelGateway = class {
|
|
882
|
+
constructor(options) {
|
|
883
|
+
_classPrivateMethodInitSpec(this, _ChannelGateway_brand);
|
|
884
|
+
_classPrivateFieldInitSpec(this, _channels, void 0);
|
|
885
|
+
_classPrivateFieldInitSpec(this, _agent, void 0);
|
|
886
|
+
_classPrivateFieldInitSpec(this, _onRoute, void 0);
|
|
887
|
+
_classPrivateFieldSet2(_channels, this, { ...options.channels });
|
|
888
|
+
for (const [channelKey, channel] of Object.entries(_classPrivateFieldGet2(_channels, this))) if ((channel.ingress || channel.emailIngress) && !channel.participant) throw new Error(`Channel "${channelKey}" takes ingress but has no participant; say who its senders are`);
|
|
889
|
+
_classPrivateFieldSet2(_agent, this, options.agent);
|
|
890
|
+
_classPrivateFieldSet2(_onRoute, this, options.onRoute);
|
|
891
|
+
}
|
|
892
|
+
/** Serve a request if it is for Channels, or return undefined. */
|
|
893
|
+
async fetch(request) {
|
|
894
|
+
if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") {
|
|
895
|
+
const web = await _assertClassBrand(_ChannelGateway_brand, this, _upgrade).call(this, request);
|
|
896
|
+
if (web) return web;
|
|
897
|
+
}
|
|
898
|
+
return _assertClassBrand(_ChannelGateway_brand, this, _webhook).call(this, request);
|
|
899
|
+
}
|
|
900
|
+
async handleEmail(email) {
|
|
901
|
+
for (const [channelKey, channel] of Object.entries(_classPrivateFieldGet2(_channels, this))) {
|
|
902
|
+
const ingress = channel.emailIngress;
|
|
903
|
+
if (!ingress) continue;
|
|
904
|
+
const result = await ingress.receive(email);
|
|
905
|
+
if (!result) continue;
|
|
906
|
+
for (const envelope of result.events) await _assertClassBrand(_ChannelGateway_brand, this, _dispatch).call(this, channelKey, channel, envelope);
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
/** Deliver through the configured Channel named by the surface. */
|
|
912
|
+
deliver(surface, message, options) {
|
|
913
|
+
return _assertClassBrand(_ChannelGateway_brand, this, _outbound).call(this, surface, (channel, destination) => {
|
|
914
|
+
if (channel.deliver) return channel.deliver(destination, message, options);
|
|
915
|
+
if (channel.stream) return channel.stream(destination, messageChunks(message.markdown), {
|
|
916
|
+
...message.title !== void 0 && { title: message.title },
|
|
917
|
+
...options
|
|
918
|
+
});
|
|
919
|
+
return Promise.resolve(unsupported("CHANNEL_DELIVERY_UNSUPPORTED", `Channel "${destination.channelKey}" does not support delivery`));
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Deliver a progressively generated answer to the Channel
|
|
924
|
+
* named by the surface.
|
|
925
|
+
*
|
|
926
|
+
* A Channel that can stream consumes the stream itself. A Channel that
|
|
927
|
+
* cannot never learns it was a stream, because the Host collects the answer
|
|
928
|
+
* and calls `deliver` once.
|
|
929
|
+
*/
|
|
930
|
+
async stream(surface, chunks, options = {}) {
|
|
931
|
+
if (!isChannelMessageSurface(surface)) {
|
|
932
|
+
await chunks.cancel().catch(() => {});
|
|
933
|
+
return invalidSurface();
|
|
934
|
+
}
|
|
935
|
+
let channel;
|
|
936
|
+
try {
|
|
937
|
+
channel = _assertClassBrand(_ChannelGateway_brand, this, _configuredChannel).call(this, surface.channelKey);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
await chunks.cancel().catch(() => {});
|
|
940
|
+
throw error;
|
|
941
|
+
}
|
|
942
|
+
if (!channel.stream && !channel.deliver) {
|
|
943
|
+
await chunks.cancel().catch(() => {});
|
|
944
|
+
return unsupported("CHANNEL_DELIVERY_UNSUPPORTED", `Channel "${surface.channelKey}" does not support delivery`);
|
|
945
|
+
}
|
|
946
|
+
if (channel.stream) return channel.stream(surface, chunks, options);
|
|
947
|
+
return collectAndDeliver(channel, surface, chunks, options);
|
|
948
|
+
}
|
|
949
|
+
/** Return the identity's configured Channel destination, when supported. */
|
|
950
|
+
contactSurface(identity) {
|
|
951
|
+
const surface = (Object.prototype.hasOwnProperty.call(_classPrivateFieldGet2(_channels, this), identity.channelKey) ? _classPrivateFieldGet2(_channels, this)[identity.channelKey] : void 0)?.contactSurface?.(identity);
|
|
952
|
+
return surface ? stampSurface(identity.channelKey, surface) : null;
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
/**
|
|
956
|
+
* Forward a WebSocket upgrade to its agent with the resolved identity.
|
|
957
|
+
* Any identity header the client sent is replaced. An upgrade no Channel
|
|
958
|
+
* takes is left to the Worker, header and all, so the Worker must not
|
|
959
|
+
* forward it to a Channels agent.
|
|
960
|
+
*/
|
|
961
|
+
async function _upgrade(request) {
|
|
962
|
+
for (const [channelKey, channel] of Object.entries(_classPrivateFieldGet2(_channels, this))) {
|
|
963
|
+
const upgrade = channel.upgrade;
|
|
964
|
+
const match = upgrade?.match(request);
|
|
965
|
+
if (!upgrade || !match) continue;
|
|
966
|
+
return _assertClassBrand(_ChannelGateway_brand, this, _forwardUpgrade).call(this, channelKey, upgrade, match, request);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
async function _forwardUpgrade(channelKey, upgrade, match, request) {
|
|
970
|
+
const source = `Channel "${channelKey}"`;
|
|
971
|
+
const participant = toParticipant(await upgrade.participant(request), source);
|
|
972
|
+
if (!participant) return new Response("Unauthorized", { status: 401 });
|
|
973
|
+
const route = checkRoute(await upgrade.route(request, participant), source);
|
|
974
|
+
if (route === null) return new Response("Forbidden", { status: 403 });
|
|
975
|
+
const identity = {
|
|
976
|
+
route,
|
|
977
|
+
...match.conversationId !== void 0 && { conversationId: match.conversationId },
|
|
978
|
+
participant
|
|
979
|
+
};
|
|
980
|
+
const headers = new Headers(request.headers);
|
|
981
|
+
headers.set(WEB_IDENTITY_HEADER, JSON.stringify(identity));
|
|
982
|
+
return _classPrivateFieldGet2(_agent, this).call(this, route).fetch(new Request(request, { headers }));
|
|
983
|
+
}
|
|
984
|
+
async function _webhook(request) {
|
|
985
|
+
for (const [channelKey, channel] of Object.entries(_classPrivateFieldGet2(_channels, this))) {
|
|
986
|
+
const ingress = channel.ingress;
|
|
987
|
+
if (!ingress) continue;
|
|
988
|
+
try {
|
|
989
|
+
const result = await ingress.receive(request);
|
|
990
|
+
if (!result) continue;
|
|
991
|
+
for (const envelope of result.events) await _assertClassBrand(_ChannelGateway_brand, this, _dispatch).call(this, channelKey, channel, envelope);
|
|
992
|
+
return result.response;
|
|
993
|
+
} catch {
|
|
994
|
+
return new Response("Failed to handle Channel event", { status: 500 });
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async function _outbound(surface, operation) {
|
|
999
|
+
if (!isChannelMessageSurface(surface)) return invalidSurface();
|
|
1000
|
+
return operation(_assertClassBrand(_ChannelGateway_brand, this, _configuredChannel).call(this, surface.channelKey), surface);
|
|
1001
|
+
}
|
|
1002
|
+
function _configuredChannel(channelKey) {
|
|
1003
|
+
const channel = Object.prototype.hasOwnProperty.call(_classPrivateFieldGet2(_channels, this), channelKey) ? _classPrivateFieldGet2(_channels, this)[channelKey] : void 0;
|
|
1004
|
+
if (!channel) throw new Error(`Channel message surface names unknown configured Channel key "${channelKey}"`);
|
|
1005
|
+
return channel;
|
|
1006
|
+
}
|
|
1007
|
+
async function _dispatch(channelKey, channel, envelope) {
|
|
1008
|
+
const rawEvent = envelope.event;
|
|
1009
|
+
const event = stampEvent(channelKey, rawEvent);
|
|
1010
|
+
const source = `Channel "${channelKey}"`;
|
|
1011
|
+
const participant = toParticipant(await channel.participant(event, envelope.raw), source);
|
|
1012
|
+
const route = participant ? checkRoute(channel.route ? await channel.route(event, envelope.raw, participant) : participantRoute(participant), source) : null;
|
|
1013
|
+
const dispatchId = await createDispatchId(channelKey, event.eventId);
|
|
1014
|
+
await _classPrivateFieldGet2(_onRoute, this)?.call(this, {
|
|
1015
|
+
channelKey,
|
|
1016
|
+
event,
|
|
1017
|
+
participant,
|
|
1018
|
+
route,
|
|
1019
|
+
dispatchId
|
|
1020
|
+
});
|
|
1021
|
+
if (!participant || route === null) return;
|
|
1022
|
+
if (!event.replySurface) throw new Error(`Channel "${channelKey}" produced an event without a reply surface`);
|
|
1023
|
+
await _classPrivateFieldGet2(_agent, this).call(this, route).receive(toInboundEvent(event, dispatchId), {
|
|
1024
|
+
route,
|
|
1025
|
+
participant,
|
|
1026
|
+
surface: event.replySurface
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
/** Check what an application's `route` callback returned. */
|
|
1030
|
+
function checkRoute(route, source) {
|
|
1031
|
+
if (route === void 0) throw new Error(`${source} route returned undefined; return null to ignore an event`);
|
|
1032
|
+
if (route !== null && typeof route !== "string") throw new Error(`${source} route must return a string or null`);
|
|
1033
|
+
return route;
|
|
1034
|
+
}
|
|
1035
|
+
/** The provider's event as an inbound event, keyed by its dispatch id. */
|
|
1036
|
+
function toInboundEvent(event, dispatchId) {
|
|
1037
|
+
if (event.type === "approval-response") return {
|
|
1038
|
+
type: "approval-response",
|
|
1039
|
+
eventId: dispatchId,
|
|
1040
|
+
approvalId: event.approvalId,
|
|
1041
|
+
approved: event.decision === "approve"
|
|
1042
|
+
};
|
|
1043
|
+
const { message } = event;
|
|
1044
|
+
const parts = [{
|
|
1045
|
+
type: "text",
|
|
1046
|
+
text: message.markdown ?? message.text
|
|
1047
|
+
}];
|
|
1048
|
+
for (const attachment of message.attachments ?? []) if (attachment.url && attachment.mediaType) parts.push({
|
|
1049
|
+
type: "file",
|
|
1050
|
+
url: attachment.url,
|
|
1051
|
+
mediaType: attachment.mediaType,
|
|
1052
|
+
...attachment.name !== void 0 && { filename: attachment.name }
|
|
1053
|
+
});
|
|
1054
|
+
return {
|
|
1055
|
+
type: "message",
|
|
1056
|
+
eventId: dispatchId,
|
|
1057
|
+
message: {
|
|
1058
|
+
id: dispatchId,
|
|
1059
|
+
role: "user",
|
|
1060
|
+
parts
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function invalidSurface() {
|
|
1065
|
+
return unsupported("CHANNEL_SURFACE_INVALID", "Cannot resolve an invalid Channel message surface");
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Serve a Channel that cannot stream by collecting the answer first.
|
|
1069
|
+
*
|
|
1070
|
+
* A generation that failed part-way still delivers what it produced, because
|
|
1071
|
+
* losing the partial answer helps nobody, but the result is downgraded to
|
|
1072
|
+
* `uncertain` since the reader received an incomplete answer.
|
|
1073
|
+
*/
|
|
1074
|
+
async function collectAndDeliver(channel, surface, stream, options) {
|
|
1075
|
+
const collected = await collectText(stream);
|
|
1076
|
+
if (collected.interrupted && collected.text.length === 0) return {
|
|
1077
|
+
status: "failed",
|
|
1078
|
+
retryable: false,
|
|
1079
|
+
error: {
|
|
1080
|
+
code: "CHANNEL_STREAM_INTERRUPTED",
|
|
1081
|
+
message: "The stream ended before producing any content to deliver"
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
const result = await channel.deliver(surface, {
|
|
1085
|
+
...options.title !== void 0 && { title: options.title },
|
|
1086
|
+
markdown: collected.text
|
|
1087
|
+
}, options.delivery ? { delivery: options.delivery } : void 0);
|
|
1088
|
+
if (!collected.interrupted || result.status !== "delivered") return result;
|
|
1089
|
+
return {
|
|
1090
|
+
status: "uncertain",
|
|
1091
|
+
...result.reference !== void 0 && { reference: result.reference },
|
|
1092
|
+
error: {
|
|
1093
|
+
code: "CHANNEL_STREAM_INTERRUPTED",
|
|
1094
|
+
message: "An incomplete answer was delivered because the stream ended early"
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function stampSurface(channelKey, surface) {
|
|
1099
|
+
return {
|
|
1100
|
+
...surface,
|
|
1101
|
+
channelKey
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
function stampIdentity(channelKey, identity) {
|
|
1105
|
+
return {
|
|
1106
|
+
...identity,
|
|
1107
|
+
channelKey
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function stampEvent(channelKey, event) {
|
|
1111
|
+
return {
|
|
1112
|
+
...event,
|
|
1113
|
+
...event.replySurface && { replySurface: stampSurface(channelKey, event.replySurface) },
|
|
1114
|
+
...event.actor && { actor: {
|
|
1115
|
+
...event.actor,
|
|
1116
|
+
...event.actor.identity && { identity: stampIdentity(channelKey, event.actor.identity) }
|
|
1117
|
+
} }
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
/** Hash an unambiguous tuple so dispatch identities remain safe to carry. */
|
|
1121
|
+
async function createDispatchId(channelKey, eventId) {
|
|
1122
|
+
const identity = new TextEncoder().encode(JSON.stringify([channelKey, eventId]));
|
|
1123
|
+
const digest = await crypto.subtle.digest("SHA-256", identity);
|
|
1124
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
1125
|
+
}
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/experimental/channels/identity.ts
|
|
1128
|
+
const DEFAULT_IDENTITY_SCOPE = "default";
|
|
1129
|
+
function normalizeIdentity(identity) {
|
|
1130
|
+
return {
|
|
1131
|
+
channelKey: identity.channelKey,
|
|
1132
|
+
scope: identity.scope ?? DEFAULT_IDENTITY_SCOPE,
|
|
1133
|
+
subject: identity.subject
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
/** Build a stable key for comparing or indexing a Channel identity. */
|
|
1137
|
+
function identityKey(identity) {
|
|
1138
|
+
const normalized = normalizeIdentity(identity);
|
|
1139
|
+
return JSON.stringify([
|
|
1140
|
+
normalized.channelKey,
|
|
1141
|
+
normalized.scope,
|
|
1142
|
+
normalized.subject
|
|
1143
|
+
]);
|
|
1144
|
+
}
|
|
1145
|
+
//#endregion
|
|
1146
|
+
//#region src/experimental/channels/routes.ts
|
|
1147
|
+
/**
|
|
1148
|
+
* Common deterministic routing policies for normalized Channel events.
|
|
1149
|
+
*
|
|
1150
|
+
* Each one maps an event to a namespaced agent object. That object is the
|
|
1151
|
+
* authorization boundary: anyone routed to it may use every conversation in
|
|
1152
|
+
* it. Deciding whether an event is relevant at all is application policy:
|
|
1153
|
+
* write that in your own `route` function and return `null` to ignore it.
|
|
1154
|
+
*/
|
|
1155
|
+
const routes = {
|
|
1156
|
+
/**
|
|
1157
|
+
* Give each participant an agent object of their own. The default for every
|
|
1158
|
+
* Channel, so that a group chat never shares an object by accident.
|
|
1159
|
+
*
|
|
1160
|
+
* A surface that names no conversation, such as a Slack thread or an
|
|
1161
|
+
* email, joins the object's default conversation, so a participant keeps
|
|
1162
|
+
* one history wherever they write from.
|
|
1163
|
+
*/
|
|
1164
|
+
perParticipant(_event, _raw, participant) {
|
|
1165
|
+
return participantRoute(participant);
|
|
1166
|
+
},
|
|
1167
|
+
/**
|
|
1168
|
+
* Give every event its own application route. Useful to kick off a
|
|
1169
|
+
* new conversation for each ingress, and where you don't want subsequent
|
|
1170
|
+
* messages routed back to that same conversation.
|
|
1171
|
+
**/
|
|
1172
|
+
perEvent(event) {
|
|
1173
|
+
return `event:${event.eventId}`;
|
|
1174
|
+
},
|
|
1175
|
+
/**
|
|
1176
|
+
* Send every event in the same thread to the same object, whoever sent
|
|
1177
|
+
* it. Everyone in the thread can then use that object's conversations.
|
|
1178
|
+
*/
|
|
1179
|
+
perThread(event) {
|
|
1180
|
+
return `thread:${event.thread.id}`;
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
//#endregion
|
|
1184
|
+
export { ChannelGateway, Channels, applyChunks, consumeChunks, identityKey, isChannelMessageSurface, matchesPath, routes };
|
|
1185
|
+
|
|
1186
|
+
//# sourceMappingURL=index.js.map
|