agents 0.26.0 → 0.28.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/abortable-C1SpfVoi.js +21 -0
- package/dist/abortable-C1SpfVoi.js.map +1 -0
- package/dist/{agent-routing-0DR0Tk2v.d.ts → agent-routing-QDcchcQP.d.ts} +16 -16
- 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-DHL2XI7u.d.ts} +2 -2
- package/dist/agent-tools.d.ts +1 -1
- package/dist/ai-ahruGw7e.d.ts +239 -0
- package/dist/ai-sdk-Bdsf6mSA.js +2444 -0
- package/dist/ai-sdk-Bdsf6mSA.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.d.ts +8 -227
- package/dist/browser/ai.js +259 -1
- package/dist/browser/ai.js.map +1 -0
- package/dist/browser/index.d.ts +56 -46
- package/dist/browser/index.js +4 -361
- package/dist/browser/{ai-sdk.d.ts → tools/ai-sdk.d.ts} +7 -7
- package/dist/browser/{ai-sdk.js → tools/ai-sdk.js} +6 -5
- package/dist/browser/tools/ai-sdk.js.map +1 -0
- package/dist/browser/tools/pi.d.ts +111 -0
- package/dist/browser/tools/pi.js +119 -0
- package/dist/browser/tools/pi.js.map +1 -0
- package/dist/browser/{tanstack-ai.d.ts → tools/tanstack-ai.d.ts} +39 -37
- package/dist/browser/{tanstack-ai.js → tools/tanstack-ai.js} +73 -59
- package/dist/browser/tools/tanstack-ai.js.map +1 -0
- package/dist/browser-CEdNlsaD.js +481 -0
- package/dist/browser-CEdNlsaD.js.map +1 -0
- package/dist/{browser-DE3d-RLk.d.ts → browser-D30VlpcA.d.ts} +63 -37
- package/dist/{browser-tool-CQ10e6Kj.js → browser-tool-4iomavs_.js} +198 -62
- package/dist/browser-tool-4iomavs_.js.map +1 -0
- package/dist/{browser-tool-DuNdlwZ9.d.ts → browser-tool-vtrPOV8e.d.ts} +25 -6
- package/dist/{capability-B4WbF81e.js → capability-DBAKc6UZ.js} +1 -3
- package/dist/capability-DBAKc6UZ.js.map +1 -0
- package/dist/{capability-runner-DBAnGr6L.d.ts → capability-runner-BiVUtjqC.d.ts} +9 -7
- 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-je3koD9Z.d.ts → client-BGIQKJhX.d.ts} +2 -2
- package/dist/{client-fyQrLGkD.js → client-BnYbNT3z.js} +29 -5
- package/dist/client-BnYbNT3z.js.map +1 -0
- package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
- package/dist/client.d.ts +1 -1
- package/dist/{connector-58XDGvc3.js → connector-CtZylXdk.js} +3 -603
- package/dist/connector-CtZylXdk.js.map +1 -0
- package/dist/{connector-Cl-VqPmA.d.ts → connector-DEXzVoaY.d.ts} +9 -374
- package/dist/conversations-FjHDRZyD.d.ts +192 -0
- package/dist/{current-agent-vAk_5JMt.d.ts → current-agent-CKgT5d1O.d.ts} +3 -5
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- 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-DOI_bzCH.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-BFO6vRDn.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/harness/pi/index.d.ts +3 -2
- package/dist/harness/pi/index.js +3 -3
- 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 +2721 -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-BEL3ZV-s.d.ts} +2 -6
- package/dist/index-CBt8oRJt.d.ts +198 -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-BQykTEp_.js +20 -0
- package/dist/input-schema-BQykTEp_.js.map +1 -0
- package/dist/input-schema-D2QAtHZH.js +23 -0
- package/dist/input-schema-D2QAtHZH.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/lifecycle/index.js +2 -2
- package/dist/{lifecycle-CFu6OV03.js → lifecycle-DGorpAmA.js} +2 -4
- package/dist/lifecycle-DGorpAmA.js.map +1 -0
- 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 +9 -7
- package/dist/models/pi-ai/index.js +2 -1
- package/dist/models/pi-ai/index.js.map +1 -1
- package/dist/options-CiIsJHXR.d.ts +130 -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/queue/index.js +1 -1
- package/dist/{queue-BB-zfIYF.js → queue-CcIjVYTE.js} +2 -2
- package/dist/{queue-BB-zfIYF.js.map → queue-CcIjVYTE.js.map} +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/routing/index.d.ts +2 -2
- package/dist/routing/index.js +1 -1
- package/dist/{scheduler-RdACNXDP.d.ts → scheduler-BCLESHSC.d.ts} +2 -12
- package/dist/{scheduler-DZ3xQfeK.js → scheduler-FIHv2-kJ.js} +2 -4
- package/dist/scheduler-FIHv2-kJ.js.map +1 -0
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/session-store-Do4xkDcf.js +606 -0
- package/dist/session-store-Do4xkDcf.js.map +1 -0
- package/dist/session-store-DoPY12xM.d.ts +383 -0
- package/dist/sessions/index.d.ts +11 -434
- package/dist/sessions/index.js +1 -2271
- package/dist/sessions-_vN5BEZX.js +2272 -0
- package/dist/sessions-_vN5BEZX.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-CQxJFv4l.js} +8 -8
- package/dist/src-CQxJFv4l.js.map +1 -0
- 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/state/index.js +1 -3
- package/dist/state/index.js.map +1 -1
- package/dist/store-BKM4Xm9M.d.ts +332 -0
- package/dist/store-CslVzfN7.js +458 -0
- package/dist/store-CslVzfN7.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-lsHMJhRW.js} +17 -4
- package/dist/streams-lsHMJhRW.js.map +1 -0
- package/dist/{streams-Cmkj9QfW.d.ts → streams-vrCtbUIn.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-BN_5AWqI.js} +3 -2
- package/dist/tasks-BN_5AWqI.js.map +1 -0
- package/dist/{tasks-9YxYtByh.d.ts → tasks-E89KdwkS.d.ts} +2 -2
- package/dist/tool-CTp72Jb_.d.ts +278 -0
- package/dist/tool-CxKsvxjm.js +1126 -0
- package/dist/tool-CxKsvxjm.js.map +1 -0
- package/dist/tool-DoVberpN.d.ts +301 -0
- package/dist/tool-cQRkwSZZ.js +410 -0
- package/dist/tool-cQRkwSZZ.js.map +1 -0
- package/dist/tool-helpers-O5BDVEAY.js +58 -0
- package/dist/tool-helpers-O5BDVEAY.js.map +1 -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/web/fetch/index.d.ts +89 -0
- package/dist/web/fetch/index.js +20 -0
- package/dist/web/fetch/index.js.map +1 -0
- package/dist/web/fetch/tools/ai-sdk.d.ts +80 -0
- package/dist/web/fetch/tools/ai-sdk.js +47 -0
- package/dist/web/fetch/tools/ai-sdk.js.map +1 -0
- package/dist/web/fetch/tools/pi.d.ts +84 -0
- package/dist/web/fetch/tools/pi.js +88 -0
- package/dist/web/fetch/tools/pi.js.map +1 -0
- package/dist/web/fetch/tools/tanstack-ai.d.ts +78 -0
- package/dist/web/fetch/tools/tanstack-ai.js +45 -0
- package/dist/web/fetch/tools/tanstack-ai.js.map +1 -0
- package/dist/web/search/index.d.ts +58 -0
- package/dist/web/search/index.js +2 -0
- package/dist/web/search/tools/ai-sdk.d.ts +77 -0
- package/dist/web/search/tools/ai-sdk.js +46 -0
- package/dist/web/search/tools/ai-sdk.js.map +1 -0
- package/dist/web/search/tools/pi.d.ts +82 -0
- package/dist/web/search/tools/pi.js +89 -0
- package/dist/web/search/tools/pi.js.map +1 -0
- package/dist/web/search/tools/tanstack-ai.d.ts +71 -0
- package/dist/web/search/tools/tanstack-ai.js +46 -0
- package/dist/web/search/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-C3zy8YR0.js} +20 -8
- package/dist/websockets-C3zy8YR0.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 +29 -1
- package/docs/fetch-the-web.md +276 -0
- package/docs/harnesses/pi.md +25 -0
- package/docs/harnesses/think.md +215 -0
- package/docs/index.md +99 -75
- package/docs/lifecycle.md +42 -3
- package/docs/queue.md +2 -3
- package/docs/scheduling.md +2 -6
- package/docs/search-the-web.md +135 -0
- package/package.json +148 -44
- package/dist/ai-fm7xx1BK.js +0 -312
- package/dist/ai-fm7xx1BK.js.map +0 -1
- package/dist/browser/ai-sdk.js.map +0 -1
- package/dist/browser/index.js.map +0 -1
- package/dist/browser/tanstack-ai.js.map +0 -1
- package/dist/browser-tool-CQ10e6Kj.js.map +0 -1
- package/dist/capability-B4WbF81e.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/client-fyQrLGkD.js.map +0 -1
- package/dist/connector-58XDGvc3.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/lifecycle-CFu6OV03.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/scheduler-DZ3xQfeK.js.map +0 -1
- package/dist/sessions/index.js.map +0 -1
- package/dist/src-BZeu-SCD.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,2721 @@
|
|
|
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 { n as bindLifecycleCapability, t as LifecycleCapability } from "../../capability-DBAKc6UZ.js";
|
|
4
|
+
import { n as STREAM_RESUME_NONE_REASONS, r as StreamAccumulator, t as CHAT_MESSAGE_TYPES } from "../../protocol-eXeJxhgu.js";
|
|
5
|
+
import { n as Streams } from "../../streams-lsHMJhRW.js";
|
|
6
|
+
import { i as createToolsFromClientSchemas, n as iterateWithStallWatchdog, r as parseProtocolMessage, t as ChatStreamStalledError } from "../../stall-watchdog-BpRF2uwG.js";
|
|
7
|
+
import { t as Sessions } from "../../sessions-_vN5BEZX.js";
|
|
8
|
+
import { a as toTranscriptMessage, r as toResponseChunk, t as answerToolCall } from "../../turns-DCV3ycDl.js";
|
|
9
|
+
import { n as openHarnessStore } from "../../store-CslVzfN7.js";
|
|
10
|
+
import { convertToModelMessages, stepCountIs, streamText, toUIMessageStream } from "ai";
|
|
11
|
+
//#region src/harness/think/events.ts
|
|
12
|
+
var _listeners = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
var _observers = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
/** Fan-out of session events to listeners, per session. */
|
|
15
|
+
var SessionEvents = class {
|
|
16
|
+
constructor() {
|
|
17
|
+
_classPrivateFieldInitSpec(this, _listeners, /* @__PURE__ */ new Map());
|
|
18
|
+
_classPrivateFieldInitSpec(this, _observers, /* @__PURE__ */ new Set());
|
|
19
|
+
}
|
|
20
|
+
/** Listen to every session. Returns the unsubscribe. */
|
|
21
|
+
observe(listener) {
|
|
22
|
+
_classPrivateFieldGet2(_observers, this).add(listener);
|
|
23
|
+
return () => {
|
|
24
|
+
_classPrivateFieldGet2(_observers, this).delete(listener);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Listen to one session. Returns the unsubscribe. */
|
|
28
|
+
subscribe(session, listener) {
|
|
29
|
+
let listeners = _classPrivateFieldGet2(_listeners, this).get(session);
|
|
30
|
+
if (!listeners) {
|
|
31
|
+
listeners = /* @__PURE__ */ new Set();
|
|
32
|
+
_classPrivateFieldGet2(_listeners, this).set(session, listeners);
|
|
33
|
+
}
|
|
34
|
+
listeners.add(listener);
|
|
35
|
+
return () => {
|
|
36
|
+
listeners.delete(listener);
|
|
37
|
+
if (listeners.size === 0) _classPrivateFieldGet2(_listeners, this).delete(session);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Deliver an event to a session's listeners. A throwing listener is logged. */
|
|
41
|
+
emit(session, event) {
|
|
42
|
+
for (const observer of _classPrivateFieldGet2(_observers, this)) try {
|
|
43
|
+
observer(session, event);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("A ThinkHarness observer failed", error);
|
|
46
|
+
}
|
|
47
|
+
for (const listener of _classPrivateFieldGet2(_listeners, this).get(session) ?? []) try {
|
|
48
|
+
listener(event);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("A ThinkHarness session listener failed", error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var _close = /* @__PURE__ */ new WeakMap();
|
|
55
|
+
var _listener = /* @__PURE__ */ new WeakMap();
|
|
56
|
+
var _queue = /* @__PURE__ */ new WeakMap();
|
|
57
|
+
var _onStop = /* @__PURE__ */ new WeakMap();
|
|
58
|
+
var _draining = /* @__PURE__ */ new WeakMap();
|
|
59
|
+
var _stopped = /* @__PURE__ */ new WeakMap();
|
|
60
|
+
var _Watcher_brand = /* @__PURE__ */ new WeakSet();
|
|
61
|
+
/** One watch: events queue until `start`, then reach the listener in order. */
|
|
62
|
+
var Watcher = class {
|
|
63
|
+
constructor(state, onStop) {
|
|
64
|
+
_classPrivateMethodInitSpec(this, _Watcher_brand);
|
|
65
|
+
_classPrivateFieldInitSpec(this, _close, () => {});
|
|
66
|
+
_classPrivateFieldInitSpec(this, _listener, void 0);
|
|
67
|
+
_classPrivateFieldInitSpec(this, _queue, []);
|
|
68
|
+
_classPrivateFieldInitSpec(this, _onStop, void 0);
|
|
69
|
+
_classPrivateFieldInitSpec(this, _draining, false);
|
|
70
|
+
_classPrivateFieldInitSpec(this, _stopped, false);
|
|
71
|
+
this.state = state;
|
|
72
|
+
_classPrivateFieldSet2(_onStop, this, onStop);
|
|
73
|
+
this.closed = new Promise((resolve) => {
|
|
74
|
+
_classPrivateFieldSet2(_close, this, resolve);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Start delivering queued and later events. */
|
|
78
|
+
start(listener) {
|
|
79
|
+
_classPrivateFieldSet2(_listener, this, listener);
|
|
80
|
+
_assertClassBrand(_Watcher_brand, this, _drain).call(this);
|
|
81
|
+
}
|
|
82
|
+
/** Stop the watch. */
|
|
83
|
+
async stop() {
|
|
84
|
+
if (_classPrivateFieldGet2(_stopped, this)) return;
|
|
85
|
+
_classPrivateFieldSet2(_stopped, this, true);
|
|
86
|
+
_classPrivateFieldGet2(_onStop, this).call(this);
|
|
87
|
+
_classPrivateFieldGet2(_close, this).call(this);
|
|
88
|
+
}
|
|
89
|
+
/** Queue events for the listener. */
|
|
90
|
+
push(events) {
|
|
91
|
+
if (_classPrivateFieldGet2(_stopped, this) || events.length === 0) return;
|
|
92
|
+
_classPrivateFieldGet2(_queue, this).push(events);
|
|
93
|
+
_assertClassBrand(_Watcher_brand, this, _drain).call(this);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
async function _drain() {
|
|
97
|
+
if (!_classPrivateFieldGet2(_listener, this) || _classPrivateFieldGet2(_draining, this)) return;
|
|
98
|
+
_classPrivateFieldSet2(_draining, this, true);
|
|
99
|
+
try {
|
|
100
|
+
for (;;) {
|
|
101
|
+
const events = _classPrivateFieldGet2(_queue, this).shift();
|
|
102
|
+
if (!events || _classPrivateFieldGet2(_stopped, this)) return;
|
|
103
|
+
try {
|
|
104
|
+
await _classPrivateFieldGet2(_listener, this).call(this, events);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error("A ThinkHarness session watcher failed", error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
_classPrivateFieldSet2(_draining, this, false);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/harness/think/records.ts
|
|
115
|
+
/** The shared store's table prefix for ThinkHarness's records. */
|
|
116
|
+
const STORE_PREFIX = "cf_think_harness_store_";
|
|
117
|
+
/** The tables ThinkHarness kept its records in before the shared store. */
|
|
118
|
+
const LEGACY_SESSIONS = "cf_think_harness_sessions";
|
|
119
|
+
const LEGACY_OPERATIONS = "cf_think_harness_operations";
|
|
120
|
+
const LEGACY_TOOL_CALLS = "cf_think_harness_tool_calls";
|
|
121
|
+
const OPEN$1 = ["queued", "running"];
|
|
122
|
+
const EMPTY_STATE = {
|
|
123
|
+
clientTools: [],
|
|
124
|
+
toolCalls: {}
|
|
125
|
+
};
|
|
126
|
+
function isRecord$1(value) {
|
|
127
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
function stringOrNull(value) {
|
|
130
|
+
return typeof value === "string" ? value : null;
|
|
131
|
+
}
|
|
132
|
+
function count(value) {
|
|
133
|
+
return typeof value === "number" ? value : 0;
|
|
134
|
+
}
|
|
135
|
+
function parseMeta(value) {
|
|
136
|
+
const fields = isRecord$1(value) ? value : {};
|
|
137
|
+
return {
|
|
138
|
+
source: fields.source === "client" ? "client" : "server",
|
|
139
|
+
parentId: stringOrNull(fields.parentId),
|
|
140
|
+
messageId: stringOrNull(fields.messageId),
|
|
141
|
+
streamId: stringOrNull(fields.streamId),
|
|
142
|
+
pendingModel: fields.pendingModel === true,
|
|
143
|
+
steps: count(fields.steps),
|
|
144
|
+
interruptions: count(fields.interruptions),
|
|
145
|
+
overflowRetries: count(fields.overflowRetries),
|
|
146
|
+
abandonReason: stringOrNull(fields.abandonReason)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function newMeta(source) {
|
|
150
|
+
return {
|
|
151
|
+
source,
|
|
152
|
+
parentId: null,
|
|
153
|
+
messageId: null,
|
|
154
|
+
streamId: null,
|
|
155
|
+
pendingModel: false,
|
|
156
|
+
steps: 0,
|
|
157
|
+
interruptions: 0,
|
|
158
|
+
overflowRetries: 0,
|
|
159
|
+
abandonReason: null
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function metaJson(meta) {
|
|
163
|
+
return { ...meta };
|
|
164
|
+
}
|
|
165
|
+
function withProgress(meta, change) {
|
|
166
|
+
return {
|
|
167
|
+
source: meta.source,
|
|
168
|
+
parentId: change.parentId === void 0 ? meta.parentId : change.parentId,
|
|
169
|
+
messageId: change.messageId === void 0 ? meta.messageId : change.messageId,
|
|
170
|
+
streamId: change.streamId === void 0 ? meta.streamId : change.streamId,
|
|
171
|
+
pendingModel: change.pendingModel ?? meta.pendingModel,
|
|
172
|
+
steps: change.steps ?? meta.steps,
|
|
173
|
+
interruptions: change.interruptions ?? meta.interruptions,
|
|
174
|
+
overflowRetries: change.overflowRetries ?? meta.overflowRetries,
|
|
175
|
+
abandonReason: change.abandonReason === void 0 ? meta.abandonReason : change.abandonReason
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function inputJson(input) {
|
|
179
|
+
return input;
|
|
180
|
+
}
|
|
181
|
+
function parseInput(value) {
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
function parseText(result) {
|
|
185
|
+
return isRecord$1(result) && typeof result.text === "string" ? result.text : void 0;
|
|
186
|
+
}
|
|
187
|
+
function parseSessionState(value) {
|
|
188
|
+
if (!isRecord$1(value)) return EMPTY_STATE;
|
|
189
|
+
const toolCalls = Object.create(null);
|
|
190
|
+
const calls = value.toolCalls;
|
|
191
|
+
if (isRecord$1(calls)) {
|
|
192
|
+
for (const [toolCallId, call] of Object.entries(calls)) if (isRecord$1(call) && typeof call.operationId === "string" && typeof call.attempts === "number") toolCalls[toolCallId] = {
|
|
193
|
+
operationId: call.operationId,
|
|
194
|
+
attempts: call.attempts
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
clientTools: Array.isArray(value.clientTools) ? value.clientTools : [],
|
|
199
|
+
toolCalls
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function sessionStateJson(state) {
|
|
203
|
+
return {
|
|
204
|
+
clientTools: state.clientTools,
|
|
205
|
+
toolCalls: { ...state.toolCalls }
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function parseOperation(stored) {
|
|
209
|
+
const meta = parseMeta(stored.meta);
|
|
210
|
+
return {
|
|
211
|
+
session: stored.session,
|
|
212
|
+
operationId: stored.id,
|
|
213
|
+
seq: stored.seq,
|
|
214
|
+
input: parseInput(stored.input),
|
|
215
|
+
status: stored.status,
|
|
216
|
+
source: meta.source,
|
|
217
|
+
parentId: meta.parentId ?? void 0,
|
|
218
|
+
messageId: meta.messageId ?? void 0,
|
|
219
|
+
streamId: meta.streamId ?? void 0,
|
|
220
|
+
pendingModel: meta.pendingModel,
|
|
221
|
+
steps: meta.steps,
|
|
222
|
+
interruptions: meta.interruptions,
|
|
223
|
+
overflowRetries: meta.overflowRetries,
|
|
224
|
+
reason: stored.reason,
|
|
225
|
+
text: stored.status === "done" ? parseText(stored.result) : void 0,
|
|
226
|
+
abandonReason: meta.abandonReason ?? void 0,
|
|
227
|
+
createdAt: stored.createdAt
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
var _store = /* @__PURE__ */ new WeakMap();
|
|
231
|
+
var _OperationRecords_brand = /* @__PURE__ */ new WeakSet();
|
|
232
|
+
/**
|
|
233
|
+
* The harness's records, over the shared harness store: sessions, the
|
|
234
|
+
* operation queue, and started tool calls. Opening it moves records an
|
|
235
|
+
* earlier version left in its own tables into the store.
|
|
236
|
+
*/
|
|
237
|
+
var OperationRecords = class {
|
|
238
|
+
/**
|
|
239
|
+
* Open the records, creating the store's tables and importing any legacy
|
|
240
|
+
* tables. Cheap after the first time: keep one per isolate.
|
|
241
|
+
*
|
|
242
|
+
* @param storage - The Durable Object's storage.
|
|
243
|
+
*/
|
|
244
|
+
constructor(storage) {
|
|
245
|
+
_classPrivateMethodInitSpec(this, _OperationRecords_brand);
|
|
246
|
+
_classPrivateFieldInitSpec(this, _store, void 0);
|
|
247
|
+
_classPrivateFieldSet2(_store, this, openHarnessStore(storage, { prefix: STORE_PREFIX }));
|
|
248
|
+
importLegacyTables(storage, _classPrivateFieldGet2(_store, this));
|
|
249
|
+
}
|
|
250
|
+
/** Record a session, unless it exists. */
|
|
251
|
+
ensureSession(id, parent) {
|
|
252
|
+
_classPrivateFieldGet2(_store, this).createSession({
|
|
253
|
+
id,
|
|
254
|
+
...parent !== void 0 && { parent },
|
|
255
|
+
state: sessionStateJson(EMPTY_STATE)
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/** Every session, oldest first. */
|
|
259
|
+
sessions() {
|
|
260
|
+
return _classPrivateFieldGet2(_store, this).sessions().map((session) => ({
|
|
261
|
+
id: session.id,
|
|
262
|
+
parent: session.parent,
|
|
263
|
+
createdAt: session.createdAt
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
/** The client tools a session's client last declared. */
|
|
267
|
+
clientTools(session) {
|
|
268
|
+
return [..._assertClassBrand(_OperationRecords_brand, this, _state).call(this, session).clientTools];
|
|
269
|
+
}
|
|
270
|
+
/** Replace the client tools a session's client declared. */
|
|
271
|
+
setClientTools(session, tools) {
|
|
272
|
+
_assertClassBrand(_OperationRecords_brand, this, _updateState).call(this, session, (state) => ({
|
|
273
|
+
...state,
|
|
274
|
+
clientTools: tools
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
/** Insert a queued operation. Returns false when the id already exists. */
|
|
278
|
+
insert(session, operationId, input, source) {
|
|
279
|
+
return _classPrivateFieldGet2(_store, this).enqueue({
|
|
280
|
+
session,
|
|
281
|
+
id: operationId,
|
|
282
|
+
input: inputJson(input),
|
|
283
|
+
meta: metaJson(newMeta(source))
|
|
284
|
+
}).accepted;
|
|
285
|
+
}
|
|
286
|
+
/** One operation. */
|
|
287
|
+
get(session, operationId) {
|
|
288
|
+
const stored = _classPrivateFieldGet2(_store, this).operation(session, operationId);
|
|
289
|
+
return stored ? parseOperation(stored) : void 0;
|
|
290
|
+
}
|
|
291
|
+
/** Unsettled operations, oldest first; every session when none is given. */
|
|
292
|
+
open(session) {
|
|
293
|
+
return _classPrivateFieldGet2(_store, this).operations({
|
|
294
|
+
...session !== void 0 && { session },
|
|
295
|
+
status: OPEN$1
|
|
296
|
+
}).map(parseOperation);
|
|
297
|
+
}
|
|
298
|
+
/** Change an operation's progress. */
|
|
299
|
+
update(session, operationId, change) {
|
|
300
|
+
_classPrivateFieldGet2(_store, this).transaction(() => {
|
|
301
|
+
const stored = _classPrivateFieldGet2(_store, this).operation(session, operationId);
|
|
302
|
+
if (!stored) return;
|
|
303
|
+
_classPrivateFieldGet2(_store, this).setOperationMeta(session, operationId, metaJson(withProgress(parseMeta(stored.meta), change)));
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Move a queued operation to running, with its progress so far.
|
|
308
|
+
* Returns false when it was not queued.
|
|
309
|
+
*/
|
|
310
|
+
start(session, operationId, change) {
|
|
311
|
+
return _classPrivateFieldGet2(_store, this).transaction(() => {
|
|
312
|
+
if (!_classPrivateFieldGet2(_store, this).start(session, operationId)) return false;
|
|
313
|
+
this.update(session, operationId, change);
|
|
314
|
+
return true;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Settle an open operation, with a last change to its progress. Returns
|
|
319
|
+
* false when it had already settled.
|
|
320
|
+
*/
|
|
321
|
+
settle(session, operationId, outcome, change = {}) {
|
|
322
|
+
return _classPrivateFieldGet2(_store, this).transaction(() => {
|
|
323
|
+
const settled = _classPrivateFieldGet2(_store, this).settle(session, operationId, outcome.status === "done" ? {
|
|
324
|
+
status: "done",
|
|
325
|
+
result: { text: outcome.text }
|
|
326
|
+
} : outcome);
|
|
327
|
+
if (settled) this.update(session, operationId, change);
|
|
328
|
+
return settled;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/** Forget every settled operation in a session, for a reset. */
|
|
332
|
+
deleteSettled(session) {
|
|
333
|
+
_classPrivateFieldGet2(_store, this).transaction(() => {
|
|
334
|
+
_classPrivateFieldGet2(_store, this).deleteSettled(session);
|
|
335
|
+
_assertClassBrand(_OperationRecords_brand, this, _updateState).call(this, session, (state) => ({
|
|
336
|
+
...state,
|
|
337
|
+
toolCalls: {}
|
|
338
|
+
}));
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/** The record of a started tool call, if it was started. */
|
|
342
|
+
toolCall(session, toolCallId) {
|
|
343
|
+
const { toolCalls } = _assertClassBrand(_OperationRecords_brand, this, _state).call(this, session);
|
|
344
|
+
if (!Object.hasOwn(toolCalls, toolCallId)) return void 0;
|
|
345
|
+
const call = toolCalls[toolCallId];
|
|
346
|
+
return call && {
|
|
347
|
+
session,
|
|
348
|
+
toolCallId,
|
|
349
|
+
...call
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/** Record that a tool call is starting. Returns the attempt number. */
|
|
353
|
+
startToolCall(session, toolCallId, operationId) {
|
|
354
|
+
const attempts = (this.toolCall(session, toolCallId)?.attempts ?? 0) + 1;
|
|
355
|
+
_assertClassBrand(_OperationRecords_brand, this, _updateState).call(this, session, (state) => ({
|
|
356
|
+
...state,
|
|
357
|
+
toolCalls: {
|
|
358
|
+
...state.toolCalls,
|
|
359
|
+
[toolCallId]: {
|
|
360
|
+
operationId,
|
|
361
|
+
attempts
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}));
|
|
365
|
+
return attempts;
|
|
366
|
+
}
|
|
367
|
+
/** Forget a tool call once its result is in the transcript. */
|
|
368
|
+
finishToolCall(session, toolCallId) {
|
|
369
|
+
_assertClassBrand(_OperationRecords_brand, this, _updateState).call(this, session, (state) => {
|
|
370
|
+
const { [toolCallId]: _finished, ...toolCalls } = state.toolCalls;
|
|
371
|
+
return {
|
|
372
|
+
...state,
|
|
373
|
+
toolCalls
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
function _state(session) {
|
|
379
|
+
return parseSessionState(_classPrivateFieldGet2(_store, this).session(session)?.state);
|
|
380
|
+
}
|
|
381
|
+
function _updateState(session, change) {
|
|
382
|
+
_classPrivateFieldGet2(_store, this).transaction(() => {
|
|
383
|
+
const stored = _classPrivateFieldGet2(_store, this).session(session);
|
|
384
|
+
if (!stored) return;
|
|
385
|
+
_classPrivateFieldGet2(_store, this).setSessionState(session, sessionStateJson(change(parseSessionState(stored.state))));
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
function parseJsonColumn(text) {
|
|
389
|
+
return JSON.parse(text);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Move the records an earlier version kept in its own tables into the
|
|
393
|
+
* store, then drop those tables, in one transaction: either every record
|
|
394
|
+
* moves or none does and the next start tries again. Operations keep their
|
|
395
|
+
* order, status and progress; sessions keep their creation time, client
|
|
396
|
+
* tools and started tool calls, so an operation that was in flight resumes
|
|
397
|
+
* and recovers as it would have.
|
|
398
|
+
*/
|
|
399
|
+
function importLegacyTables(storage, store) {
|
|
400
|
+
const sql = storage.sql;
|
|
401
|
+
const present = new Set(sql.exec(`SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (?, ?, ?)`, LEGACY_SESSIONS, LEGACY_OPERATIONS, LEGACY_TOOL_CALLS).toArray().map((row) => row.name));
|
|
402
|
+
if (present.size === 0) return;
|
|
403
|
+
storage.transactionSync(() => {
|
|
404
|
+
const states = /* @__PURE__ */ new Map();
|
|
405
|
+
if (present.has(LEGACY_SESSIONS)) {
|
|
406
|
+
const rows = sql.exec(`SELECT id, parent, client_tools, created_at FROM ${LEGACY_SESSIONS}
|
|
407
|
+
ORDER BY created_at, id`).toArray();
|
|
408
|
+
for (const row of rows) {
|
|
409
|
+
store.createSession({
|
|
410
|
+
id: row.id,
|
|
411
|
+
...row.parent !== null && { parent: row.parent },
|
|
412
|
+
createdAt: row.created_at
|
|
413
|
+
});
|
|
414
|
+
states.set(row.id, parseSessionState({
|
|
415
|
+
clientTools: row.client_tools === null ? [] : parseJsonColumn(row.client_tools),
|
|
416
|
+
toolCalls: {}
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (present.has(LEGACY_TOOL_CALLS)) {
|
|
421
|
+
const rows = sql.exec(`SELECT session_id, tool_call_id, operation_id, attempts
|
|
422
|
+
FROM ${LEGACY_TOOL_CALLS}`).toArray();
|
|
423
|
+
for (const row of rows) {
|
|
424
|
+
const state = states.get(row.session_id) ?? EMPTY_STATE;
|
|
425
|
+
states.set(row.session_id, {
|
|
426
|
+
...state,
|
|
427
|
+
toolCalls: {
|
|
428
|
+
...state.toolCalls,
|
|
429
|
+
[row.tool_call_id]: {
|
|
430
|
+
operationId: row.operation_id,
|
|
431
|
+
attempts: row.attempts
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
for (const [id, state] of states) {
|
|
438
|
+
store.createSession({ id });
|
|
439
|
+
store.setSessionState(id, sessionStateJson(state));
|
|
440
|
+
}
|
|
441
|
+
if (present.has(LEGACY_OPERATIONS)) {
|
|
442
|
+
const rows = sql.exec(`SELECT * FROM ${LEGACY_OPERATIONS} ORDER BY seq`).toArray();
|
|
443
|
+
for (const row of rows) importLegacyOperation(store, row);
|
|
444
|
+
}
|
|
445
|
+
for (const table of [
|
|
446
|
+
LEGACY_SESSIONS,
|
|
447
|
+
LEGACY_OPERATIONS,
|
|
448
|
+
LEGACY_TOOL_CALLS
|
|
449
|
+
]) sql.exec(`DROP TABLE IF EXISTS ${table}`);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function importLegacyOperation(store, row) {
|
|
453
|
+
const session = row.session_id;
|
|
454
|
+
const id = row.operation_id;
|
|
455
|
+
if (!store.session(session)) store.createSession({
|
|
456
|
+
id: session,
|
|
457
|
+
state: sessionStateJson(EMPTY_STATE)
|
|
458
|
+
});
|
|
459
|
+
const meta = {
|
|
460
|
+
source: row.source === "client" ? "client" : "server",
|
|
461
|
+
parentId: row.parent_id,
|
|
462
|
+
messageId: row.message_id,
|
|
463
|
+
streamId: row.stream_id,
|
|
464
|
+
pendingModel: row.pending_model === 1,
|
|
465
|
+
steps: row.steps,
|
|
466
|
+
interruptions: row.interruptions,
|
|
467
|
+
overflowRetries: row.overflow_retries,
|
|
468
|
+
abandonReason: row.abandon_reason ?? null
|
|
469
|
+
};
|
|
470
|
+
store.deleteOperation(session, id);
|
|
471
|
+
store.enqueue({
|
|
472
|
+
session,
|
|
473
|
+
id,
|
|
474
|
+
input: parseJsonColumn(row.input),
|
|
475
|
+
meta: metaJson(meta)
|
|
476
|
+
});
|
|
477
|
+
switch (row.status) {
|
|
478
|
+
case "queued": return;
|
|
479
|
+
case "running":
|
|
480
|
+
store.start(session, id);
|
|
481
|
+
return;
|
|
482
|
+
case "done":
|
|
483
|
+
store.settle(session, id, {
|
|
484
|
+
status: "done",
|
|
485
|
+
result: { text: row.text ?? "" }
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
488
|
+
case "unanswered":
|
|
489
|
+
store.settle(session, id, {
|
|
490
|
+
status: "unanswered",
|
|
491
|
+
reason: row.reason ?? "unanswered"
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
default: throw new Error(`Unknown ThinkHarness operation status ${row.status} in ${LEGACY_OPERATIONS}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/harness/think/tools.ts
|
|
499
|
+
/** The model-facing copy of a tool set, restricted to `activeTools` if given. */
|
|
500
|
+
function modelTools(tools, activeTools) {
|
|
501
|
+
const active = activeTools ? new Set(activeTools) : void 0;
|
|
502
|
+
const offered = {};
|
|
503
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
504
|
+
if (active && !active.has(name)) continue;
|
|
505
|
+
if (tool.type === "provider") {
|
|
506
|
+
offered[name] = tool;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const { execute: _execute, needsApproval: _needsApproval, ...definition } = tool;
|
|
510
|
+
if ("recovery" in definition) delete definition.recovery;
|
|
511
|
+
offered[name] = definition;
|
|
512
|
+
}
|
|
513
|
+
return offered;
|
|
514
|
+
}
|
|
515
|
+
/** Whether the harness runs this tool, as opposed to a client or provider. */
|
|
516
|
+
function isServerTool(tools, name) {
|
|
517
|
+
const tool = Object.hasOwn(tools, name) ? tools[name] : void 0;
|
|
518
|
+
return tool !== void 0 && tool.type !== "provider" && typeof tool.execute === "function";
|
|
519
|
+
}
|
|
520
|
+
/** Whether a tool's own `needsApproval` asks for approval of this call. */
|
|
521
|
+
async function toolNeedsApproval(tool, input, options) {
|
|
522
|
+
const needsApproval = tool.needsApproval;
|
|
523
|
+
if (typeof needsApproval === "function") return Boolean(await needsApproval(input, {
|
|
524
|
+
...options,
|
|
525
|
+
context: void 0
|
|
526
|
+
}));
|
|
527
|
+
return needsApproval === true;
|
|
528
|
+
}
|
|
529
|
+
function isAsyncIterable(value) {
|
|
530
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Run one tool's `execute`. A streaming tool's outputs before its last are
|
|
534
|
+
* reported to `onPreliminary`; its last output is the result.
|
|
535
|
+
*/
|
|
536
|
+
async function executeTool(tool, input, options) {
|
|
537
|
+
const execute = tool.execute;
|
|
538
|
+
if (typeof execute !== "function") return {
|
|
539
|
+
ok: false,
|
|
540
|
+
errorText: "The tool has no execute function"
|
|
541
|
+
};
|
|
542
|
+
try {
|
|
543
|
+
const result = await execute(input, {
|
|
544
|
+
toolCallId: options.toolCallId,
|
|
545
|
+
messages: options.messages,
|
|
546
|
+
abortSignal: options.abortSignal,
|
|
547
|
+
context: void 0
|
|
548
|
+
});
|
|
549
|
+
if (!isAsyncIterable(result)) return {
|
|
550
|
+
ok: true,
|
|
551
|
+
output: result
|
|
552
|
+
};
|
|
553
|
+
let last;
|
|
554
|
+
let seen = false;
|
|
555
|
+
for await (const output of result) {
|
|
556
|
+
if (seen) options.onPreliminary(last);
|
|
557
|
+
last = output;
|
|
558
|
+
seen = true;
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
ok: true,
|
|
562
|
+
output: last
|
|
563
|
+
};
|
|
564
|
+
} catch (error) {
|
|
565
|
+
return {
|
|
566
|
+
ok: false,
|
|
567
|
+
errorText: error instanceof Error ? error.message : String(error)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/harness/think/turn.ts
|
|
573
|
+
/** Whether a part is a tool call. */
|
|
574
|
+
function isToolCallPart(part) {
|
|
575
|
+
return (part.type.startsWith("tool-") || part.type === "dynamic-tool") && "toolCallId" in part;
|
|
576
|
+
}
|
|
577
|
+
/** The tool name a tool call part carries. */
|
|
578
|
+
function toolNameOf(part) {
|
|
579
|
+
return part.type === "dynamic-tool" ? part.toolName : part.type.slice(5);
|
|
580
|
+
}
|
|
581
|
+
/** The parts of a message's last model step. */
|
|
582
|
+
function lastStep(message) {
|
|
583
|
+
let start = 0;
|
|
584
|
+
message.parts.forEach((part, index) => {
|
|
585
|
+
if (part.type === "step-start") start = index + 1;
|
|
586
|
+
});
|
|
587
|
+
return message.parts.slice(start);
|
|
588
|
+
}
|
|
589
|
+
/** Whether a tool call has its result, error or denial. */
|
|
590
|
+
function isSettled(part) {
|
|
591
|
+
return part.state === "output-available" || part.state === "output-error" || part.state === "output-denied";
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Decide the turn's next action.
|
|
595
|
+
*
|
|
596
|
+
* @param progress - The operation's durable progress.
|
|
597
|
+
* @param message - The turn's assistant message, once it has one.
|
|
598
|
+
* @param isServerTool - Whether the harness runs a tool of this name.
|
|
599
|
+
* @param maxSteps - Most model calls one operation makes.
|
|
600
|
+
* @returns What to do next.
|
|
601
|
+
*/
|
|
602
|
+
function nextAction(progress, message, isServerTool, maxSteps) {
|
|
603
|
+
if (progress.streamId !== void 0) return { _tag: "recover-model" };
|
|
604
|
+
if (message === void 0) return progress.pendingModel ? { _tag: "call-model" } : { _tag: "end" };
|
|
605
|
+
const runnable = [];
|
|
606
|
+
let awaiting = false;
|
|
607
|
+
for (const part of lastStep(message)) {
|
|
608
|
+
if (!isToolCallPart(part) || isSettled(part) || part.providerExecuted) continue;
|
|
609
|
+
const server = isServerTool(toolNameOf(part));
|
|
610
|
+
switch (part.state) {
|
|
611
|
+
case "input-available":
|
|
612
|
+
if (server) runnable.push(part);
|
|
613
|
+
else awaiting = true;
|
|
614
|
+
break;
|
|
615
|
+
case "approval-responded":
|
|
616
|
+
if (server && part.approval.approved) runnable.push(part);
|
|
617
|
+
else awaiting = true;
|
|
618
|
+
break;
|
|
619
|
+
case "approval-requested":
|
|
620
|
+
awaiting = true;
|
|
621
|
+
break;
|
|
622
|
+
case "input-streaming": break;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (runnable.length > 0) return {
|
|
626
|
+
_tag: "run-tools",
|
|
627
|
+
calls: runnable
|
|
628
|
+
};
|
|
629
|
+
if (awaiting) return { _tag: "await-input" };
|
|
630
|
+
if (progress.pendingModel && progress.steps < maxSteps) return { _tag: "call-model" };
|
|
631
|
+
return { _tag: "end" };
|
|
632
|
+
}
|
|
633
|
+
/** The text of a message's text parts. */
|
|
634
|
+
function textOf(message) {
|
|
635
|
+
if (!message) return "";
|
|
636
|
+
return message.parts.map((part) => part.type === "text" ? part.text : "").join("");
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* The partial message an interrupted model call leaves, made safe to keep:
|
|
640
|
+
* tool calls whose input never finished streaming are dropped, since the
|
|
641
|
+
* model never committed to them.
|
|
642
|
+
*/
|
|
643
|
+
function settledPartial(message) {
|
|
644
|
+
return {
|
|
645
|
+
...message,
|
|
646
|
+
parts: message.parts.filter((part) => !(isToolCallPart(part) && part.state === "input-streaming"))
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
/** Whether a message has anything besides step markers. */
|
|
650
|
+
function hasContent(message) {
|
|
651
|
+
return message.parts.some((part) => part.type !== "step-start");
|
|
652
|
+
}
|
|
653
|
+
//#endregion
|
|
654
|
+
//#region src/harness/think/harness.ts
|
|
655
|
+
/**
|
|
656
|
+
* Thrown by `submit()` when asked to steer a running turn, which
|
|
657
|
+
* ThinkHarness does not support. Submit without `whenBusy` to queue the
|
|
658
|
+
* input behind the running turn instead.
|
|
659
|
+
*
|
|
660
|
+
* @experimental
|
|
661
|
+
*/
|
|
662
|
+
var SteerNotSupportedError = class extends Error {
|
|
663
|
+
/** @param session - The session the submission was for. */
|
|
664
|
+
constructor(session) {
|
|
665
|
+
super("ThinkHarness does not support whenBusy: \"steer\". Submit without it to queue the input behind the running turn.");
|
|
666
|
+
this.session = session;
|
|
667
|
+
this._tag = "SteerNotSupportedError";
|
|
668
|
+
this.name = "SteerNotSupportedError";
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
/** The root session's id: the Sessions capability's default session. */
|
|
672
|
+
const ROOT_SESSION = "";
|
|
673
|
+
const WAKE_FN = "wake";
|
|
674
|
+
const HEARTBEAT_MS = 3e4;
|
|
675
|
+
const DEFAULT_MAX_STEPS = 10;
|
|
676
|
+
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
677
|
+
const DEFAULT_BACKOFF_MS = 1e3;
|
|
678
|
+
const MAX_BACKOFF_MS = 6e4;
|
|
679
|
+
const DEFAULT_STALL_TIMEOUT_MS = 12e4;
|
|
680
|
+
const MAX_OVERFLOW_RETRIES = 1;
|
|
681
|
+
/** Ceiling for one stored segment, under the 2 MB SQLite row limit. */
|
|
682
|
+
const STREAM_MAX_CHUNK_BYTES = 19e5;
|
|
683
|
+
/** Chunks packed into one stored stream segment. */
|
|
684
|
+
const SEGMENT_CHUNKS = 10;
|
|
685
|
+
/** Raw bytes packed into one stored segment before it is written. */
|
|
686
|
+
const SEGMENT_MAX_BYTES = 256e3;
|
|
687
|
+
/** Longest a streamed chunk waits in memory before it is written. */
|
|
688
|
+
const FLUSH_AFTER_MS = 100;
|
|
689
|
+
/** Segments read at a time when rebuilding an interrupted model call. */
|
|
690
|
+
const READ_PAGE = 50;
|
|
691
|
+
const INTERRUPTED_TOOL_ERROR = "The tool call was interrupted before it finished, because the agent restarted. It may or may not have taken effect.";
|
|
692
|
+
function wakeJobId(session) {
|
|
693
|
+
return `think-wake:${session}`;
|
|
694
|
+
}
|
|
695
|
+
function sessionOfJob(payload) {
|
|
696
|
+
return typeof payload === "object" && payload !== null && "session" in payload && typeof payload.session === "string" ? payload.session : void 0;
|
|
697
|
+
}
|
|
698
|
+
async function resolve(value, session) {
|
|
699
|
+
if (typeof value === "function") return value({ session });
|
|
700
|
+
return value;
|
|
701
|
+
}
|
|
702
|
+
function errorText(error) {
|
|
703
|
+
return error instanceof Error ? error.message : String(error);
|
|
704
|
+
}
|
|
705
|
+
function isToolAnswer(input) {
|
|
706
|
+
return typeof input === "object" && !Array.isArray(input) && "type" in input && (input.type === "approval" || input.type === "tool-result");
|
|
707
|
+
}
|
|
708
|
+
function isUIMessage(input) {
|
|
709
|
+
return typeof input === "object" && !Array.isArray(input) && "role" in input && "parts" in input;
|
|
710
|
+
}
|
|
711
|
+
function harnessInputMessage(input, id) {
|
|
712
|
+
return {
|
|
713
|
+
id: input.messageId ?? id,
|
|
714
|
+
role: "user",
|
|
715
|
+
parts: input.parts.map((part) => part.type === "text" ? {
|
|
716
|
+
type: "text",
|
|
717
|
+
text: part.text
|
|
718
|
+
} : {
|
|
719
|
+
type: "file",
|
|
720
|
+
mediaType: part.mediaType,
|
|
721
|
+
url: part.url,
|
|
722
|
+
...part.filename !== void 0 && { filename: part.filename }
|
|
723
|
+
})
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/** The stored form of an input. */
|
|
727
|
+
function operationInput(input, operationId, autoContinue) {
|
|
728
|
+
if (typeof input === "string") return {
|
|
729
|
+
kind: "messages",
|
|
730
|
+
messages: [{
|
|
731
|
+
id: operationId,
|
|
732
|
+
role: "user",
|
|
733
|
+
parts: [{
|
|
734
|
+
type: "text",
|
|
735
|
+
text: input
|
|
736
|
+
}]
|
|
737
|
+
}]
|
|
738
|
+
};
|
|
739
|
+
if (Array.isArray(input)) return {
|
|
740
|
+
kind: "messages",
|
|
741
|
+
messages: input
|
|
742
|
+
};
|
|
743
|
+
if (isToolAnswer(input)) return {
|
|
744
|
+
kind: "answer",
|
|
745
|
+
answer: input,
|
|
746
|
+
autoContinue
|
|
747
|
+
};
|
|
748
|
+
if (isUIMessage(input)) return {
|
|
749
|
+
kind: "messages",
|
|
750
|
+
messages: [input]
|
|
751
|
+
};
|
|
752
|
+
return {
|
|
753
|
+
kind: "messages",
|
|
754
|
+
messages: [harnessInputMessage(input, operationId)]
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function toolCallAnswer(answer) {
|
|
758
|
+
return answer.type === "approval" ? {
|
|
759
|
+
type: "approval-response",
|
|
760
|
+
approvalId: answer.approvalId,
|
|
761
|
+
approved: answer.approved,
|
|
762
|
+
...answer.reason !== void 0 && { reason: answer.reason }
|
|
763
|
+
} : {
|
|
764
|
+
type: "tool-result",
|
|
765
|
+
toolCallId: answer.toolCallId,
|
|
766
|
+
result: answer.result
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
/** Replace one tool call part of a message. */
|
|
770
|
+
function withToolPart(message, toolCallId, update) {
|
|
771
|
+
return {
|
|
772
|
+
...message,
|
|
773
|
+
parts: message.parts.map((part) => isToolCallPart(part) && part.toolCallId === toolCallId ? update(part) : part)
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function withOutcome(part, outcome) {
|
|
777
|
+
return outcome.ok ? {
|
|
778
|
+
...part,
|
|
779
|
+
state: "output-available",
|
|
780
|
+
output: outcome.output,
|
|
781
|
+
preliminary: void 0
|
|
782
|
+
} : {
|
|
783
|
+
...part,
|
|
784
|
+
state: "output-error",
|
|
785
|
+
errorText: outcome.errorText
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function asUIMessages(messages) {
|
|
789
|
+
return messages;
|
|
790
|
+
}
|
|
791
|
+
function asUIMessage(message) {
|
|
792
|
+
return message;
|
|
793
|
+
}
|
|
794
|
+
function asSessionMessage(message) {
|
|
795
|
+
return message;
|
|
796
|
+
}
|
|
797
|
+
var _options = /* @__PURE__ */ new WeakMap();
|
|
798
|
+
var _events = /* @__PURE__ */ new WeakMap();
|
|
799
|
+
var _drives = /* @__PURE__ */ new WeakMap();
|
|
800
|
+
var _live = /* @__PURE__ */ new WeakMap();
|
|
801
|
+
var _waiters = /* @__PURE__ */ new WeakMap();
|
|
802
|
+
var _configured = /* @__PURE__ */ new WeakMap();
|
|
803
|
+
var _streams = /* @__PURE__ */ new WeakMap();
|
|
804
|
+
var _sessions = /* @__PURE__ */ new WeakMap();
|
|
805
|
+
var _bound = /* @__PURE__ */ new WeakMap();
|
|
806
|
+
var _ownMessages = /* @__PURE__ */ new WeakMap();
|
|
807
|
+
var _ownClears = /* @__PURE__ */ new WeakMap();
|
|
808
|
+
var _heartbeatMs = /* @__PURE__ */ new WeakMap();
|
|
809
|
+
var _records = /* @__PURE__ */ new WeakMap();
|
|
810
|
+
var _ThinkHarness_brand = /* @__PURE__ */ new WeakSet();
|
|
811
|
+
/**
|
|
812
|
+
* Think's agent loop hosted in a Durable Object, behind the same small
|
|
813
|
+
* harness interface as `PiHarness`: `harness.prompt()`, `harness.sessions`,
|
|
814
|
+
* `harness.session(id)`. It implements the shared `AgentHarness`
|
|
815
|
+
* interface, so a Channels host can serve its sessions.
|
|
816
|
+
*
|
|
817
|
+
* Transcripts live in a Sessions capability and in-flight model output in
|
|
818
|
+
* a Streams capability, both owned by the harness, and the harness's queue
|
|
819
|
+
* of operations in its own table. The harness runs every server tool call itself, so after an
|
|
820
|
+
* eviction it knows exactly which call was cut short and applies that
|
|
821
|
+
* tool's recovery policy.
|
|
822
|
+
*
|
|
823
|
+
* Durability is the Lifecycle's job queue, used directly: one wake job per
|
|
824
|
+
* session. A submission is written, then the session's wake is scheduled.
|
|
825
|
+
* While a session has work, the wake job reschedules itself as a heartbeat,
|
|
826
|
+
* so an eviction leaves it due and its alarm restarts the object. Each step
|
|
827
|
+
* of a turn reads only durable state, so the restarted object continues
|
|
828
|
+
* where the last write left off.
|
|
829
|
+
*
|
|
830
|
+
* @experimental The API may change between releases.
|
|
831
|
+
*/
|
|
832
|
+
var ThinkHarness = class extends LifecycleCapability {
|
|
833
|
+
/**
|
|
834
|
+
* @param options - The model, tools and hooks.
|
|
835
|
+
*/
|
|
836
|
+
constructor(options) {
|
|
837
|
+
super("think-harness");
|
|
838
|
+
_classPrivateMethodInitSpec(this, _ThinkHarness_brand);
|
|
839
|
+
_classPrivateFieldInitSpec(this, _options, void 0);
|
|
840
|
+
_classPrivateFieldInitSpec(this, _events, new SessionEvents());
|
|
841
|
+
_classPrivateFieldInitSpec(this, _drives, /* @__PURE__ */ new Map());
|
|
842
|
+
_classPrivateFieldInitSpec(this, _live, /* @__PURE__ */ new Map());
|
|
843
|
+
_classPrivateFieldInitSpec(this, _waiters, /* @__PURE__ */ new Map());
|
|
844
|
+
_classPrivateFieldInitSpec(this, _configured, /* @__PURE__ */ new Set());
|
|
845
|
+
_classPrivateFieldInitSpec(this, _streams, new Streams({ maxChunkBytes: STREAM_MAX_CHUNK_BYTES }));
|
|
846
|
+
_classPrivateFieldInitSpec(this, _sessions, void 0);
|
|
847
|
+
_classPrivateFieldInitSpec(this, _bound, false);
|
|
848
|
+
_classPrivateFieldInitSpec(this, _ownMessages, /* @__PURE__ */ new Map());
|
|
849
|
+
_classPrivateFieldInitSpec(this, _ownClears, /* @__PURE__ */ new Map());
|
|
850
|
+
_classPrivateFieldInitSpec(this, _heartbeatMs, HEARTBEAT_MS);
|
|
851
|
+
_classPrivateFieldInitSpec(this, _records, void 0);
|
|
852
|
+
_classPrivateFieldSet2(_options, this, options);
|
|
853
|
+
_classPrivateFieldSet2(_sessions, this, new Sessions({ ...options.reservedMetadataKeys && { reservedMetadataKeys: options.reservedMetadataKeys } }));
|
|
854
|
+
this.sessions = new ThinkSessions(this);
|
|
855
|
+
}
|
|
856
|
+
/** Create the harness's tables and wake every session with open work. */
|
|
857
|
+
async onStart() {
|
|
858
|
+
_assertClassBrand(_ThinkHarness_brand, this, _bindOwned).call(this);
|
|
859
|
+
await _classPrivateFieldGet2(_sessions, this).onStart();
|
|
860
|
+
await _classPrivateFieldGet2(_streams, this).onStart();
|
|
861
|
+
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
|
|
862
|
+
store.ensureSession("");
|
|
863
|
+
const sessions = new Set(store.open().map((op) => op.session));
|
|
864
|
+
for (const session of sessions) {
|
|
865
|
+
const wake = this.lifecycle.jobs.get(wakeJobId(session));
|
|
866
|
+
if (wake && wake.time > Date.now()) continue;
|
|
867
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _wake).call(this, session);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* The alarm memory-limit breaker sealed: the work it stopped would only
|
|
872
|
+
* run out of memory again. Mark the running operations to be settled
|
|
873
|
+
* `unanswered` with reason `"out_of_memory"` rather than run again.
|
|
874
|
+
* Queued operations behind them still run. Runs after the isolate's
|
|
875
|
+
* work unwound, so it keeps to small synchronous writes.
|
|
876
|
+
*/
|
|
877
|
+
onMemoryLimit(context) {
|
|
878
|
+
if (!context.sealed) return;
|
|
879
|
+
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
|
|
880
|
+
for (const op of store.open()) {
|
|
881
|
+
if (op.status !== "running") continue;
|
|
882
|
+
store.update(op.session, op.operationId, { abandonReason: "out_of_memory" });
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/** Run a session's wake job. */
|
|
886
|
+
async onJob(context) {
|
|
887
|
+
if (context.job.fn !== WAKE_FN) return;
|
|
888
|
+
const session = sessionOfJob(context.job.payload);
|
|
889
|
+
if (session !== void 0) return _assertClassBrand(_ThinkHarness_brand, this, _wakeStep).call(this, session);
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* A handle on one session. No I/O until you call it.
|
|
893
|
+
*
|
|
894
|
+
* @param id - The session. Default: the root session.
|
|
895
|
+
*/
|
|
896
|
+
session(id = "") {
|
|
897
|
+
return new ThinkSession(this, id);
|
|
898
|
+
}
|
|
899
|
+
/** Submit to the root session and wait for the answer. */
|
|
900
|
+
prompt(input, options = {}) {
|
|
901
|
+
return this.session().prompt(input, options);
|
|
902
|
+
}
|
|
903
|
+
/** Durably submit to the root session. Resolves before the model runs. */
|
|
904
|
+
submit(input, options = {}) {
|
|
905
|
+
return this.session().submit(input, options);
|
|
906
|
+
}
|
|
907
|
+
/** Operations not settled yet, oldest first, in one session or all. */
|
|
908
|
+
async pending(session) {
|
|
909
|
+
await this.lifecycle.ready();
|
|
910
|
+
return _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session).map((op) => ({
|
|
911
|
+
operationId: op.operationId,
|
|
912
|
+
session: op.session,
|
|
913
|
+
status: op.status === "queued" ? "queued" : "running"
|
|
914
|
+
}));
|
|
915
|
+
}
|
|
916
|
+
/** @internal */
|
|
917
|
+
async enqueue(session, input, options) {
|
|
918
|
+
if (options.whenBusy === "steer") throw new SteerNotSupportedError(session);
|
|
919
|
+
await this.lifecycle.ready();
|
|
920
|
+
const operationId = options.operationId ?? crypto.randomUUID();
|
|
921
|
+
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
|
|
922
|
+
let accepted = false;
|
|
923
|
+
this.lifecycle.storage.transactionSync(() => {
|
|
924
|
+
store.ensureSession(session);
|
|
925
|
+
if (options.clientTools !== void 0) store.setClientTools(session, options.clientTools);
|
|
926
|
+
accepted = store.insert(session, operationId, input, options.source ?? "server");
|
|
927
|
+
});
|
|
928
|
+
if (accepted) _classPrivateFieldGet2(_events, this).emit(session, {
|
|
929
|
+
type: "operation",
|
|
930
|
+
status: {
|
|
931
|
+
operationId,
|
|
932
|
+
status: "queued"
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _wake).call(this, session);
|
|
936
|
+
_assertClassBrand(_ThinkHarness_brand, this, _ensureDrive).call(this, session);
|
|
937
|
+
return {
|
|
938
|
+
operationId,
|
|
939
|
+
session,
|
|
940
|
+
accepted
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
/** @internal Stop one operation, or everything open in a session. */
|
|
944
|
+
async abort(session, operationId) {
|
|
945
|
+
await this.lifecycle.ready();
|
|
946
|
+
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
|
|
947
|
+
const targets = operationId === void 0 ? store.open(session) : [store.get(session, operationId)].filter((op) => op !== void 0 && (op.status === "queued" || op.status === "running"));
|
|
948
|
+
for (const op of targets) {
|
|
949
|
+
const live = _classPrivateFieldGet2(_live, this).get(session);
|
|
950
|
+
if (live?.operationId === op.operationId) live.abort.abort();
|
|
951
|
+
else if (op.status === "queued") {
|
|
952
|
+
store.settle(session, op.operationId, {
|
|
953
|
+
status: "unanswered",
|
|
954
|
+
reason: "withdrawn"
|
|
955
|
+
});
|
|
956
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, op.operationId);
|
|
957
|
+
} else await _assertClassBrand(_ThinkHarness_brand, this, _abandon).call(this, op, "aborted");
|
|
958
|
+
}
|
|
959
|
+
return targets.length > 0;
|
|
960
|
+
}
|
|
961
|
+
/** @internal Wait for an operation to settle. */
|
|
962
|
+
async wait(session, operationId, signal) {
|
|
963
|
+
await this.lifecycle.ready();
|
|
964
|
+
for (;;) {
|
|
965
|
+
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).get(session, operationId);
|
|
966
|
+
if (!op) return {
|
|
967
|
+
operationId,
|
|
968
|
+
session,
|
|
969
|
+
status: "unanswered",
|
|
970
|
+
reason: "not_found"
|
|
971
|
+
};
|
|
972
|
+
if (op.status === "done" || op.status === "unanswered") return _assertClassBrand(_ThinkHarness_brand, this, _result).call(this, op);
|
|
973
|
+
await new Promise((resolveWait, reject) => {
|
|
974
|
+
if (signal?.aborted) {
|
|
975
|
+
reject(signal.reason);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const key = waiterKey(session, operationId);
|
|
979
|
+
const waiters = _classPrivateFieldGet2(_waiters, this).get(key) ?? /* @__PURE__ */ new Set();
|
|
980
|
+
_classPrivateFieldGet2(_waiters, this).set(key, waiters);
|
|
981
|
+
const done = () => {
|
|
982
|
+
signal?.removeEventListener("abort", aborted);
|
|
983
|
+
resolveWait();
|
|
984
|
+
};
|
|
985
|
+
const aborted = () => {
|
|
986
|
+
waiters.delete(done);
|
|
987
|
+
reject(signal?.reason);
|
|
988
|
+
};
|
|
989
|
+
waiters.add(done);
|
|
990
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
/** @internal */
|
|
995
|
+
async inspect(session, operationId) {
|
|
996
|
+
await this.lifecycle.ready();
|
|
997
|
+
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).get(session, operationId);
|
|
998
|
+
if (!op) return void 0;
|
|
999
|
+
switch (op.status) {
|
|
1000
|
+
case "queued":
|
|
1001
|
+
case "running": return {
|
|
1002
|
+
operationId,
|
|
1003
|
+
session,
|
|
1004
|
+
status: op.status
|
|
1005
|
+
};
|
|
1006
|
+
case "done":
|
|
1007
|
+
case "unanswered": return _assertClassBrand(_ThinkHarness_brand, this, _result).call(this, op);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/** @internal The active transcript of a session. */
|
|
1011
|
+
async messages(session) {
|
|
1012
|
+
return asUIMessages(await _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).getHistory());
|
|
1013
|
+
}
|
|
1014
|
+
/** @internal Abort everything in a session and clear its transcript. */
|
|
1015
|
+
async reset(session, handoff) {
|
|
1016
|
+
await this.abort(session);
|
|
1017
|
+
await _classPrivateFieldGet2(_drives, this).get(session);
|
|
1018
|
+
const handle = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session);
|
|
1019
|
+
_classPrivateFieldGet2(_ownClears, this).set(session, (_classPrivateFieldGet2(_ownClears, this).get(session) ?? 0) + 1);
|
|
1020
|
+
try {
|
|
1021
|
+
await handle.clearMessages();
|
|
1022
|
+
} finally {
|
|
1023
|
+
const left = (_classPrivateFieldGet2(_ownClears, this).get(session) ?? 1) - 1;
|
|
1024
|
+
if (left === 0) _classPrivateFieldGet2(_ownClears, this).delete(session);
|
|
1025
|
+
else _classPrivateFieldGet2(_ownClears, this).set(session, left);
|
|
1026
|
+
}
|
|
1027
|
+
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).deleteSettled(session);
|
|
1028
|
+
_classPrivateFieldGet2(_events, this).emit(session, { type: "reset" });
|
|
1029
|
+
if (handoff) {
|
|
1030
|
+
const note = {
|
|
1031
|
+
id: crypto.randomUUID(),
|
|
1032
|
+
role: "system",
|
|
1033
|
+
parts: [{
|
|
1034
|
+
type: "text",
|
|
1035
|
+
text: handoff
|
|
1036
|
+
}]
|
|
1037
|
+
};
|
|
1038
|
+
const release = _assertClassBrand(_ThinkHarness_brand, this, _markOwn).call(this, session, [note.id]);
|
|
1039
|
+
try {
|
|
1040
|
+
await handle.appendMessage(asSessionMessage(note));
|
|
1041
|
+
} finally {
|
|
1042
|
+
release();
|
|
1043
|
+
}
|
|
1044
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1045
|
+
type: "message",
|
|
1046
|
+
message: note
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/** @internal */
|
|
1051
|
+
subscribe(session, listener) {
|
|
1052
|
+
return _classPrivateFieldGet2(_events, this).subscribe(session, listener);
|
|
1053
|
+
}
|
|
1054
|
+
/** @internal The shared harness interface's watch. */
|
|
1055
|
+
async watch(session) {
|
|
1056
|
+
await this.lifecycle.ready();
|
|
1057
|
+
let watcher;
|
|
1058
|
+
const early = [];
|
|
1059
|
+
const unsubscribe = _classPrivateFieldGet2(_events, this).subscribe(session, (event) => {
|
|
1060
|
+
const events = toSessionEvents(event);
|
|
1061
|
+
if (watcher) watcher.push(events);
|
|
1062
|
+
else early.push(events);
|
|
1063
|
+
});
|
|
1064
|
+
const live = _classPrivateFieldGet2(_live, this).get(session);
|
|
1065
|
+
const partial = live ? [...live.partial] : _assertClassBrand(_ThinkHarness_brand, this, _recoverableChunks).call(this, session);
|
|
1066
|
+
const messages = await this.messages(session);
|
|
1067
|
+
const open = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session);
|
|
1068
|
+
const running = open.find((op) => op.status === "running");
|
|
1069
|
+
watcher = new Watcher({
|
|
1070
|
+
messages: messages.map((message) => toTranscriptMessage(message)),
|
|
1071
|
+
pending: open.map((op) => ({
|
|
1072
|
+
operationId: op.operationId,
|
|
1073
|
+
status: op.status === "queued" ? "queued" : "placed"
|
|
1074
|
+
})),
|
|
1075
|
+
...running && { run: {
|
|
1076
|
+
operations: [running.operationId],
|
|
1077
|
+
...partial.length > 0 && { partial: partial.flatMap((chunk) => {
|
|
1078
|
+
const converted = toResponseChunk(chunk, {});
|
|
1079
|
+
return converted ? [converted] : [];
|
|
1080
|
+
}) }
|
|
1081
|
+
} }
|
|
1082
|
+
}, unsubscribe);
|
|
1083
|
+
for (const events of early) watcher.push(events);
|
|
1084
|
+
return watcher;
|
|
1085
|
+
}
|
|
1086
|
+
/** @internal The running operation and its model call's chunks so far. */
|
|
1087
|
+
async inFlight(session) {
|
|
1088
|
+
await this.lifecycle.ready();
|
|
1089
|
+
const running = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session).find((op) => op.status === "running");
|
|
1090
|
+
if (!running) return void 0;
|
|
1091
|
+
const live = _classPrivateFieldGet2(_live, this).get(session);
|
|
1092
|
+
if (live?.operationId === running.operationId) return {
|
|
1093
|
+
operationId: running.operationId,
|
|
1094
|
+
continuation: live.continues,
|
|
1095
|
+
chunks: [...live.partial]
|
|
1096
|
+
};
|
|
1097
|
+
return {
|
|
1098
|
+
operationId: running.operationId,
|
|
1099
|
+
continuation: await _assertClassBrand(_ThinkHarness_brand, this, _message).call(this, running) !== void 0,
|
|
1100
|
+
chunks: running.streamId ? _assertClassBrand(_ThinkHarness_brand, this, _streamChunks).call(this, running.streamId) : []
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Listen to every session's events, for a host that serves several
|
|
1105
|
+
* sessions over one set of connections. Returns the unsubscribe.
|
|
1106
|
+
*/
|
|
1107
|
+
observe(listener) {
|
|
1108
|
+
return _classPrivateFieldGet2(_events, this).observe(listener);
|
|
1109
|
+
}
|
|
1110
|
+
/** @internal */
|
|
1111
|
+
async createSession(parent) {
|
|
1112
|
+
await this.lifecycle.ready();
|
|
1113
|
+
const id = crypto.randomUUID();
|
|
1114
|
+
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).ensureSession(id, parent);
|
|
1115
|
+
return this.session(id);
|
|
1116
|
+
}
|
|
1117
|
+
/** @internal Copy a session's active path into a new session. */
|
|
1118
|
+
async fork(from) {
|
|
1119
|
+
const source = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, from);
|
|
1120
|
+
const rows = await source.getHistoryRowStats();
|
|
1121
|
+
const fork = await this.createSession(from);
|
|
1122
|
+
const target = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, fork.id);
|
|
1123
|
+
let parentId = null;
|
|
1124
|
+
for (const row of rows) {
|
|
1125
|
+
const message = await source.getMessage(row.id);
|
|
1126
|
+
if (!message) continue;
|
|
1127
|
+
await target.appendMessage(message, { parentId });
|
|
1128
|
+
parentId = message.id;
|
|
1129
|
+
}
|
|
1130
|
+
return fork;
|
|
1131
|
+
}
|
|
1132
|
+
/** @internal */
|
|
1133
|
+
async listSessions() {
|
|
1134
|
+
await this.lifecycle.ready();
|
|
1135
|
+
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
|
|
1136
|
+
const busy = new Set(store.open().map((op) => op.session));
|
|
1137
|
+
return store.sessions().map((record) => ({
|
|
1138
|
+
id: record.id,
|
|
1139
|
+
...record.parent !== void 0 && { parent: record.parent },
|
|
1140
|
+
busy: busy.has(record.id)
|
|
1141
|
+
}));
|
|
1142
|
+
}
|
|
1143
|
+
/** @internal The answers to a message: its children in the tree. */
|
|
1144
|
+
async branches(session, messageId) {
|
|
1145
|
+
return asUIMessages(await _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).getBranches(messageId));
|
|
1146
|
+
}
|
|
1147
|
+
/** @internal Full-text search over a session's messages. */
|
|
1148
|
+
search(session, query, options) {
|
|
1149
|
+
return _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).search(query, options?.limit === void 0 ? void 0 : { limit: options.limit });
|
|
1150
|
+
}
|
|
1151
|
+
/** @internal Compact a session with its compaction function. */
|
|
1152
|
+
async compact(session) {
|
|
1153
|
+
return _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).compact();
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
/** Schedule the session's wake job, now or at `time`. */
|
|
1157
|
+
function _wake(session, time = Date.now()) {
|
|
1158
|
+
return this.lifecycle.jobs.push({
|
|
1159
|
+
id: wakeJobId(session),
|
|
1160
|
+
fn: WAKE_FN,
|
|
1161
|
+
time,
|
|
1162
|
+
payload: { session },
|
|
1163
|
+
singleflight: true,
|
|
1164
|
+
recoveryLoop: true
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* One run of a session's wake job. With work open, it makes sure a drive
|
|
1169
|
+
* is running and keeps a heartbeat; with none, it completes.
|
|
1170
|
+
*/
|
|
1171
|
+
function _wakeStep(session) {
|
|
1172
|
+
const heartbeat = { rescheduleAt: Date.now() + _classPrivateFieldGet2(_heartbeatMs, this) };
|
|
1173
|
+
if (_classPrivateFieldGet2(_drives, this).has(session)) return heartbeat;
|
|
1174
|
+
if (_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session).length === 0) return void 0;
|
|
1175
|
+
const drive = _assertClassBrand(_ThinkHarness_brand, this, _ensureDrive).call(this, session);
|
|
1176
|
+
this.lifecycle.trackAlarmWork(drive);
|
|
1177
|
+
return heartbeat;
|
|
1178
|
+
}
|
|
1179
|
+
/** Run the session's open operations in order, unless already running. */
|
|
1180
|
+
function _ensureDrive(session) {
|
|
1181
|
+
const running = _classPrivateFieldGet2(_drives, this).get(session);
|
|
1182
|
+
if (running) return running;
|
|
1183
|
+
let wakeAt;
|
|
1184
|
+
const drive = (async () => {
|
|
1185
|
+
try {
|
|
1186
|
+
await this.lifecycle.runInHostContext(async () => {
|
|
1187
|
+
for (;;) {
|
|
1188
|
+
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session)[0];
|
|
1189
|
+
if (!op) return;
|
|
1190
|
+
const outcome = await _assertClassBrand(_ThinkHarness_brand, this, _run).call(this, op);
|
|
1191
|
+
if (outcome.retryAt !== void 0) {
|
|
1192
|
+
wakeAt = outcome.retryAt;
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
console.error("ThinkHarness drive failed", error);
|
|
1199
|
+
wakeAt = Date.now() + _classPrivateFieldGet2(_heartbeatMs, this);
|
|
1200
|
+
} finally {
|
|
1201
|
+
_classPrivateFieldGet2(_drives, this).delete(session);
|
|
1202
|
+
}
|
|
1203
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _wake).call(this, session, wakeAt);
|
|
1204
|
+
})();
|
|
1205
|
+
_classPrivateFieldGet2(_drives, this).set(session, drive);
|
|
1206
|
+
return drive;
|
|
1207
|
+
}
|
|
1208
|
+
async function _run(first) {
|
|
1209
|
+
const { session, operationId } = first;
|
|
1210
|
+
const live = {
|
|
1211
|
+
operationId,
|
|
1212
|
+
abort: new AbortController(),
|
|
1213
|
+
partial: [],
|
|
1214
|
+
continues: false
|
|
1215
|
+
};
|
|
1216
|
+
_classPrivateFieldGet2(_live, this).set(session, live);
|
|
1217
|
+
try {
|
|
1218
|
+
let op = first;
|
|
1219
|
+
if (op.status === "queued") {
|
|
1220
|
+
op = await _assertClassBrand(_ThinkHarness_brand, this, _place).call(this, op);
|
|
1221
|
+
if (!op) return {};
|
|
1222
|
+
}
|
|
1223
|
+
const continuation = await _assertClassBrand(_ThinkHarness_brand, this, _message).call(this, op) !== void 0;
|
|
1224
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1225
|
+
type: "run-start",
|
|
1226
|
+
operationId,
|
|
1227
|
+
continuation
|
|
1228
|
+
});
|
|
1229
|
+
try {
|
|
1230
|
+
return await _assertClassBrand(_ThinkHarness_brand, this, _steps).call(this, session, operationId, live);
|
|
1231
|
+
} finally {
|
|
1232
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1233
|
+
type: "run-end",
|
|
1234
|
+
operationId
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
console.error(`ThinkHarness operation ${operationId} failed`, error);
|
|
1239
|
+
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).get(session, operationId);
|
|
1240
|
+
if (op && (op.status === "running" || op.status === "queued")) await _assertClassBrand(_ThinkHarness_brand, this, _abandon).call(this, op, errorText(error));
|
|
1241
|
+
return {};
|
|
1242
|
+
} finally {
|
|
1243
|
+
_classPrivateFieldGet2(_live, this).delete(session);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
async function _steps(session, operationId, live) {
|
|
1247
|
+
const maxSteps = _classPrivateFieldGet2(_options, this).maxSteps ?? DEFAULT_MAX_STEPS;
|
|
1248
|
+
for (;;) {
|
|
1249
|
+
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).get(session, operationId);
|
|
1250
|
+
if (!op || op.status !== "running") return {};
|
|
1251
|
+
if (live.abort.signal.aborted) {
|
|
1252
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _abandon).call(this, op, "aborted");
|
|
1253
|
+
return {};
|
|
1254
|
+
}
|
|
1255
|
+
if (op.abandonReason !== void 0) {
|
|
1256
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _abandon).call(this, op, op.abandonReason);
|
|
1257
|
+
return {};
|
|
1258
|
+
}
|
|
1259
|
+
const message = await _assertClassBrand(_ThinkHarness_brand, this, _message).call(this, op);
|
|
1260
|
+
const tools = await _assertClassBrand(_ThinkHarness_brand, this, _tools).call(this, session);
|
|
1261
|
+
const action = nextAction(op, message, (name) => isServerTool(tools, name), maxSteps);
|
|
1262
|
+
switch (action._tag) {
|
|
1263
|
+
case "recover-model": {
|
|
1264
|
+
const outcome = await _assertClassBrand(_ThinkHarness_brand, this, _recoverModel).call(this, op, message);
|
|
1265
|
+
if (outcome) return outcome;
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
case "call-model": {
|
|
1269
|
+
const outcome = await _assertClassBrand(_ThinkHarness_brand, this, _callModel).call(this, op, message, tools, live);
|
|
1270
|
+
if (outcome) return outcome;
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
case "run-tools":
|
|
1274
|
+
if (!message) return {};
|
|
1275
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _runTools).call(this, op, message, action.calls, tools, live);
|
|
1276
|
+
break;
|
|
1277
|
+
case "await-input":
|
|
1278
|
+
case "end":
|
|
1279
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, () => {
|
|
1280
|
+
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).settle(session, operationId, {
|
|
1281
|
+
status: "done",
|
|
1282
|
+
text: textOf(message)
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId, message);
|
|
1286
|
+
return {};
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Place a queued operation: write what it brings to the transcript and
|
|
1292
|
+
* mark it running, in one transaction. Returns the running operation, or
|
|
1293
|
+
* undefined when placing settled it.
|
|
1294
|
+
*/
|
|
1295
|
+
async function _place(op) {
|
|
1296
|
+
const { session, operationId } = op;
|
|
1297
|
+
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
|
|
1298
|
+
const handle = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session);
|
|
1299
|
+
const input = op.input;
|
|
1300
|
+
const placed = [];
|
|
1301
|
+
let settledText;
|
|
1302
|
+
let unanswered;
|
|
1303
|
+
const running = (change) => store.start(session, operationId, {
|
|
1304
|
+
parentId: change.parentId ?? null,
|
|
1305
|
+
messageId: change.messageId,
|
|
1306
|
+
pendingModel: true
|
|
1307
|
+
});
|
|
1308
|
+
switch (input.kind) {
|
|
1309
|
+
case "messages": {
|
|
1310
|
+
let messages = input.messages;
|
|
1311
|
+
if (op.source === "client") {
|
|
1312
|
+
if (messages.some((message) => message.role !== "user")) {
|
|
1313
|
+
unanswered = "client_role";
|
|
1314
|
+
break;
|
|
1315
|
+
}
|
|
1316
|
+
const fresh = [];
|
|
1317
|
+
for (const message of messages) if (!await handle.getMessage(message.id)) fresh.push(message);
|
|
1318
|
+
messages = fresh;
|
|
1319
|
+
}
|
|
1320
|
+
const last = messages.at(-1);
|
|
1321
|
+
if (!last) {
|
|
1322
|
+
unanswered = "empty";
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, (put) => {
|
|
1326
|
+
let parentId;
|
|
1327
|
+
for (const message of messages) {
|
|
1328
|
+
put(message, {
|
|
1329
|
+
parentId,
|
|
1330
|
+
source: op.source
|
|
1331
|
+
});
|
|
1332
|
+
parentId = message.id;
|
|
1333
|
+
placed.push(message);
|
|
1334
|
+
}
|
|
1335
|
+
running({
|
|
1336
|
+
parentId: last.id,
|
|
1337
|
+
messageId: crypto.randomUUID()
|
|
1338
|
+
});
|
|
1339
|
+
});
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
case "answer": {
|
|
1343
|
+
const history = asUIMessages(await handle.getHistory());
|
|
1344
|
+
const tools = await _assertClassBrand(_ThinkHarness_brand, this, _tools).call(this, session);
|
|
1345
|
+
const answer = input.answer;
|
|
1346
|
+
if (answer.type === "tool-result" && op.source === "client") {
|
|
1347
|
+
const call = history.flatMap((message) => message.parts).find((part) => isToolCallPart(part) && part.toolCallId === answer.toolCallId);
|
|
1348
|
+
if (call && isServerTool(tools, toolNameOf(call))) {
|
|
1349
|
+
unanswered = "not_client_tool";
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const answered = answerToolCall(history, toolCallAnswer(answer));
|
|
1354
|
+
if (!answered) {
|
|
1355
|
+
unanswered = "not_waiting";
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
const latest = history.at(-1);
|
|
1359
|
+
const continues = input.autoContinue && latest?.id === answered.id && nextAction({
|
|
1360
|
+
streamId: void 0,
|
|
1361
|
+
pendingModel: true,
|
|
1362
|
+
steps: 0
|
|
1363
|
+
}, answered, (name) => isServerTool(tools, name), Number.POSITIVE_INFINITY)._tag !== "await-input";
|
|
1364
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, (put) => {
|
|
1365
|
+
put(answered);
|
|
1366
|
+
placed.push(answered);
|
|
1367
|
+
if (continues) running({ messageId: answered.id });
|
|
1368
|
+
else store.settle(session, operationId, {
|
|
1369
|
+
status: "done",
|
|
1370
|
+
text: textOf(answered)
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1373
|
+
if (!continues) settledText = textOf(answered);
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
case "regenerate": {
|
|
1377
|
+
const history = asUIMessages(await handle.getHistory());
|
|
1378
|
+
const target = input.messageId ?? [...history].reverse().find((message) => message.role === "user")?.id;
|
|
1379
|
+
if (!target || !await handle.getMessage(target)) {
|
|
1380
|
+
unanswered = "not_found";
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, () => running({
|
|
1384
|
+
parentId: target,
|
|
1385
|
+
messageId: crypto.randomUUID()
|
|
1386
|
+
}));
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
case "continue": {
|
|
1390
|
+
const latest = await handle.getLatestLeaf();
|
|
1391
|
+
if (!latest) {
|
|
1392
|
+
unanswered = "not_found";
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, () => latest.role === "assistant" ? running({ messageId: latest.id }) : running({
|
|
1396
|
+
parentId: latest.id,
|
|
1397
|
+
messageId: crypto.randomUUID()
|
|
1398
|
+
}));
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
for (const message of placed) _classPrivateFieldGet2(_events, this).emit(session, {
|
|
1403
|
+
type: "message",
|
|
1404
|
+
message,
|
|
1405
|
+
operationId
|
|
1406
|
+
});
|
|
1407
|
+
if (unanswered !== void 0) {
|
|
1408
|
+
store.settle(session, operationId, {
|
|
1409
|
+
status: "unanswered",
|
|
1410
|
+
reason: unanswered
|
|
1411
|
+
});
|
|
1412
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId);
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (settledText !== void 0) {
|
|
1416
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId);
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1420
|
+
type: "operation",
|
|
1421
|
+
status: {
|
|
1422
|
+
operationId,
|
|
1423
|
+
status: "placed"
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
return store.get(session, operationId);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* One model call. Its chunks go to a stream as they arrive; when it ends,
|
|
1430
|
+
* the message is persisted and the stream discarded in one transaction.
|
|
1431
|
+
* Returns an outcome only when the operation must wait.
|
|
1432
|
+
*/
|
|
1433
|
+
async function _callModel(op, existing, tools, live) {
|
|
1434
|
+
const { session, operationId } = op;
|
|
1435
|
+
const messageId = op.messageId ?? crypto.randomUUID();
|
|
1436
|
+
const hooks = _classPrivateFieldGet2(_options, this).hooks;
|
|
1437
|
+
const handle = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session);
|
|
1438
|
+
const history = asUIMessages(await handle.getHistory({ leafId: existing ? existing.id : op.parentId }));
|
|
1439
|
+
const system = await resolve(_classPrivateFieldGet2(_options, this).system, session);
|
|
1440
|
+
const config = await hooks?.beforeTurn?.({
|
|
1441
|
+
session,
|
|
1442
|
+
operationId,
|
|
1443
|
+
step: op.steps,
|
|
1444
|
+
continuation: existing !== void 0,
|
|
1445
|
+
messages: history,
|
|
1446
|
+
system,
|
|
1447
|
+
tools: Object.keys(tools)
|
|
1448
|
+
}) ?? {};
|
|
1449
|
+
const model = config.model ?? await resolve(_classPrivateFieldGet2(_options, this).model, session);
|
|
1450
|
+
if (model === void 0) throw new Error("ThinkHarness has no model");
|
|
1451
|
+
const offered = modelTools(tools, config.activeTools);
|
|
1452
|
+
const effectiveSystem = config.system ?? system;
|
|
1453
|
+
const messages = config.messages ?? await convertToModelMessages(history, {
|
|
1454
|
+
tools,
|
|
1455
|
+
ignoreIncompleteToolCalls: true
|
|
1456
|
+
});
|
|
1457
|
+
const streamId = `think:${crypto.randomUUID()}`;
|
|
1458
|
+
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).update(session, operationId, {
|
|
1459
|
+
streamId,
|
|
1460
|
+
messageId
|
|
1461
|
+
});
|
|
1462
|
+
const writer = await _classPrivateFieldGet2(_streams, this).open(streamId, {
|
|
1463
|
+
tag: streamTag(session, operationId),
|
|
1464
|
+
metadata: {
|
|
1465
|
+
session,
|
|
1466
|
+
operationId,
|
|
1467
|
+
messageId
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
const stall = new AbortController();
|
|
1471
|
+
const signal = AbortSignal.any([live.abort.signal, stall.signal]);
|
|
1472
|
+
let modelError;
|
|
1473
|
+
let final;
|
|
1474
|
+
let finishReason;
|
|
1475
|
+
let caught;
|
|
1476
|
+
live.partial = [];
|
|
1477
|
+
live.continues = existing !== void 0;
|
|
1478
|
+
let segment = [];
|
|
1479
|
+
let segmentBytes = 0;
|
|
1480
|
+
let flushTimer;
|
|
1481
|
+
const flush = () => {
|
|
1482
|
+
if (flushTimer !== void 0) clearTimeout(flushTimer);
|
|
1483
|
+
flushTimer = void 0;
|
|
1484
|
+
if (segment.length === 0) return;
|
|
1485
|
+
writer.append(segment);
|
|
1486
|
+
segment = [];
|
|
1487
|
+
segmentBytes = 0;
|
|
1488
|
+
};
|
|
1489
|
+
try {
|
|
1490
|
+
const stream = toUIMessageStream({
|
|
1491
|
+
stream: streamText({
|
|
1492
|
+
model,
|
|
1493
|
+
...effectiveSystem !== void 0 && { system: effectiveSystem },
|
|
1494
|
+
messages,
|
|
1495
|
+
tools: offered,
|
|
1496
|
+
...config.toolChoice !== void 0 && { toolChoice: config.toolChoice },
|
|
1497
|
+
...config.providerOptions !== void 0 && { providerOptions: config.providerOptions },
|
|
1498
|
+
...config.maxOutputTokens !== void 0 && { maxOutputTokens: config.maxOutputTokens },
|
|
1499
|
+
...config.temperature !== void 0 && { temperature: config.temperature },
|
|
1500
|
+
stopWhen: stepCountIs(1),
|
|
1501
|
+
abortSignal: signal,
|
|
1502
|
+
onError: ({ error }) => {
|
|
1503
|
+
modelError ??= error;
|
|
1504
|
+
}
|
|
1505
|
+
}).stream,
|
|
1506
|
+
tools: offered,
|
|
1507
|
+
originalMessages: existing ? history : [],
|
|
1508
|
+
generateMessageId: () => messageId,
|
|
1509
|
+
onEnd: ({ responseMessage }) => {
|
|
1510
|
+
final = responseMessage;
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
const stallTimeoutMs = _classPrivateFieldGet2(_options, this).recovery?.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
|
|
1514
|
+
for await (const chunk of untilAborted(iterateWithStallWatchdog(stream, stallTimeoutMs, () => stall.abort(new ChatStreamStalledError("Model stream stalled"))), live.abort.signal)) {
|
|
1515
|
+
if (chunk.type === "finish") finishReason = chunk.finishReason;
|
|
1516
|
+
live.partial.push(chunk);
|
|
1517
|
+
segment.push(chunk);
|
|
1518
|
+
segmentBytes += JSON.stringify(chunk).length;
|
|
1519
|
+
if (segment.length >= SEGMENT_CHUNKS || segmentBytes >= SEGMENT_MAX_BYTES) flush();
|
|
1520
|
+
else flushTimer ??= setTimeout(flush, FLUSH_AFTER_MS);
|
|
1521
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1522
|
+
type: "chunk",
|
|
1523
|
+
operationId,
|
|
1524
|
+
chunk
|
|
1525
|
+
});
|
|
1526
|
+
if (hooks?.onChunk) try {
|
|
1527
|
+
hooks.onChunk({
|
|
1528
|
+
session,
|
|
1529
|
+
operationId,
|
|
1530
|
+
chunk
|
|
1531
|
+
});
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
console.error("ThinkHarness onChunk hook failed", error);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
caught = error;
|
|
1538
|
+
}
|
|
1539
|
+
flush();
|
|
1540
|
+
const message = final ?? accumulate(messageId, existing, live.partial);
|
|
1541
|
+
const partial = message ? settledPartial(message) : void 0;
|
|
1542
|
+
const keep = partial && hasContent(partial) ? partial : void 0;
|
|
1543
|
+
const stalled = caught instanceof ChatStreamStalledError || stall.signal.aborted;
|
|
1544
|
+
const failure = stalled ? void 0 : modelError ?? caught;
|
|
1545
|
+
if (live.abort.signal.aborted) {
|
|
1546
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, streamId, (put) => {
|
|
1547
|
+
if (keep) put(keep, { parentId: op.parentId });
|
|
1548
|
+
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).settle(session, operationId, {
|
|
1549
|
+
status: "unanswered",
|
|
1550
|
+
reason: "aborted"
|
|
1551
|
+
}, { streamId: null });
|
|
1552
|
+
});
|
|
1553
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId, keep);
|
|
1554
|
+
return {};
|
|
1555
|
+
}
|
|
1556
|
+
if (stalled || failure !== void 0) {
|
|
1557
|
+
if (failure !== void 0) try {
|
|
1558
|
+
await hooks?.onError?.(failure, {
|
|
1559
|
+
session,
|
|
1560
|
+
operationId
|
|
1561
|
+
});
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
console.error("ThinkHarness onError hook failed", error);
|
|
1564
|
+
}
|
|
1565
|
+
const kind = stalled ? "retry" : hooks?.classifyError?.(failure) ?? "fail";
|
|
1566
|
+
if (kind === "context-overflow" && op.overflowRetries < MAX_OVERFLOW_RETRIES) {
|
|
1567
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, streamId, (put) => {
|
|
1568
|
+
if (keep) put(keep, { parentId: op.parentId });
|
|
1569
|
+
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).update(session, operationId, {
|
|
1570
|
+
streamId: null,
|
|
1571
|
+
overflowRetries: op.overflowRetries + 1
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
try {
|
|
1575
|
+
await handle.compact();
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
console.warn("ThinkHarness could not compact the session", error);
|
|
1578
|
+
}
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
if (kind === "retry") return _assertClassBrand(_ThinkHarness_brand, this, _interrupted).call(this, op, streamId, keep);
|
|
1582
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, streamId, (put) => {
|
|
1583
|
+
if (keep) put(keep, { parentId: op.parentId });
|
|
1584
|
+
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).settle(session, operationId, {
|
|
1585
|
+
status: "unanswered",
|
|
1586
|
+
reason: errorText(failure)
|
|
1587
|
+
}, { streamId: null });
|
|
1588
|
+
});
|
|
1589
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId, keep);
|
|
1590
|
+
return {};
|
|
1591
|
+
}
|
|
1592
|
+
if (!message) throw new Error("The model call produced no message");
|
|
1593
|
+
const toolCalls = lastStep(message).some((part) => isToolCallPart(part) && !part.providerExecuted);
|
|
1594
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, streamId, (put) => {
|
|
1595
|
+
put(message, { parentId: op.parentId });
|
|
1596
|
+
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).update(session, operationId, {
|
|
1597
|
+
streamId: null,
|
|
1598
|
+
steps: op.steps + 1,
|
|
1599
|
+
interruptions: 0,
|
|
1600
|
+
pendingModel: toolCalls
|
|
1601
|
+
});
|
|
1602
|
+
});
|
|
1603
|
+
live.partial = [];
|
|
1604
|
+
live.continues = true;
|
|
1605
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1606
|
+
type: "message",
|
|
1607
|
+
message,
|
|
1608
|
+
operationId
|
|
1609
|
+
});
|
|
1610
|
+
await hooks?.onStepFinish?.({
|
|
1611
|
+
session,
|
|
1612
|
+
operationId,
|
|
1613
|
+
step: op.steps,
|
|
1614
|
+
message,
|
|
1615
|
+
finishReason
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* A model call an eviction cut short: keep what it produced, then call
|
|
1620
|
+
* the model again to continue the same message, within the budget.
|
|
1621
|
+
*/
|
|
1622
|
+
async function _recoverModel(op, existing) {
|
|
1623
|
+
const streamId = op.streamId;
|
|
1624
|
+
if (streamId === void 0) return void 0;
|
|
1625
|
+
const chunks = _assertClassBrand(_ThinkHarness_brand, this, _streamChunks).call(this, streamId);
|
|
1626
|
+
const rebuilt = chunks.length > 0 ? accumulate(op.messageId ?? crypto.randomUUID(), existing, chunks) : void 0;
|
|
1627
|
+
const partial = rebuilt ? settledPartial(rebuilt) : void 0;
|
|
1628
|
+
const keep = partial && hasContent(partial) ? partial : void 0;
|
|
1629
|
+
return _assertClassBrand(_ThinkHarness_brand, this, _interrupted).call(this, op, streamId, keep);
|
|
1630
|
+
}
|
|
1631
|
+
/** Count an interruption, keep the partial, and decide whether to go on. */
|
|
1632
|
+
async function _interrupted(op, streamId, partial) {
|
|
1633
|
+
const { session, operationId } = op;
|
|
1634
|
+
const recovery = _classPrivateFieldGet2(_options, this).recovery;
|
|
1635
|
+
const attempt = op.interruptions + 1;
|
|
1636
|
+
let decision = attempt > (recovery?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS) ? "abandon" : "continue";
|
|
1637
|
+
if (decision === "continue" && _classPrivateFieldGet2(_options, this).hooks?.onRecovery) decision = await _classPrivateFieldGet2(_options, this).hooks.onRecovery({
|
|
1638
|
+
session,
|
|
1639
|
+
operationId,
|
|
1640
|
+
attempt,
|
|
1641
|
+
partial
|
|
1642
|
+
});
|
|
1643
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, streamId, (put) => {
|
|
1644
|
+
if (partial) put(partial, { parentId: op.parentId });
|
|
1645
|
+
const progress = {
|
|
1646
|
+
streamId: null,
|
|
1647
|
+
interruptions: attempt
|
|
1648
|
+
};
|
|
1649
|
+
if (decision === "abandon") _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).settle(session, operationId, {
|
|
1650
|
+
status: "unanswered",
|
|
1651
|
+
reason: "interrupted"
|
|
1652
|
+
}, progress);
|
|
1653
|
+
else _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).update(session, operationId, progress);
|
|
1654
|
+
});
|
|
1655
|
+
if (partial) _classPrivateFieldGet2(_events, this).emit(session, {
|
|
1656
|
+
type: "message",
|
|
1657
|
+
message: partial,
|
|
1658
|
+
operationId
|
|
1659
|
+
});
|
|
1660
|
+
if (decision === "abandon") {
|
|
1661
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId, partial);
|
|
1662
|
+
return {};
|
|
1663
|
+
}
|
|
1664
|
+
const delay = backoff(attempt, recovery?.backoffMs ?? DEFAULT_BACKOFF_MS);
|
|
1665
|
+
return delay > 0 ? { retryAt: Date.now() + delay } : void 0;
|
|
1666
|
+
}
|
|
1667
|
+
async function _runTools(op, message, calls, tools, live) {
|
|
1668
|
+
const { session, operationId } = op;
|
|
1669
|
+
const store = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this);
|
|
1670
|
+
const hooks = _classPrivateFieldGet2(_options, this).hooks;
|
|
1671
|
+
const modelMessages = await convertToModelMessages(asUIMessages(await _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).getHistory({ leafId: message.id })).slice(0, -1), {
|
|
1672
|
+
tools,
|
|
1673
|
+
ignoreIncompleteToolCalls: true
|
|
1674
|
+
});
|
|
1675
|
+
let current = message;
|
|
1676
|
+
const toRun = [];
|
|
1677
|
+
const approvals = [];
|
|
1678
|
+
for (const call of calls) {
|
|
1679
|
+
const toolName = toolNameOf(call);
|
|
1680
|
+
const tool = tools[toolName];
|
|
1681
|
+
if (!tool) continue;
|
|
1682
|
+
if (call.state === "input-available") {
|
|
1683
|
+
if (await _classPrivateFieldGet2(_options, this).toolApproval?.({
|
|
1684
|
+
session,
|
|
1685
|
+
toolName,
|
|
1686
|
+
toolCallId: call.toolCallId,
|
|
1687
|
+
input: call.input
|
|
1688
|
+
}) || await toolNeedsApproval(tool, call.input, {
|
|
1689
|
+
toolCallId: call.toolCallId,
|
|
1690
|
+
messages: modelMessages
|
|
1691
|
+
})) {
|
|
1692
|
+
approvals.push(call.toolCallId);
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
toRun.push(call);
|
|
1697
|
+
}
|
|
1698
|
+
if (approvals.length > 0) {
|
|
1699
|
+
for (const toolCallId of approvals) current = withToolPart(current, toolCallId, (part) => ({
|
|
1700
|
+
...part,
|
|
1701
|
+
state: "approval-requested",
|
|
1702
|
+
approval: { id: `approval:${toolCallId}` }
|
|
1703
|
+
}));
|
|
1704
|
+
const snapshot = current;
|
|
1705
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, (put) => put(snapshot));
|
|
1706
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1707
|
+
type: "message",
|
|
1708
|
+
message: snapshot,
|
|
1709
|
+
operationId
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
await Promise.all(toRun.map(async (call) => {
|
|
1713
|
+
const toolName = toolNameOf(call);
|
|
1714
|
+
const tool = tools[toolName];
|
|
1715
|
+
if (!tool) return;
|
|
1716
|
+
const { toolCallId } = call;
|
|
1717
|
+
const started = store.toolCall(session, toolCallId);
|
|
1718
|
+
let outcome;
|
|
1719
|
+
if (started) {
|
|
1720
|
+
const policy = toolRecovery(tool);
|
|
1721
|
+
const maxAttempts = _classPrivateFieldGet2(_options, this).recovery?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
1722
|
+
if (policy === "report" || started.attempts >= maxAttempts) outcome = {
|
|
1723
|
+
ok: false,
|
|
1724
|
+
errorText: INTERRUPTED_TOOL_ERROR
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
let attempt = started?.attempts ?? 0;
|
|
1728
|
+
let input = call.input;
|
|
1729
|
+
if (!outcome) {
|
|
1730
|
+
attempt = store.startToolCall(session, toolCallId, operationId);
|
|
1731
|
+
const decision = await hooks?.beforeToolCall?.({
|
|
1732
|
+
session,
|
|
1733
|
+
operationId,
|
|
1734
|
+
toolCallId,
|
|
1735
|
+
toolName,
|
|
1736
|
+
input,
|
|
1737
|
+
attempt
|
|
1738
|
+
});
|
|
1739
|
+
if (decision?.action === "block") outcome = {
|
|
1740
|
+
ok: false,
|
|
1741
|
+
errorText: decision.reason
|
|
1742
|
+
};
|
|
1743
|
+
else if (decision?.action === "substitute") outcome = {
|
|
1744
|
+
ok: true,
|
|
1745
|
+
output: decision.output
|
|
1746
|
+
};
|
|
1747
|
+
else {
|
|
1748
|
+
if (decision?.action === "run" && decision.input !== void 0) input = decision.input;
|
|
1749
|
+
outcome = await executeTool(tool, input, {
|
|
1750
|
+
toolCallId,
|
|
1751
|
+
messages: modelMessages,
|
|
1752
|
+
abortSignal: live.abort.signal,
|
|
1753
|
+
onPreliminary: (output) => _classPrivateFieldGet2(_events, this).emit(session, {
|
|
1754
|
+
type: "chunk",
|
|
1755
|
+
operationId,
|
|
1756
|
+
chunk: {
|
|
1757
|
+
type: "tool-output-available",
|
|
1758
|
+
toolCallId,
|
|
1759
|
+
output,
|
|
1760
|
+
preliminary: true
|
|
1761
|
+
}
|
|
1762
|
+
})
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
const settled = outcome;
|
|
1767
|
+
await hooks?.afterToolCall?.({
|
|
1768
|
+
session,
|
|
1769
|
+
operationId,
|
|
1770
|
+
toolCallId,
|
|
1771
|
+
toolName,
|
|
1772
|
+
input,
|
|
1773
|
+
attempt,
|
|
1774
|
+
...settled
|
|
1775
|
+
});
|
|
1776
|
+
current = withToolPart(current, toolCallId, (part) => withOutcome(part, settled));
|
|
1777
|
+
const snapshot = current;
|
|
1778
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, void 0, (put) => {
|
|
1779
|
+
put(snapshot);
|
|
1780
|
+
store.finishToolCall(session, toolCallId);
|
|
1781
|
+
store.update(session, operationId, { interruptions: 0 });
|
|
1782
|
+
});
|
|
1783
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1784
|
+
type: "chunk",
|
|
1785
|
+
operationId,
|
|
1786
|
+
chunk: settled.ok ? {
|
|
1787
|
+
type: "tool-output-available",
|
|
1788
|
+
toolCallId,
|
|
1789
|
+
output: settled.output
|
|
1790
|
+
} : {
|
|
1791
|
+
type: "tool-output-error",
|
|
1792
|
+
toolCallId,
|
|
1793
|
+
errorText: settled.errorText
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1797
|
+
type: "message",
|
|
1798
|
+
message: snapshot,
|
|
1799
|
+
operationId
|
|
1800
|
+
});
|
|
1801
|
+
}));
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* End an operation that cannot run on: keep what its interrupted model
|
|
1805
|
+
* call produced, mark any tool call it left open as failed, and settle
|
|
1806
|
+
* it `unanswered`.
|
|
1807
|
+
*/
|
|
1808
|
+
async function _abandon(op, reason) {
|
|
1809
|
+
const { session, operationId } = op;
|
|
1810
|
+
const existing = await _assertClassBrand(_ThinkHarness_brand, this, _message).call(this, op);
|
|
1811
|
+
let message = existing;
|
|
1812
|
+
const chunks = op.streamId === void 0 ? [] : _assertClassBrand(_ThinkHarness_brand, this, _streamChunks).call(this, op.streamId);
|
|
1813
|
+
if (chunks.length > 0) {
|
|
1814
|
+
const rebuilt = accumulate(op.messageId ?? crypto.randomUUID(), existing, chunks);
|
|
1815
|
+
if (rebuilt) message = settledPartial(rebuilt);
|
|
1816
|
+
}
|
|
1817
|
+
if (message) {
|
|
1818
|
+
for (const part of lastStep(message)) if (isToolCallPart(part) && !part.providerExecuted && (part.state === "input-available" || part.state === "approval-responded")) message = withToolPart(message, part.toolCallId, (call) => withOutcome(call, {
|
|
1819
|
+
ok: false,
|
|
1820
|
+
errorText: reason
|
|
1821
|
+
}));
|
|
1822
|
+
}
|
|
1823
|
+
const keep = message && hasContent(message) && message !== existing ? message : void 0;
|
|
1824
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _commit).call(this, session, op.streamId, (put) => {
|
|
1825
|
+
if (keep) put(keep, { parentId: op.parentId });
|
|
1826
|
+
_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).settle(session, operationId, {
|
|
1827
|
+
status: "unanswered",
|
|
1828
|
+
reason
|
|
1829
|
+
}, { streamId: null });
|
|
1830
|
+
});
|
|
1831
|
+
if (keep) _classPrivateFieldGet2(_events, this).emit(session, {
|
|
1832
|
+
type: "message",
|
|
1833
|
+
message: keep,
|
|
1834
|
+
operationId
|
|
1835
|
+
});
|
|
1836
|
+
await _assertClassBrand(_ThinkHarness_brand, this, _settled).call(this, session, operationId, keep ?? existing);
|
|
1837
|
+
}
|
|
1838
|
+
/** Tell everyone an operation settled. Its row is already written. */
|
|
1839
|
+
async function _settled(session, operationId, message) {
|
|
1840
|
+
const op = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).get(session, operationId);
|
|
1841
|
+
if (!op) return;
|
|
1842
|
+
const result = _assertClassBrand(_ThinkHarness_brand, this, _result).call(this, op);
|
|
1843
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1844
|
+
type: "operation",
|
|
1845
|
+
status: result.status === "done" ? {
|
|
1846
|
+
operationId,
|
|
1847
|
+
status: "done",
|
|
1848
|
+
...result.text !== void 0 && { text: result.text }
|
|
1849
|
+
} : {
|
|
1850
|
+
operationId,
|
|
1851
|
+
status: "unanswered",
|
|
1852
|
+
...result.reason !== void 0 && { reason: result.reason }
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
const key = waiterKey(session, operationId);
|
|
1856
|
+
for (const waiter of _classPrivateFieldGet2(_waiters, this).get(key) ?? []) waiter();
|
|
1857
|
+
_classPrivateFieldGet2(_waiters, this).delete(key);
|
|
1858
|
+
try {
|
|
1859
|
+
await _classPrivateFieldGet2(_options, this).hooks?.onTurnEnd?.({
|
|
1860
|
+
...result,
|
|
1861
|
+
...message && { message }
|
|
1862
|
+
});
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
console.error("ThinkHarness onTurnEnd hook failed", error);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
function _result(op) {
|
|
1868
|
+
const { operationId, session } = op;
|
|
1869
|
+
return op.status === "done" ? {
|
|
1870
|
+
operationId,
|
|
1871
|
+
session,
|
|
1872
|
+
status: "done",
|
|
1873
|
+
text: op.text ?? "",
|
|
1874
|
+
...op.messageId !== void 0 && { messageId: op.messageId }
|
|
1875
|
+
} : {
|
|
1876
|
+
operationId,
|
|
1877
|
+
session,
|
|
1878
|
+
status: "unanswered",
|
|
1879
|
+
...op.reason !== void 0 && { reason: op.reason }
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
function _tables() {
|
|
1883
|
+
_classPrivateFieldGet2(_records, this) ?? _classPrivateFieldSet2(_records, this, new OperationRecords(this.lifecycle.storage));
|
|
1884
|
+
return _classPrivateFieldGet2(_records, this);
|
|
1885
|
+
}
|
|
1886
|
+
/** The Sessions handle for a session, configured on first use. */
|
|
1887
|
+
/**
|
|
1888
|
+
* Give the Sessions and Streams the harness owns this capability's
|
|
1889
|
+
* Lifecycle services. Done on first use, not only in `onStart`, because an
|
|
1890
|
+
* RPC can read a transcript before startup (the read itself then waits
|
|
1891
|
+
* for startup).
|
|
1892
|
+
*/
|
|
1893
|
+
function _bindOwned() {
|
|
1894
|
+
if (_classPrivateFieldGet2(_bound, this)) return;
|
|
1895
|
+
bindLifecycleCapability(_classPrivateFieldGet2(_sessions, this), this.lifecycle);
|
|
1896
|
+
bindLifecycleCapability(_classPrivateFieldGet2(_streams, this), this.lifecycle);
|
|
1897
|
+
_classPrivateFieldSet2(_bound, this, true);
|
|
1898
|
+
_classPrivateFieldGet2(_sessions, this).subscribe((change) => {
|
|
1899
|
+
const session = change.sessionId;
|
|
1900
|
+
switch (change.type) {
|
|
1901
|
+
case "append":
|
|
1902
|
+
case "update":
|
|
1903
|
+
case "import":
|
|
1904
|
+
if (_assertClassBrand(_ThinkHarness_brand, this, _takeOwn).call(this, session, change.message.id)) return;
|
|
1905
|
+
_classPrivateFieldGet2(_events, this).emit(session, {
|
|
1906
|
+
type: "message",
|
|
1907
|
+
message: asUIMessage(change.message)
|
|
1908
|
+
});
|
|
1909
|
+
return;
|
|
1910
|
+
case "clear":
|
|
1911
|
+
if ((_classPrivateFieldGet2(_ownClears, this).get(session) ?? 0) > 0) return;
|
|
1912
|
+
_classPrivateFieldGet2(_events, this).emit(session, { type: "reset" });
|
|
1913
|
+
return;
|
|
1914
|
+
case "delete":
|
|
1915
|
+
case "compact":
|
|
1916
|
+
case "compaction":
|
|
1917
|
+
_classPrivateFieldGet2(_events, this).emit(session, { type: "transcript" });
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
/** Note messages the harness is about to write; returns the release. */
|
|
1923
|
+
function _markOwn(session, ids) {
|
|
1924
|
+
let own = _classPrivateFieldGet2(_ownMessages, this).get(session);
|
|
1925
|
+
if (!own) {
|
|
1926
|
+
own = /* @__PURE__ */ new Map();
|
|
1927
|
+
_classPrivateFieldGet2(_ownMessages, this).set(session, own);
|
|
1928
|
+
}
|
|
1929
|
+
for (const id of ids) own.set(id, (own.get(id) ?? 0) + 1);
|
|
1930
|
+
return () => {
|
|
1931
|
+
const current = _classPrivateFieldGet2(_ownMessages, this).get(session);
|
|
1932
|
+
if (!current) return;
|
|
1933
|
+
for (const id of ids) {
|
|
1934
|
+
const left = (current.get(id) ?? 1) - 1;
|
|
1935
|
+
if (left <= 0) current.delete(id);
|
|
1936
|
+
else current.set(id, left);
|
|
1937
|
+
}
|
|
1938
|
+
if (current.size === 0) _classPrivateFieldGet2(_ownMessages, this).delete(session);
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
/** Whether a change is one of the harness's own writes; consumes the mark. */
|
|
1942
|
+
function _takeOwn(session, id) {
|
|
1943
|
+
const own = _classPrivateFieldGet2(_ownMessages, this).get(session);
|
|
1944
|
+
const count = own?.get(id);
|
|
1945
|
+
if (!own || count === void 0) return false;
|
|
1946
|
+
if (count <= 1) own.delete(id);
|
|
1947
|
+
else own.set(id, count - 1);
|
|
1948
|
+
if (own.size === 0) _classPrivateFieldGet2(_ownMessages, this).delete(session);
|
|
1949
|
+
return true;
|
|
1950
|
+
}
|
|
1951
|
+
function _handle$1(session) {
|
|
1952
|
+
_assertClassBrand(_ThinkHarness_brand, this, _bindOwned).call(this);
|
|
1953
|
+
const handle = _classPrivateFieldGet2(_sessions, this).session(session);
|
|
1954
|
+
if (!_classPrivateFieldGet2(_configured, this).has(session)) {
|
|
1955
|
+
_classPrivateFieldGet2(_configured, this).add(session);
|
|
1956
|
+
_classPrivateFieldGet2(_options, this).configureSession?.(handle, session);
|
|
1957
|
+
}
|
|
1958
|
+
return handle;
|
|
1959
|
+
}
|
|
1960
|
+
async function _message(op) {
|
|
1961
|
+
if (op.messageId === void 0) return void 0;
|
|
1962
|
+
const message = await _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, op.session).getMessage(op.messageId);
|
|
1963
|
+
return message ? asUIMessages([message])[0] : void 0;
|
|
1964
|
+
}
|
|
1965
|
+
async function _tools(session) {
|
|
1966
|
+
const tools = await resolve(_classPrivateFieldGet2(_options, this).tools, session) ?? {};
|
|
1967
|
+
const client = createToolsFromClientSchemas(_assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).clientTools(session));
|
|
1968
|
+
for (const name of Object.keys(client)) if (Object.hasOwn(tools, name)) console.warn(`ThinkHarness ignored a client tool named "${name}": a server tool has that name`);
|
|
1969
|
+
return {
|
|
1970
|
+
...client,
|
|
1971
|
+
...tools
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Persist messages and operation changes in one transaction; with a
|
|
1976
|
+
* stream id, the transaction also settles and discards that stream (the
|
|
1977
|
+
* cutover), so an eviction leaves either the stream or the message.
|
|
1978
|
+
*/
|
|
1979
|
+
async function _commit(session, streamId, writes) {
|
|
1980
|
+
const sync = _assertClassBrand(_ThinkHarness_brand, this, _handle$1).call(this, session).__DO_NOT_USE_WILL_BREAK__sync();
|
|
1981
|
+
const afters = [];
|
|
1982
|
+
const ids = [];
|
|
1983
|
+
const commit = () => {
|
|
1984
|
+
afters.length = 0;
|
|
1985
|
+
ids.length = 0;
|
|
1986
|
+
writes((message, options = {}) => {
|
|
1987
|
+
ids.push(message.id);
|
|
1988
|
+
afters.push(sync.upsert(asSessionMessage(message), {
|
|
1989
|
+
...options.parentId !== void 0 && { parentId: options.parentId },
|
|
1990
|
+
source: options.source ?? "server"
|
|
1991
|
+
}).after);
|
|
1992
|
+
});
|
|
1993
|
+
};
|
|
1994
|
+
try {
|
|
1995
|
+
if (!(streamId !== void 0 && _classPrivateFieldGet2(_streams, this).__DO_NOT_USE_WILL_BREAK__sync().settle(streamId, "completed", null, {
|
|
1996
|
+
commit,
|
|
1997
|
+
discard: true
|
|
1998
|
+
}))) this.lifecycle.storage.transactionSync(commit);
|
|
1999
|
+
} catch (error) {
|
|
2000
|
+
sync.abandon();
|
|
2001
|
+
throw error;
|
|
2002
|
+
}
|
|
2003
|
+
const release = _assertClassBrand(_ThinkHarness_brand, this, _markOwn).call(this, session, ids);
|
|
2004
|
+
try {
|
|
2005
|
+
for (const after of afters) await after();
|
|
2006
|
+
} finally {
|
|
2007
|
+
release();
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
/** Every chunk a stream holds, without waiting for more. */
|
|
2011
|
+
function _streamChunks(streamId) {
|
|
2012
|
+
const streams = _classPrivateFieldGet2(_streams, this).__DO_NOT_USE_WILL_BREAK__sync();
|
|
2013
|
+
if (!streams.getStream(streamId)) return [];
|
|
2014
|
+
const chunks = [];
|
|
2015
|
+
let from = 0;
|
|
2016
|
+
for (;;) {
|
|
2017
|
+
const rows = streams.readChunks(streamId, from, READ_PAGE);
|
|
2018
|
+
for (const row of rows) {
|
|
2019
|
+
const segment = JSON.parse(row.chunk);
|
|
2020
|
+
chunks.push(...segment);
|
|
2021
|
+
}
|
|
2022
|
+
if (rows.length < READ_PAGE) return chunks;
|
|
2023
|
+
from = (rows.at(-1)?.seq ?? from) + 1;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
/** Chunks an evicted model call left in its stream, for a late watcher. */
|
|
2027
|
+
function _recoverableChunks(session) {
|
|
2028
|
+
const running = _assertClassBrand(_ThinkHarness_brand, this, _tables).call(this).open(session).find((op) => op.status === "running");
|
|
2029
|
+
return running?.streamId ? _assertClassBrand(_ThinkHarness_brand, this, _streamChunks).call(this, running.streamId) : [];
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Rebuild a message from the chunks of a model call, applied on top of the
|
|
2033
|
+
* message as it was before the call.
|
|
2034
|
+
*/
|
|
2035
|
+
function accumulate(messageId, existing, chunks) {
|
|
2036
|
+
if (chunks.length === 0) return existing;
|
|
2037
|
+
const accumulator = new StreamAccumulator({
|
|
2038
|
+
messageId: existing?.id ?? messageId,
|
|
2039
|
+
continuation: existing !== void 0,
|
|
2040
|
+
...existing && {
|
|
2041
|
+
existingParts: existing.parts,
|
|
2042
|
+
...isRecord(existing.metadata) && { existingMetadata: existing.metadata }
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
for (const chunk of chunks) accumulator.applyChunk(chunk);
|
|
2046
|
+
return {
|
|
2047
|
+
...accumulator.toMessage(),
|
|
2048
|
+
role: "assistant"
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Stop iterating as soon as `signal` aborts, even if the source never
|
|
2053
|
+
* notices: a model stream that ignores its abort signal must not hold an
|
|
2054
|
+
* aborted turn open.
|
|
2055
|
+
*/
|
|
2056
|
+
async function* untilAborted(source, signal) {
|
|
2057
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
2058
|
+
let onAbort = () => {};
|
|
2059
|
+
const aborted = new Promise((resolveAbort) => {
|
|
2060
|
+
onAbort = () => resolveAbort("aborted");
|
|
2061
|
+
if (signal.aborted) onAbort();
|
|
2062
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2063
|
+
});
|
|
2064
|
+
try {
|
|
2065
|
+
for (;;) {
|
|
2066
|
+
const next = await Promise.race([iterator.next(), aborted]);
|
|
2067
|
+
if (next === "aborted") {
|
|
2068
|
+
iterator.return?.().catch(() => {});
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
if (next.done) return;
|
|
2072
|
+
yield next.value;
|
|
2073
|
+
}
|
|
2074
|
+
} finally {
|
|
2075
|
+
signal.removeEventListener("abort", onAbort);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Classify the context-window overflow errors of common providers
|
|
2080
|
+
* (Anthropic, OpenAI, Google, Bedrock, Mistral and others) as
|
|
2081
|
+
* `"context-overflow"`, and everything else as `"fail"`. Pass it as
|
|
2082
|
+
* `hooks.classifyError`, or call it from your own classifier.
|
|
2083
|
+
*
|
|
2084
|
+
* @param error - The model call's error.
|
|
2085
|
+
* @returns `"context-overflow"` or `"fail"`.
|
|
2086
|
+
* @experimental
|
|
2087
|
+
*/
|
|
2088
|
+
function classifyContextOverflow(error) {
|
|
2089
|
+
let text;
|
|
2090
|
+
if (error instanceof Error) text = error.message;
|
|
2091
|
+
else if (typeof error === "string") text = error;
|
|
2092
|
+
else try {
|
|
2093
|
+
text = JSON.stringify(error);
|
|
2094
|
+
} catch {
|
|
2095
|
+
text = String(error);
|
|
2096
|
+
}
|
|
2097
|
+
return CONTEXT_OVERFLOW_PATTERN.test(text) ? "context-overflow" : "fail";
|
|
2098
|
+
}
|
|
2099
|
+
const CONTEXT_OVERFLOW_PATTERN = /prompt is too long|context[_ ]length[_ ]exceeded|maximum context length|exceeds the maximum number of tokens|input token count|reduce the length of|input is too long|too many (?:input )?tokens|context window/i;
|
|
2100
|
+
/** A tool's own recovery field, or the default. */
|
|
2101
|
+
function toolRecovery(tool) {
|
|
2102
|
+
return "recovery" in tool && tool.recovery === "rerun" ? "rerun" : "report";
|
|
2103
|
+
}
|
|
2104
|
+
function isRecord(value) {
|
|
2105
|
+
return typeof value === "object" && value !== null;
|
|
2106
|
+
}
|
|
2107
|
+
function backoff(attempt, baseMs) {
|
|
2108
|
+
if (attempt <= 1) return 0;
|
|
2109
|
+
return Math.min(baseMs * 2 ** (attempt - 2), MAX_BACKOFF_MS);
|
|
2110
|
+
}
|
|
2111
|
+
function streamTag(session, operationId) {
|
|
2112
|
+
return `think:${JSON.stringify([session, operationId])}`;
|
|
2113
|
+
}
|
|
2114
|
+
function waiterKey(session, operationId) {
|
|
2115
|
+
return JSON.stringify([session, operationId]);
|
|
2116
|
+
}
|
|
2117
|
+
/** Project a session event onto the shared harness interface's events. */
|
|
2118
|
+
function toSessionEvents(event) {
|
|
2119
|
+
switch (event.type) {
|
|
2120
|
+
case "operation": return [{
|
|
2121
|
+
type: "operation",
|
|
2122
|
+
status: event.status
|
|
2123
|
+
}];
|
|
2124
|
+
case "run-start": return [{
|
|
2125
|
+
type: "run-start",
|
|
2126
|
+
operations: [event.operationId]
|
|
2127
|
+
}];
|
|
2128
|
+
case "run-end": return [{
|
|
2129
|
+
type: "run-end",
|
|
2130
|
+
operations: [event.operationId]
|
|
2131
|
+
}];
|
|
2132
|
+
case "chunk": {
|
|
2133
|
+
const chunk = toResponseChunk(event.chunk, {});
|
|
2134
|
+
return chunk ? [{
|
|
2135
|
+
type: "chunk",
|
|
2136
|
+
chunk
|
|
2137
|
+
}] : [];
|
|
2138
|
+
}
|
|
2139
|
+
case "message": return [{
|
|
2140
|
+
type: "message",
|
|
2141
|
+
message: toTranscriptMessage(event.message)
|
|
2142
|
+
}];
|
|
2143
|
+
case "reset": return [{ type: "reset" }];
|
|
2144
|
+
case "transcript": return [];
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
var _harness$1 = /* @__PURE__ */ new WeakMap();
|
|
2148
|
+
/**
|
|
2149
|
+
* One session, addressed through the harness.
|
|
2150
|
+
*
|
|
2151
|
+
* @experimental
|
|
2152
|
+
*/
|
|
2153
|
+
var ThinkSession = class {
|
|
2154
|
+
/**
|
|
2155
|
+
* @param harness - The harness that owns the session.
|
|
2156
|
+
* @param id - The session's id.
|
|
2157
|
+
*/
|
|
2158
|
+
constructor(harness, id) {
|
|
2159
|
+
_classPrivateFieldInitSpec(this, _harness$1, void 0);
|
|
2160
|
+
_classPrivateFieldSet2(_harness$1, this, harness);
|
|
2161
|
+
this.id = id;
|
|
2162
|
+
}
|
|
2163
|
+
/** Durably submit input. Resolves before the model runs. */
|
|
2164
|
+
submit(input, options = {}) {
|
|
2165
|
+
const operationId = options.operationId ?? crypto.randomUUID();
|
|
2166
|
+
return _classPrivateFieldGet2(_harness$1, this).enqueue(this.id, operationInput(input, operationId, options.autoContinue ?? true), {
|
|
2167
|
+
...options,
|
|
2168
|
+
operationId
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
/** Submit and wait for the answer and the updated transcript. */
|
|
2172
|
+
async prompt(input, options = {}) {
|
|
2173
|
+
const receipt = await this.submit(input, options);
|
|
2174
|
+
return {
|
|
2175
|
+
...await this.wait(receipt.operationId),
|
|
2176
|
+
messages: await this.messages()
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Answer a user message again. The new answer is a branch beside the
|
|
2181
|
+
* earlier ones, which `Session.getBranches()` still reads.
|
|
2182
|
+
*
|
|
2183
|
+
* @param messageId - The user message. Default: the latest one.
|
|
2184
|
+
*/
|
|
2185
|
+
regenerate(messageId, options = {}) {
|
|
2186
|
+
return _classPrivateFieldGet2(_harness$1, this).enqueue(this.id, {
|
|
2187
|
+
kind: "regenerate",
|
|
2188
|
+
...messageId !== void 0 && { messageId }
|
|
2189
|
+
}, options);
|
|
2190
|
+
}
|
|
2191
|
+
/** Call the model again to continue the latest message. */
|
|
2192
|
+
continue(options = {}) {
|
|
2193
|
+
return _classPrivateFieldGet2(_harness$1, this).enqueue(this.id, { kind: "continue" }, options);
|
|
2194
|
+
}
|
|
2195
|
+
/** Wait for an operation to settle. Aborting `signal` stops only the wait. */
|
|
2196
|
+
wait(operationId, signal) {
|
|
2197
|
+
return _classPrivateFieldGet2(_harness$1, this).wait(this.id, operationId, signal);
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Withdraw one queued operation or abort it while it runs; with no id,
|
|
2201
|
+
* everything open in the session.
|
|
2202
|
+
*/
|
|
2203
|
+
abort(operationId) {
|
|
2204
|
+
return _classPrivateFieldGet2(_harness$1, this).abort(this.id, operationId);
|
|
2205
|
+
}
|
|
2206
|
+
/** Abort everything and clear the transcript, optionally leaving a note. */
|
|
2207
|
+
reset(handoff) {
|
|
2208
|
+
return _classPrivateFieldGet2(_harness$1, this).reset(this.id, handoff);
|
|
2209
|
+
}
|
|
2210
|
+
/** The active transcript. */
|
|
2211
|
+
messages() {
|
|
2212
|
+
return _classPrivateFieldGet2(_harness$1, this).messages(this.id);
|
|
2213
|
+
}
|
|
2214
|
+
/** Operations not settled yet, oldest first. */
|
|
2215
|
+
pending() {
|
|
2216
|
+
return _classPrivateFieldGet2(_harness$1, this).pending(this.id);
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* The running operation, with the chunks its model call has streamed so
|
|
2220
|
+
* far, including those an eviction left behind. Send them, then
|
|
2221
|
+
* `subscribe()`'s chunk events, to a client that joins mid-turn.
|
|
2222
|
+
*/
|
|
2223
|
+
inFlight() {
|
|
2224
|
+
return _classPrivateFieldGet2(_harness$1, this).inFlight(this.id);
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* The answers to a message: its children in the transcript tree. A
|
|
2228
|
+
* regenerated answer is a branch beside the earlier ones.
|
|
2229
|
+
*
|
|
2230
|
+
* @param messageId - The message, usually a user message.
|
|
2231
|
+
*/
|
|
2232
|
+
branches(messageId) {
|
|
2233
|
+
return _classPrivateFieldGet2(_harness$1, this).branches(this.id, messageId);
|
|
2234
|
+
}
|
|
2235
|
+
/** Full-text search over this session's messages. */
|
|
2236
|
+
search(query, options) {
|
|
2237
|
+
return _classPrivateFieldGet2(_harness$1, this).search(this.id, query, options);
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Summarize older messages with the compaction function set in
|
|
2241
|
+
* `configureSession`. Resolves to null when there was nothing to compact.
|
|
2242
|
+
*/
|
|
2243
|
+
compact() {
|
|
2244
|
+
return _classPrivateFieldGet2(_harness$1, this).compact(this.id);
|
|
2245
|
+
}
|
|
2246
|
+
/** Listen to the session's events in the AI SDK's vocabulary. */
|
|
2247
|
+
subscribe(listener) {
|
|
2248
|
+
return _classPrivateFieldGet2(_harness$1, this).subscribe(this.id, listener);
|
|
2249
|
+
}
|
|
2250
|
+
/** The shared harness interface's watch: the state, then every change. */
|
|
2251
|
+
watch() {
|
|
2252
|
+
return _classPrivateFieldGet2(_harness$1, this).watch(this.id);
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Submit input and stream its answer to `callback`, as Think's `chat()`
|
|
2256
|
+
* does: `onStart`, then `onEvent` with each UI chunk as JSON, then
|
|
2257
|
+
* `onDone` or `onError`. Works over RPC with an `RpcTarget` callback.
|
|
2258
|
+
*
|
|
2259
|
+
* The callback lives in memory. If the object is evicted mid-turn the
|
|
2260
|
+
* turn still finishes, but the callback is gone; a caller that must not
|
|
2261
|
+
* lose the answer should `wait()` on the receipt as well.
|
|
2262
|
+
*/
|
|
2263
|
+
async chat(input, callback, options = {}) {
|
|
2264
|
+
const operationId = options.operationId ?? crypto.randomUUID();
|
|
2265
|
+
let delivered = Promise.resolve();
|
|
2266
|
+
const deliver = (fn) => {
|
|
2267
|
+
delivered = delivered.then(fn).catch((error) => {
|
|
2268
|
+
console.error("ThinkHarness chat callback failed", error);
|
|
2269
|
+
});
|
|
2270
|
+
};
|
|
2271
|
+
const unsubscribe = this.subscribe((event) => {
|
|
2272
|
+
if (event.type === "chunk" && event.operationId === operationId) deliver(() => callback.onEvent(JSON.stringify(event.chunk)));
|
|
2273
|
+
});
|
|
2274
|
+
try {
|
|
2275
|
+
const receipt = await this.submit(input, {
|
|
2276
|
+
...options,
|
|
2277
|
+
operationId
|
|
2278
|
+
});
|
|
2279
|
+
deliver(() => callback.onStart?.({ operationId: receipt.operationId }));
|
|
2280
|
+
const result = await this.wait(receipt.operationId);
|
|
2281
|
+
unsubscribe();
|
|
2282
|
+
await delivered;
|
|
2283
|
+
if (result.status === "done") await callback.onDone();
|
|
2284
|
+
else await callback.onError(result.reason ?? "unanswered");
|
|
2285
|
+
return result;
|
|
2286
|
+
} finally {
|
|
2287
|
+
unsubscribe();
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
/** One operation's status, or undefined when the id is unknown. */
|
|
2291
|
+
inspect(operationId) {
|
|
2292
|
+
return _classPrivateFieldGet2(_harness$1, this).inspect(this.id, operationId);
|
|
2293
|
+
}
|
|
2294
|
+
/** Whether the session has open work. */
|
|
2295
|
+
async busy() {
|
|
2296
|
+
return (await this.pending()).length > 0;
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
var _harness2 = /* @__PURE__ */ new WeakMap();
|
|
2300
|
+
/**
|
|
2301
|
+
* Every session in this object.
|
|
2302
|
+
*
|
|
2303
|
+
* @experimental
|
|
2304
|
+
*/
|
|
2305
|
+
var ThinkSessions = class {
|
|
2306
|
+
/** @param harness - The harness that owns the sessions. */
|
|
2307
|
+
constructor(harness) {
|
|
2308
|
+
_classPrivateFieldInitSpec(this, _harness2, void 0);
|
|
2309
|
+
_classPrivateFieldSet2(_harness2, this, harness);
|
|
2310
|
+
}
|
|
2311
|
+
/** A handle on one session. */
|
|
2312
|
+
get(id) {
|
|
2313
|
+
return _classPrivateFieldGet2(_harness2, this).session(id);
|
|
2314
|
+
}
|
|
2315
|
+
/** A new, empty session. */
|
|
2316
|
+
create() {
|
|
2317
|
+
return _classPrivateFieldGet2(_harness2, this).createSession();
|
|
2318
|
+
}
|
|
2319
|
+
/** A new session that starts with `from`'s active transcript. */
|
|
2320
|
+
fork(from) {
|
|
2321
|
+
return _classPrivateFieldGet2(_harness2, this).fork(from);
|
|
2322
|
+
}
|
|
2323
|
+
/** Every session, with whether it has open work. */
|
|
2324
|
+
list() {
|
|
2325
|
+
return _classPrivateFieldGet2(_harness2, this).listSessions();
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
//#endregion
|
|
2329
|
+
//#region src/harness/think/chat.ts
|
|
2330
|
+
/** `WebSocket.OPEN`; the constant is not defined on every runtime's global. */
|
|
2331
|
+
const OPEN = 1;
|
|
2332
|
+
function send(connection, frame) {
|
|
2333
|
+
if (connection.readyState !== OPEN) return;
|
|
2334
|
+
try {
|
|
2335
|
+
connection.send(JSON.stringify(frame));
|
|
2336
|
+
} catch {}
|
|
2337
|
+
}
|
|
2338
|
+
function parseBody(body) {
|
|
2339
|
+
if (typeof body !== "string" || body === "") return {};
|
|
2340
|
+
try {
|
|
2341
|
+
const parsed = JSON.parse(body);
|
|
2342
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
2343
|
+
} catch {
|
|
2344
|
+
return {};
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
function clientToolsOf(value) {
|
|
2348
|
+
return value.map((tool) => ({
|
|
2349
|
+
name: tool.name,
|
|
2350
|
+
...tool.description !== void 0 && { description: tool.description },
|
|
2351
|
+
...tool.parameters !== void 0 && { parameters: tool.parameters }
|
|
2352
|
+
}));
|
|
2353
|
+
}
|
|
2354
|
+
var _harness = /* @__PURE__ */ new WeakMap();
|
|
2355
|
+
var _webSockets = /* @__PURE__ */ new WeakMap();
|
|
2356
|
+
var _session = /* @__PURE__ */ new WeakMap();
|
|
2357
|
+
var _framing = /* @__PURE__ */ new WeakMap();
|
|
2358
|
+
var _awaitingAck = /* @__PURE__ */ new WeakMap();
|
|
2359
|
+
var _replayed = /* @__PURE__ */ new WeakMap();
|
|
2360
|
+
var _requesters = /* @__PURE__ */ new WeakMap();
|
|
2361
|
+
var _pendingProbes = /* @__PURE__ */ new WeakMap();
|
|
2362
|
+
var _unobserve = /* @__PURE__ */ new WeakMap();
|
|
2363
|
+
var _ThinkChat_brand = /* @__PURE__ */ new WeakSet();
|
|
2364
|
+
/**
|
|
2365
|
+
* Serves one ThinkHarness session over Think's chat WebSocket protocol and
|
|
2366
|
+
* its `GET …/get-messages` route. Install it on the Lifecycle after the
|
|
2367
|
+
* harness and the WebSockets capability:
|
|
2368
|
+
*
|
|
2369
|
+
* ```ts
|
|
2370
|
+
* readonly webSockets = new WebSockets();
|
|
2371
|
+
* readonly chat = new ThinkChat({ harness: this.harness, webSockets: this.webSockets });
|
|
2372
|
+
* readonly lifecycle = Lifecycle.install(this)
|
|
2373
|
+
* .use(this.harness)
|
|
2374
|
+
* .use(this.webSockets).use(this.chat);
|
|
2375
|
+
* ```
|
|
2376
|
+
*
|
|
2377
|
+
* Supported: chat requests (`submit-message` and `regenerate-message`),
|
|
2378
|
+
* cancel, clear, client tool results, approvals, and stream resume after
|
|
2379
|
+
* a reconnect or an eviction. Not supported: `messageConcurrency`
|
|
2380
|
+
* strategies other than queueing, client transcript sync, and the
|
|
2381
|
+
* recovery progress frame.
|
|
2382
|
+
*
|
|
2383
|
+
* @experimental The API may change between releases.
|
|
2384
|
+
*/
|
|
2385
|
+
var ThinkChat = class extends LifecycleCapability {
|
|
2386
|
+
/** @param options - The harness, its WebSockets, and the session to serve. */
|
|
2387
|
+
constructor(options) {
|
|
2388
|
+
super("think-chat");
|
|
2389
|
+
_classPrivateMethodInitSpec(this, _ThinkChat_brand);
|
|
2390
|
+
_classPrivateFieldInitSpec(this, _harness, void 0);
|
|
2391
|
+
_classPrivateFieldInitSpec(this, _webSockets, void 0);
|
|
2392
|
+
_classPrivateFieldInitSpec(this, _session, void 0);
|
|
2393
|
+
_classPrivateFieldInitSpec(this, _framing, /* @__PURE__ */ new Map());
|
|
2394
|
+
_classPrivateFieldInitSpec(this, _awaitingAck, /* @__PURE__ */ new Set());
|
|
2395
|
+
_classPrivateFieldInitSpec(this, _replayed, /* @__PURE__ */ new Map());
|
|
2396
|
+
_classPrivateFieldInitSpec(this, _requesters, /* @__PURE__ */ new Map());
|
|
2397
|
+
_classPrivateFieldInitSpec(this, _pendingProbes, /* @__PURE__ */ new Map());
|
|
2398
|
+
_classPrivateFieldInitSpec(this, _unobserve, void 0);
|
|
2399
|
+
_classPrivateFieldSet2(_harness, this, options.harness);
|
|
2400
|
+
_classPrivateFieldSet2(_webSockets, this, options.webSockets);
|
|
2401
|
+
_classPrivateFieldSet2(_session, this, options.session ?? "");
|
|
2402
|
+
_classPrivateFieldGet2(_webSockets, this).use({
|
|
2403
|
+
onConnect: (connection) => _assertClassBrand(_ThinkChat_brand, this, _onConnect).call(this, connection),
|
|
2404
|
+
onMessage: (connection, message) => _assertClassBrand(_ThinkChat_brand, this, _onMessage).call(this, connection, message),
|
|
2405
|
+
onClose: (connection) => _assertClassBrand(_ThinkChat_brand, this, _forget).call(this, connection),
|
|
2406
|
+
onError: (connection) => _assertClassBrand(_ThinkChat_brand, this, _forget).call(this, connection)
|
|
2407
|
+
});
|
|
2408
|
+
_classPrivateFieldSet2(_unobserve, this, _classPrivateFieldGet2(_harness, this).observe((session, event) => {
|
|
2409
|
+
if (session === _classPrivateFieldGet2(_session, this)) _assertClassBrand(_ThinkChat_brand, this, _onEvent).call(this, event);
|
|
2410
|
+
}));
|
|
2411
|
+
}
|
|
2412
|
+
/** Answer `GET …/get-messages` with the session's transcript. */
|
|
2413
|
+
async onRequest(context) {
|
|
2414
|
+
const { request } = context;
|
|
2415
|
+
if (request.method !== "GET") return void 0;
|
|
2416
|
+
if (!new URL(request.url).pathname.endsWith("/get-messages")) return;
|
|
2417
|
+
return Response.json(await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).messages());
|
|
2418
|
+
}
|
|
2419
|
+
/** Stop listening to the harness. */
|
|
2420
|
+
dispose() {
|
|
2421
|
+
_classPrivateFieldGet2(_unobserve, this).call(this);
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
async function _onConnect(connection) {
|
|
2425
|
+
const session = _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this));
|
|
2426
|
+
send(connection, {
|
|
2427
|
+
type: CHAT_MESSAGE_TYPES.CHAT_MESSAGES,
|
|
2428
|
+
messages: await session.messages(),
|
|
2429
|
+
connect: true
|
|
2430
|
+
});
|
|
2431
|
+
const inFlight = await session.inFlight();
|
|
2432
|
+
if (inFlight) _assertClassBrand(_ThinkChat_brand, this, _offerResume).call(this, connection, inFlight.operationId);
|
|
2433
|
+
}
|
|
2434
|
+
function _forget(connection) {
|
|
2435
|
+
_classPrivateFieldGet2(_awaitingAck, this).delete(connection.id);
|
|
2436
|
+
_classPrivateFieldGet2(_pendingProbes, this).delete(connection.id);
|
|
2437
|
+
_classPrivateFieldGet2(_replayed, this).delete(connection.id);
|
|
2438
|
+
}
|
|
2439
|
+
function _offerResume(connection, operationId, probeId) {
|
|
2440
|
+
_classPrivateFieldGet2(_awaitingAck, this).add(connection.id);
|
|
2441
|
+
send(connection, {
|
|
2442
|
+
type: CHAT_MESSAGE_TYPES.STREAM_RESUMING,
|
|
2443
|
+
id: operationId,
|
|
2444
|
+
...probeId !== void 0 && { probeId }
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
async function _onMessage(connection, raw) {
|
|
2448
|
+
if (typeof raw !== "string") return false;
|
|
2449
|
+
const event = parseProtocolMessage(raw);
|
|
2450
|
+
if (!event) return false;
|
|
2451
|
+
await _assertClassBrand(_ThinkChat_brand, this, _handle).call(this, connection, event);
|
|
2452
|
+
return true;
|
|
2453
|
+
}
|
|
2454
|
+
async function _handle(connection, event) {
|
|
2455
|
+
const session = _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this));
|
|
2456
|
+
switch (event.type) {
|
|
2457
|
+
case "chat-request": {
|
|
2458
|
+
const body = parseBody(event.init.body);
|
|
2459
|
+
const clientTools = body.clientTools ? { clientTools: body.clientTools } : {};
|
|
2460
|
+
_classPrivateFieldGet2(_requesters, this).set(event.id, connection.id);
|
|
2461
|
+
if (body.trigger === "regenerate-message") {
|
|
2462
|
+
await session.regenerate(void 0, { operationId: event.id });
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
const known = new Set((await session.messages()).map((message) => message.id));
|
|
2466
|
+
const fresh = (body.messages ?? []).filter((message) => !known.has(message.id));
|
|
2467
|
+
if (!(await session.submit(fresh, {
|
|
2468
|
+
operationId: event.id,
|
|
2469
|
+
source: "client",
|
|
2470
|
+
...clientTools
|
|
2471
|
+
})).accepted) {
|
|
2472
|
+
const status = await session.inspect(event.id);
|
|
2473
|
+
if (status?.status === "done" || status?.status === "unanswered") {
|
|
2474
|
+
_classPrivateFieldGet2(_requesters, this).delete(event.id);
|
|
2475
|
+
await _assertClassBrand(_ThinkChat_brand, this, _finish).call(this, {
|
|
2476
|
+
operationId: status.operationId,
|
|
2477
|
+
status: status.status,
|
|
2478
|
+
..."reason" in status && status.reason !== void 0 && { reason: status.reason }
|
|
2479
|
+
}, void 0, connection);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
case "cancel":
|
|
2485
|
+
await session.abort(event.id);
|
|
2486
|
+
return;
|
|
2487
|
+
case "clear":
|
|
2488
|
+
await session.reset();
|
|
2489
|
+
for (const other of _classPrivateFieldGet2(_webSockets, this).getConnections()) if (other.id !== connection.id) send(other, { type: CHAT_MESSAGE_TYPES.CHAT_CLEAR });
|
|
2490
|
+
return;
|
|
2491
|
+
case "tool-result": {
|
|
2492
|
+
const operationId = crypto.randomUUID();
|
|
2493
|
+
_classPrivateFieldGet2(_requesters, this).set(operationId, connection.id);
|
|
2494
|
+
await session.submit({
|
|
2495
|
+
type: "tool-result",
|
|
2496
|
+
toolCallId: event.toolCallId,
|
|
2497
|
+
result: event.state === "output-error" ? {
|
|
2498
|
+
ok: false,
|
|
2499
|
+
...event.errorText !== void 0 && { errorText: event.errorText }
|
|
2500
|
+
} : {
|
|
2501
|
+
ok: true,
|
|
2502
|
+
output: event.output ?? null
|
|
2503
|
+
}
|
|
2504
|
+
}, {
|
|
2505
|
+
operationId,
|
|
2506
|
+
source: "client",
|
|
2507
|
+
autoContinue: event.autoContinue ?? true,
|
|
2508
|
+
...event.clientTools && { clientTools: clientToolsOf(event.clientTools) }
|
|
2509
|
+
});
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
case "tool-approval": {
|
|
2513
|
+
const approvalId = await _assertClassBrand(_ThinkChat_brand, this, _approvalId).call(this, event.toolCallId);
|
|
2514
|
+
if (approvalId === void 0) return;
|
|
2515
|
+
const operationId = crypto.randomUUID();
|
|
2516
|
+
_classPrivateFieldGet2(_requesters, this).set(operationId, connection.id);
|
|
2517
|
+
await session.submit({
|
|
2518
|
+
type: "approval",
|
|
2519
|
+
approvalId,
|
|
2520
|
+
approved: event.approved
|
|
2521
|
+
}, {
|
|
2522
|
+
operationId,
|
|
2523
|
+
source: "client",
|
|
2524
|
+
autoContinue: event.autoContinue ?? true
|
|
2525
|
+
});
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
case "stream-resume-request": {
|
|
2529
|
+
const inFlight = await session.inFlight();
|
|
2530
|
+
if (inFlight) _assertClassBrand(_ThinkChat_brand, this, _offerResume).call(this, connection, inFlight.operationId, event.probeId);
|
|
2531
|
+
else if ((await session.pending()).length > 0) {
|
|
2532
|
+
_classPrivateFieldGet2(_pendingProbes, this).set(connection.id, event.probeId);
|
|
2533
|
+
send(connection, {
|
|
2534
|
+
type: CHAT_MESSAGE_TYPES.STREAM_PENDING,
|
|
2535
|
+
...event.probeId !== void 0 && { probeId: event.probeId }
|
|
2536
|
+
});
|
|
2537
|
+
} else send(connection, {
|
|
2538
|
+
type: CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE,
|
|
2539
|
+
reason: STREAM_RESUME_NONE_REASONS.IDLE,
|
|
2540
|
+
...event.probeId !== void 0 && { probeId: event.probeId }
|
|
2541
|
+
});
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
case "stream-resume-ack":
|
|
2545
|
+
await _assertClassBrand(_ThinkChat_brand, this, _replay).call(this, connection, event.id);
|
|
2546
|
+
return;
|
|
2547
|
+
case "messages": return;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
/** Replay a running model call's chunks to one connection, then go live. */
|
|
2551
|
+
async function _replay(connection, operationId) {
|
|
2552
|
+
const inFlight = await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).inFlight();
|
|
2553
|
+
if (!inFlight || inFlight.operationId !== operationId) {
|
|
2554
|
+
_classPrivateFieldGet2(_awaitingAck, this).delete(connection.id);
|
|
2555
|
+
send(connection, {
|
|
2556
|
+
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
|
|
2557
|
+
id: operationId,
|
|
2558
|
+
body: "",
|
|
2559
|
+
done: true,
|
|
2560
|
+
replay: true
|
|
2561
|
+
});
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
const { continuation, chunks } = inFlight;
|
|
2565
|
+
let seq = 0;
|
|
2566
|
+
for (const chunk of chunks) send(connection, {
|
|
2567
|
+
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
|
|
2568
|
+
id: operationId,
|
|
2569
|
+
body: JSON.stringify(chunk),
|
|
2570
|
+
done: false,
|
|
2571
|
+
replay: true,
|
|
2572
|
+
seq: seq++,
|
|
2573
|
+
...continuation && { continuation: true }
|
|
2574
|
+
});
|
|
2575
|
+
send(connection, {
|
|
2576
|
+
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
|
|
2577
|
+
id: operationId,
|
|
2578
|
+
body: "",
|
|
2579
|
+
done: false,
|
|
2580
|
+
replay: true,
|
|
2581
|
+
replayComplete: true,
|
|
2582
|
+
...continuation && { continuation: true }
|
|
2583
|
+
});
|
|
2584
|
+
_classPrivateFieldGet2(_replayed, this).set(connection.id, {
|
|
2585
|
+
operationId,
|
|
2586
|
+
continuation
|
|
2587
|
+
});
|
|
2588
|
+
_classPrivateFieldGet2(_awaitingAck, this).delete(connection.id);
|
|
2589
|
+
}
|
|
2590
|
+
async function _approvalId(toolCallId) {
|
|
2591
|
+
const messages = await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).messages();
|
|
2592
|
+
for (const message of [...messages].reverse()) for (const part of message.parts) if ("toolCallId" in part && part.toolCallId === toolCallId && "approval" in part && part.approval) return part.approval.id;
|
|
2593
|
+
}
|
|
2594
|
+
function _onEvent(event) {
|
|
2595
|
+
switch (event.type) {
|
|
2596
|
+
case "run-start": {
|
|
2597
|
+
const requester = _classPrivateFieldGet2(_requesters, this).get(event.operationId);
|
|
2598
|
+
_classPrivateFieldGet2(_framing, this).set(event.operationId, {
|
|
2599
|
+
continuation: event.continuation,
|
|
2600
|
+
requester,
|
|
2601
|
+
finish: void 0,
|
|
2602
|
+
started: false
|
|
2603
|
+
});
|
|
2604
|
+
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) {
|
|
2605
|
+
if (!_classPrivateFieldGet2(_pendingProbes, this).has(connection.id)) continue;
|
|
2606
|
+
const probeId = _classPrivateFieldGet2(_pendingProbes, this).get(connection.id);
|
|
2607
|
+
_classPrivateFieldGet2(_pendingProbes, this).delete(connection.id);
|
|
2608
|
+
_assertClassBrand(_ThinkChat_brand, this, _offerResume).call(this, connection, event.operationId, probeId);
|
|
2609
|
+
}
|
|
2610
|
+
if (event.continuation && requester !== void 0) {
|
|
2611
|
+
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) if (connection.id === requester) _assertClassBrand(_ThinkChat_brand, this, _offerResume).call(this, connection, event.operationId);
|
|
2612
|
+
}
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
case "chunk": {
|
|
2616
|
+
const framing = _classPrivateFieldGet2(_framing, this).get(event.operationId);
|
|
2617
|
+
if (event.chunk.type === "finish") {
|
|
2618
|
+
if (framing) framing.finish = event.chunk;
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
if (event.chunk.type === "start" && framing) {
|
|
2622
|
+
if (framing.started) return;
|
|
2623
|
+
framing.started = true;
|
|
2624
|
+
}
|
|
2625
|
+
_assertClassBrand(_ThinkChat_brand, this, _broadcastChunk).call(this, event.operationId, event.chunk, framing);
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
case "message": {
|
|
2629
|
+
if (event.operationId !== void 0 && _classPrivateFieldGet2(_framing, this).has(event.operationId)) return;
|
|
2630
|
+
const requester = event.operationId === void 0 ? void 0 : _classPrivateFieldGet2(_requesters, this).get(event.operationId);
|
|
2631
|
+
_assertClassBrand(_ThinkChat_brand, this, _broadcastTranscript).call(this, requester);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
case "operation": {
|
|
2635
|
+
const { status } = event;
|
|
2636
|
+
if (status.status !== "done" && status.status !== "unanswered") return;
|
|
2637
|
+
const framing = _classPrivateFieldGet2(_framing, this).get(status.operationId);
|
|
2638
|
+
_classPrivateFieldGet2(_framing, this).delete(status.operationId);
|
|
2639
|
+
_classPrivateFieldGet2(_requesters, this).delete(status.operationId);
|
|
2640
|
+
_assertClassBrand(_ThinkChat_brand, this, _finish).call(this, status, framing).then(() => _assertClassBrand(_ThinkChat_brand, this, _resolveProbes).call(this));
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
case "transcript":
|
|
2644
|
+
_assertClassBrand(_ThinkChat_brand, this, _broadcastTranscript).call(this);
|
|
2645
|
+
return;
|
|
2646
|
+
case "reset":
|
|
2647
|
+
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) send(connection, { type: CHAT_MESSAGE_TYPES.CHAT_CLEAR });
|
|
2648
|
+
return;
|
|
2649
|
+
case "run-end": return;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
function _broadcastChunk(operationId, chunk, framing) {
|
|
2653
|
+
const body = JSON.stringify(chunk);
|
|
2654
|
+
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) {
|
|
2655
|
+
if (_classPrivateFieldGet2(_awaitingAck, this).has(connection.id)) continue;
|
|
2656
|
+
const replayed = _classPrivateFieldGet2(_replayed, this).get(connection.id);
|
|
2657
|
+
const continuation = replayed?.operationId === operationId ? replayed.continuation : framing?.continuation ?? false;
|
|
2658
|
+
send(connection, {
|
|
2659
|
+
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
|
|
2660
|
+
id: operationId,
|
|
2661
|
+
body,
|
|
2662
|
+
done: false,
|
|
2663
|
+
...continuation && { continuation: true }
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
async function _broadcastTranscript(except) {
|
|
2668
|
+
const messages = await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).messages();
|
|
2669
|
+
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) {
|
|
2670
|
+
if (connection.id === except) continue;
|
|
2671
|
+
send(connection, {
|
|
2672
|
+
type: CHAT_MESSAGE_TYPES.CHAT_MESSAGES,
|
|
2673
|
+
messages
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
/** Tell probes still waiting that nothing will stream, once the session is idle. */
|
|
2678
|
+
async function _resolveProbes() {
|
|
2679
|
+
if (_classPrivateFieldGet2(_pendingProbes, this).size === 0) return;
|
|
2680
|
+
if ((await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).pending()).length > 0) return;
|
|
2681
|
+
for (const connection of _classPrivateFieldGet2(_webSockets, this).getConnections()) {
|
|
2682
|
+
if (!_classPrivateFieldGet2(_pendingProbes, this).has(connection.id)) continue;
|
|
2683
|
+
const probeId = _classPrivateFieldGet2(_pendingProbes, this).get(connection.id);
|
|
2684
|
+
_classPrivateFieldGet2(_pendingProbes, this).delete(connection.id);
|
|
2685
|
+
send(connection, {
|
|
2686
|
+
type: CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE,
|
|
2687
|
+
reason: STREAM_RESUME_NONE_REASONS.IDLE,
|
|
2688
|
+
...probeId !== void 0 && { probeId }
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
/** The transcript first, then the terminal frame (#2119). */
|
|
2693
|
+
async function _finish(status, framing, only) {
|
|
2694
|
+
const { operationId } = status;
|
|
2695
|
+
if (framing?.finish) _assertClassBrand(_ThinkChat_brand, this, _broadcastChunk).call(this, operationId, framing.finish, framing);
|
|
2696
|
+
const messages = await _classPrivateFieldGet2(_harness, this).session(_classPrivateFieldGet2(_session, this)).messages();
|
|
2697
|
+
const reason = status.status === "unanswered" ? status.reason : void 0;
|
|
2698
|
+
const outcome = status.status === "done" ? "completed" : reason === "aborted" ? "aborted" : reason === "withdrawn" || reason === "not_waiting" || reason === "not_found" || reason === "empty" ? "skipped" : "error";
|
|
2699
|
+
const connections = only ? [only] : _classPrivateFieldGet2(_webSockets, this).getConnections();
|
|
2700
|
+
for (const connection of connections) {
|
|
2701
|
+
send(connection, {
|
|
2702
|
+
type: CHAT_MESSAGE_TYPES.CHAT_MESSAGES,
|
|
2703
|
+
messages
|
|
2704
|
+
});
|
|
2705
|
+
send(connection, {
|
|
2706
|
+
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
|
|
2707
|
+
id: operationId,
|
|
2708
|
+
body: outcome === "error" ? reason ?? "" : "",
|
|
2709
|
+
done: true,
|
|
2710
|
+
outcome,
|
|
2711
|
+
...outcome === "error" && { error: true },
|
|
2712
|
+
...framing?.continuation && { continuation: true }
|
|
2713
|
+
});
|
|
2714
|
+
_classPrivateFieldGet2(_awaitingAck, this).delete(connection.id);
|
|
2715
|
+
if (_classPrivateFieldGet2(_replayed, this).get(connection.id)?.operationId === operationId) _classPrivateFieldGet2(_replayed, this).delete(connection.id);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
//#endregion
|
|
2719
|
+
export { ROOT_SESSION, SteerNotSupportedError, ThinkChat, ThinkHarness, ThinkSession, ThinkSessions, classifyContextOverflow };
|
|
2720
|
+
|
|
2721
|
+
//# sourceMappingURL=index.js.map
|