@genesislcap/ai-assistant 15.86.1 → 15.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-assistant.api.json +5026 -645
- package/dist/ai-assistant.d.ts +802 -0
- package/dist/chat-driver.cjs +1487 -132
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +1480 -132
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +2779 -657
- package/dist/dts/autopilot/autopilot-session.d.ts +25 -0
- package/dist/dts/autopilot/autopilot-session.d.ts.map +1 -1
- package/dist/dts/chat-driver-node.d.ts +2 -0
- package/dist/dts/chat-driver-node.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +15 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +6 -0
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +7 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/interaction-controller.d.ts +12 -0
- package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
- package/dist/dts/headless/describe-interaction.d.ts +17 -0
- package/dist/dts/headless/describe-interaction.d.ts.map +1 -0
- package/dist/dts/headless/headless-debug.d.ts +28 -0
- package/dist/dts/headless/headless-debug.d.ts.map +1 -0
- package/dist/dts/headless/headless-persistence.d.ts +35 -0
- package/dist/dts/headless/headless-persistence.d.ts.map +1 -0
- package/dist/dts/headless/headless-session.d.ts +180 -0
- package/dist/dts/headless/headless-session.d.ts.map +1 -0
- package/dist/dts/headless/headless.types.d.ts +435 -0
- package/dist/dts/headless/headless.types.d.ts.map +1 -0
- package/dist/dts/headless/index.d.ts +5 -0
- package/dist/dts/headless/index.d.ts.map +1 -0
- package/dist/dts/headless-server/compact.d.ts +52 -0
- package/dist/dts/headless-server/compact.d.ts.map +1 -0
- package/dist/dts/headless-server/index.d.ts +23 -0
- package/dist/dts/headless-server/index.d.ts.map +1 -0
- package/dist/dts/headless-server/server.d.ts +13 -0
- package/dist/dts/headless-server/server.d.ts.map +1 -0
- package/dist/dts/headless-server/types.d.ts +210 -0
- package/dist/dts/headless-server/types.d.ts.map +1 -0
- package/dist/dts/index.d.ts +2 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/state/debug-event-log.d.ts +1 -1
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/dts/state/decision-surface.d.ts +5 -4
- package/dist/dts/state/decision-surface.d.ts.map +1 -1
- package/dist/dts/state/persistence/debug-log-assembly.d.ts +49 -0
- package/dist/dts/state/persistence/debug-log-assembly.d.ts.map +1 -0
- package/dist/dts/state/persistence/index.d.ts +1 -0
- package/dist/dts/state/persistence/index.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persister.d.ts +7 -0
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/dts/types/interaction-surface.d.ts +30 -0
- package/dist/dts/types/interaction-surface.d.ts.map +1 -1
- package/dist/esm/autopilot/autopilot-session.js +31 -16
- package/dist/esm/chat-driver-node.js +8 -0
- package/dist/esm/components/ai-driver/base-driver-decorator.js +12 -0
- package/dist/esm/components/chat-driver/chat-driver.js +8 -0
- package/dist/esm/components/chat-driver/interaction-controller.js +66 -0
- package/dist/esm/headless/describe-interaction.js +55 -0
- package/dist/esm/headless/headless-debug.js +62 -0
- package/dist/esm/headless/headless-persistence.js +192 -0
- package/dist/esm/headless/headless-session.js +808 -0
- package/dist/esm/headless/headless.types.js +1 -0
- package/dist/esm/headless/index.js +3 -0
- package/dist/esm/headless-server/compact.js +160 -0
- package/dist/esm/headless-server/index.js +21 -0
- package/dist/esm/headless-server/server.js +746 -0
- package/dist/esm/headless-server/types.js +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/main/main.js +18 -37
- package/dist/esm/state/debug-event-log.js +6 -1
- package/dist/esm/state/decision-surface.js +75 -48
- package/dist/esm/state/persistence/debug-log-assembly.js +69 -0
- package/dist/esm/state/persistence/index.js +1 -0
- package/dist/esm/state/persistence/session-persister.js +16 -0
- package/dist/headless-server.cjs +853 -0
- package/dist/headless-server.cjs.map +7 -0
- package/dist/headless-server.mjs +819 -0
- package/dist/headless-server.mjs.map +7 -0
- package/package.json +28 -18
- package/scripts/build-headless-server-node.mjs +42 -0
- package/scripts/smoke-chat-driver-node.mjs +3 -0
- package/scripts/smoke-headless-server-node.mjs +112 -0
package/dist/chat-driver.cjs
CHANGED
|
@@ -39,6 +39,7 @@ __export(chat_driver_node_exports, {
|
|
|
39
39
|
GEMINI_LONG_CONTEXT_THRESHOLD: () => GEMINI_LONG_CONTEXT_THRESHOLD,
|
|
40
40
|
GeminiProvider: () => GeminiProvider,
|
|
41
41
|
GeminiTransport: () => GeminiTransport,
|
|
42
|
+
HeadlessSession: () => HeadlessSession,
|
|
42
43
|
MALFORMED_CALL_NUDGE: () => MALFORMED_CALL_NUDGE,
|
|
43
44
|
MutableAIProviderRegistry: () => MutableAIProviderRegistry,
|
|
44
45
|
NOOP_ACTIVITY_BUS: () => NOOP_ACTIVITY_BUS,
|
|
@@ -56,18 +57,24 @@ __export(chat_driver_node_exports, {
|
|
|
56
57
|
anthropicRatesFor: () => anthropicRatesFor,
|
|
57
58
|
anthropicTokenCost: () => anthropicTokenCost,
|
|
58
59
|
assembleDebugLog: () => assembleDebugLog,
|
|
60
|
+
buildDiagnosticEntries: () => buildDiagnosticEntries,
|
|
59
61
|
buildTimelineEntries: () => buildTimelineEntries,
|
|
60
62
|
clearSession: () => clearSession,
|
|
61
63
|
defineAgent: () => defineAgent,
|
|
62
64
|
defineAutopilot: () => defineAutopilot,
|
|
63
65
|
defineStatefulAgent: () => defineStatefulAgent,
|
|
66
|
+
describeInteraction: () => describeInteraction,
|
|
64
67
|
emptyUsage: () => emptyUsage,
|
|
65
68
|
friendlyFallbackAgent: () => friendlyFallbackAgent,
|
|
66
69
|
geminiRatesFor: () => geminiRatesFor,
|
|
67
70
|
geminiTokenCost: () => geminiTokenCost,
|
|
68
71
|
getMetaEvents: () => getMetaEvents,
|
|
72
|
+
headlessCurrentDebugLog: () => headlessCurrentDebugLog,
|
|
73
|
+
headlessDiagnosticEntries: () => headlessDiagnosticEntries,
|
|
74
|
+
headlessMetaSnapshot: () => headlessMetaSnapshot,
|
|
69
75
|
isObservableAIProviderRegistry: () => isObservableAIProviderRegistry,
|
|
70
76
|
isTerminalAIServiceRefusal: () => isTerminalAIServiceRefusal,
|
|
77
|
+
lifetimeDebugLog: () => lifetimeDebugLog,
|
|
71
78
|
messageUsage: () => messageUsage,
|
|
72
79
|
resolveAITiers: () => resolveAITiers,
|
|
73
80
|
restoreMachine: () => restoreMachine,
|
|
@@ -4146,6 +4153,11 @@ var META_EVENT_IMPORTANCE = {
|
|
|
4146
4153
|
"autopilot.catch-up": "normal",
|
|
4147
4154
|
// Autopilot spend that reached no message, so it is missing from the session total.
|
|
4148
4155
|
"autopilot.cost-dropped": "high",
|
|
4156
|
+
// A headless session attached to the driver: from here a host with no screen is the person.
|
|
4157
|
+
"headless.attached": "normal",
|
|
4158
|
+
// The host settled an interaction raw, without the widget's own actions: its result went to the
|
|
4159
|
+
// tool as given, unchecked by anything the widget would have enforced.
|
|
4160
|
+
"headless.raw-resolved": "normal",
|
|
4149
4161
|
"external-diagnostics.folded": "normal",
|
|
4150
4162
|
"session.cleared": "normal",
|
|
4151
4163
|
"session.switched": "low",
|
|
@@ -4223,6 +4235,30 @@ function mergeMetaEvents(targetKey, events) {
|
|
|
4223
4235
|
function clearSession(key) {
|
|
4224
4236
|
registry.delete(key);
|
|
4225
4237
|
}
|
|
4238
|
+
var DEBUG_LOG_README = [
|
|
4239
|
+
"This is an exported debug log for the Genesis AI assistant. Read it top-to-bottom.",
|
|
4240
|
+
"`sessionUsage` is what this session spent: `costUsd` (USD, provider-reported per request and summed, cache discounts already applied) plus the four TOKEN BUCKETS \u2014 `uncachedInputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `outputTokens`. Those four are disjoint and safe to add up for a total-tokens figure; the per-message fields further down are NOT (see kind:'message'). Each bucket bills at a different rate \u2014 cache reads a fraction of uncached input, cache writes a premium, output highest \u2014 so a big token count against a small cost means the prompt was mostly cache hits, not a missing charge. It covers sub-agent turns and spend a compaction banked, so for the CONVERSATION it is the authoritative total: prefer it over re-summing the timeline, which can be short by whatever the ring buffers evicted. The same figures appear under `meta.context`. It is as current as the snapshot it came from: the assistant's own download stamps a fresh one, but a log stitched by another tool can carry an older one \u2014 compare `meta.timestamp` with the last timeline entry, and if it is well behind, read these totals as historical and fall back to summing the timeline. One scope caveat: it is derived from the transcript, so it excludes billed calls that produced no message (a blank or refused attempt that was retried \u2014 see kind:'turn'.`usage`). Summing the turns can therefore come out HIGHER than this, and the difference is exactly that discarded spend, not a double-count.",
|
|
4241
|
+
"What 'session' means for those figures: the CURRENT conversation under this session key (a host-supplied per-project id, or the element id + header title when the host supplies none) \u2014 which is not necessarily everything in `timeline`. It is re-derived from the live transcript, so it spans page loads only when the chat is being remembered, and a Clear resets it to zero. `timeline` is append-only regardless: it keeps every page load and the pre-Clear conversation, with the session.cleared event as the boundary. So on a long-lived log, read these totals as the latest conversation's and the earlier sections as history \u2014 not as a total of the whole file.",
|
|
4242
|
+
"`timeline` is the entire session as one array, already sorted chronologically by `timestamp` (ISO 8601). Every entry has a `kind`.",
|
|
4243
|
+
"Timestamps are millisecond-resolution; entries that share the same millisecond are ordered by a fixed kind rank (event, then turn, then message), which is a heuristic and may not reflect exact causal order within that millisecond \u2014 e.g. a user message and the turn it triggered, or a final assistant message and its turn.end event, can appear in either order depending on whether they landed in the same millisecond. Read the logical structure of a turn rather than over-interpreting the micro-ordering of co-timestamped entries of different kinds.",
|
|
4244
|
+
"kind:'message' \u2014 the conversation. `role` is user/assistant/tool/system-event/synthetic-user; `agentName` says which agent produced it; `toolCalls`/`toolResult`/`interaction` carry tool and widget activity; `inputTokens`/`outputTokens`/`cost` are per-message LLM usage, where `inputTokens` is the WHOLE prompt for that request and `cacheReadTokens`/`cacheWriteTokens` BREAK IT DOWN rather than add to it \u2014 uncached input is `inputTokens` minus those two, and adding the cache fields to `inputTokens` counts the prompt twice. The cache fields are absent on providers that report no cache split (Gemini reports reads only, since implicit caching bills no write) and on messages persisted before they existed, so read them as 0 when missing. Each bucket bills at a different rate \u2014 cache reads a fraction of uncached input, cache writes a premium, output highest \u2014 so a large token count at a small cost means the prompt was mostly cache hits. `externalCostUsd` is any non-LLM cost a widget reported for its own external service calls (folded into the session cost total alongside `cost`). On model-produced assistant messages, `model` is the concrete model id that generated it (e.g. 'gemini-2.5-flash-lite') and `providerName` is the registry slot it resolved under (e.g. a tier name like 'high'/'low', or the default); together they attribute the message \u2014 and any tool calls it carries \u2014 to an exact model even across a mid-session vendor/tier switch, where one slot name can map to different models before and after the switch. Both are undefined on any entry that is NOT an LLM response: non-assistant roles (user/tool/system-event) and 'synthetic-user' echoes; assistant interaction/widget entries (empty content carrying an `interaction` \u2014 a rendered widget, not a model turn); driver-authored assistant fallbacks (the timeout, repeated-malformed-call, and empty-response apology messages); and messages restored from a session persisted before these fields existed. One partial case: on a genuine model turn whose provider exposes no `getStatus` (or reports no model), `providerName` is still set but `model` alone is undefined. A 'synthetic-user' message is a display-only echo of an interaction outcome (e.g. the answer a widget reported): it renders on the user's side of the chat and `agentName` is the agent that created it, but it is never sent to the LLM \u2014 so it has no matching 'turn' and the model learns the outcome only from the corresponding tool result. An echo of an answer an interceptor (such as an autopilot) gave is NOT synthetic-user: see the delegated-messages paragraph.",
|
|
4245
|
+
"The autopilot (GENC-1660) runs on a driver of its own, and everything it did is harvested into this log: entries with delegate: autopilot and an agentName ending in \u203A Autopilot are its turns (the decision message it was sent is the user message of its turn, so what it was shown is there verbatim; its systemPrompt is its grounding, such as the brief), its tool calls (widget_read, widget_act, hand_back, any read-only tools of its own, and its widget tools \u2014 reads with parameters that a widget type declares, routed to the interaction of that type being decided and refused with a reason when none is, offered on every decision so its tool list never changes; while one is open its tail starts with a line naming them) and their results (a read, view or widget tool, is capped at about 20,000 characters, cut with a note asking for less, and leaves its history at the end of the decision as a one-line note), and its own events. Its cost is already in the session total: it is booked as externalCostUsd on the interaction each decision answered, or, for a written question it answered, on the user message it sent on the user behalf (delegate: autopilot), so do not add its turns to that total. Spend that reached no message is recorded as autopilot.cost-dropped instead.",
|
|
4246
|
+
"Delegated messages (GENC-1660): a message with a `delegate` field was written or sent ON THE USER'S BEHALF by a driver decorator, and `delegate` names it (e.g. autopilot). The platform stamps it; nothing else can. It renders as the assistant whatever its role. A delegated `user` message was NOT typed by the user: it is an answer given on their behalf, and the model read it with `delegateFraming` in brackets in front of the content (the stored `content` is the bare text). A delegated `assistant` message is display-only (`excludeFromProvider`): the decorator's narration note, or, when its answer carried no note, the widget's own echo of the answer (its `userMessage`), shown as the assistant and never as the person. A widget answer a decorator gave with a note is not echoed at all: the note already says what was chosen. To tell what the person decided from what was decided for them, read `interaction.answeredBy` and these fields, never the role alone.",
|
|
4247
|
+
"Sub-agent messages appear inline. When a tool delegates to a sub-agent (via `requestSubAgent`), the sub-agent's whole conversation \u2014 its own assistant/tool messages, each with their own `content`/`thinking`/`toolCalls`/`toolResult` and per-message `model`/`providerName`/`inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheWriteTokens`/`cost` \u2014 is hoisted into the timeline as ordinary kind:'message' entries, interleaved by timestamp right after the tool call that spawned them (so you read the delegation top-to-bottom). A hoisted entry is marked: `subAgentDepth` is its delegation depth (1 for a sub-agent, 2 for a sub-agent's sub-agent, \u2026), `subAgentOf` is the id of the parent tool call that spawned it (correlates it back even when two sub-agents run in one parent turn), `subAgentName` is the sub-agent's own name, and `agentName` is rewritten to a `\"<parent> \u203A <sub-agent>\"` breadcrumb (composing when nested, e.g. `\"UI Builder \u203A Planner \u203A Grounding\"`). The sub-agent's per-LLM-call snapshots also surface as kind:'turn' entries with an N-M `turnIndex`, and subagent.started/completed (or subagent.failed) events bracket the run. Per-message `cost` on hoisted entries is already part of the session total (it is summed from the un-flattened history), so summing the top-level timeline does NOT double-count.",
|
|
4248
|
+
"kind:'turn' \u2014 one LLM call. `turnIndex` is a string: a top-level turn is the bare counter ('0', '1', \u2026); a sub-agent's turns are numbered under the parent turn that activated them ('3-1', '3-2', \u2026, and a nested sub-agent contributes '3-2-1', \u2026), and `agentName` names the agent that ran the turn. `systemPrompt` and `toolNames` are what the model saw. A systemPrompt of '<repeated \u2014 identical to turn N>' was byte-identical to turn N and de-duplicated; the full prompt is shown whenever it changes (often because a stateful agent advanced), so prompt evolution is visible.",
|
|
4249
|
+
"kind:'turn'.`model`/`providerName`/`provider` \u2014 which model ran that call: the concrete model id, the registry slot it resolved under (a tier name like 'high'/'low', or the default), and the vendor. Recorded per CALL, so an agent whose `provider` selector varies by state \u2014 a flow that plans on a high tier and executes on a low one \u2014 has every step attributed, including calls that produced no message. `model` is the SERVING model where the provider reports one, so a turn answered by a server-side fallback names the model that answered rather than the one requested; it always matches the `model` on the message that call produced. Absent when the provider exposes no `getStatus` and the transport stamped nothing.",
|
|
4250
|
+
"kind:'turn'.`usage` \u2014 what that ONE call cost, in the same four-bucket + `costUsd` shape as `sessionUsage`. Absent while a call is in flight and on providers that report no usage. This is the SAME money as the message the call produced, not extra money: never add turn usage to message usage, and read a turn plus its message as one charge. Its distinct value is the calls that produced NO message \u2014 a blank or refused response is billed and then discarded before the retry, so the turn entry is the only record of that spend, and a turn with `usage` but no message after it is exactly that. `costUsd: 0` alongside a nonzero token count means the provider reported no price, not that the call was free.",
|
|
4251
|
+
"kind:'turn'.`agentSnapshot` \u2014 the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
|
|
4252
|
+
"kind:'event' \u2014 a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
|
|
4253
|
+
"Each 'event' also has an `importance`: 'high' (failures/limits \u2014 turn.error, tool.failed, subagent.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow \u2014 connects, turns, retries, handoffs, agent/provider changes, interactions, sub-agent start/complete), or 'low' (skippable UI/bookkeeping noise \u2014 panel.toggled, attachment.added, driver.wired/unwired, context.updated, context.condensed). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason \u2014 read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance \u2014 they are the substance, always read them.",
|
|
4254
|
+
"Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle\u2194loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry \u2014 detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail \u2014 detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted/provider-refused/context-exhausted/ai-service-error, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools \u2014 real earlier this activation but retired by the current state or hidden behind an open exclusive fold \u2014 and hallucinatedTools \u2014 never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, for provider-refused the kind (spend|auth) plus the upstream status and error type \u2014 the vendor refusing the ACCOUNT rather than us refusing to spend, so the remedy is a top-up or a key rotation rather than a raised cap, and the kind is the only place that distinction survives because the user-facing copy is deliberately cause-free, for context-exhausted the estimated contextTokens against the guard that stopped the turn plus the iterations reached \u2014 the driver ending a runaway tool loop at the edge of the context window rather than issuing a request the provider would reject, so the transcript survives intact and can still be compacted, for ai-service-error the code (one of AI_SERVICE_ERROR_CODES, e.g. NO_API_KEY) and the status the AI service answered with \u2014 its own text goes to the browser console only, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched \u2014 detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation \u2014 detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted/provider_refused/context_exhausted/ai_service_error; budget_exhausted and provider_refused are terminal for the PARENT turn too, and so is ai_service_error when the refusal is about the service rather than the request and came from the vendor the parent calls \u2014 the parent stops rather than calling the model again into the same wall; after context_exhausted the parent goes on, but its own next call is likely to stop on the same guard), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), routing.decided (ONE PER ROUTING DECISION \u2014 see its own paragraph below), provider.selected (the resolved provider for the upcoming turns \u2014 detail.provider is the registry SLOT/tier name, detail.model the concrete model behind it and detail.vendor its vendor; emitted only when the slot CHANGES, so read the per-turn `model` for the model of any given call rather than assuming the nearest event still applies), interaction.requested/resolved (blocking user widgets \u2014 explain quiet gaps; note that when a sub-agent opens a widget, detail.agent \u2014 and the agentName on the interaction message \u2014 is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver; detail.ownerPath names the agents that actually asked, outermost first, the last being the one whose tool requested it, and detail.heldBy says who held it when it opened: user, or the id of the interceptor that claimed it, such as autopilot; on interaction.resolved, detail.answeredBy is who answered \u2014 the claimant id when it settled through its own hold, user for any other answer (a click counts as the person even on a widget a claimant held) \u2014 absent when it closed unanswered (a timeout or a stop), and absent too when it settled by itself other than through the hold of a claimant \u2014 a widget registered resolvesItself (one that does its work and settles on its own, asking the person nothing, such as a generation run), or one outcome its controller said settled itself (ctx.resolve with resolvedItself, such as a generation phase that failed outright before its review gate ever asked anything) \u2014 which then carries detail.resolvedItself: true instead, as the interaction message does \u2014 nobody answered it, and the platform cannot tell it finishing from the person interrupting it (a Skip), so only the result of the widget itself can say which; the same value is stamped on the interaction message as answeredBy and on the result the asking tool handler receives (absent there in the same cases), and when it is not user the tool result that interaction returned carries a decidedOnUsersBehalf record, so the model was told the choice was not made by the person; detail.edits, present only when non-zero, counts the editing actions a claimant ran on the widget before it was settled, whoever settled it, and those edits are stamped on the interaction message as edits, each with by, action, choice and args when it had some, in the order they ran, while a decidedOnUsersBehalf record carries their choice texts as edits, and when the person settled it after such edits the tool result carries an editedOnUsersBehalf record instead \u2014 the framing and those choice texts \u2014 so the model knows the widget was changed on the user behalf before the user answered, and that the user then confirmed it by finishing it), interaction.held (a claimant moved a pending interaction to the person or back to itself \u2014 detail.heldBy is the new holder, detail.claimant the interceptor; detail.late: true is an interceptor that declined it when it opened claiming it afterwards, such as the autopilot resumed while it was open; a widget held by anything but user was not answered by the person), interaction.claim-refused (a claim on a userOnly interaction, which stays with the person \u2014 detail.claimant + reason), autopilot.* (the autopilot, a decorator that answers interactions on behalf of the user \u2014 autopilot.claimed / autopilot.declined say whether it took each interaction and, for a decline, detail.reason: paused, other-flow, no-autopilot, unavailable, userOnly, policy or no-controller; one declined as paused that is still open when the person resumes the autopilot is looked at again, and the autopilot.claimed or autopilot.declined recorded for it then carries detail.onResume: true (a decline then can also be claimed-elsewhere, another interceptor having claimed it first); a restored mode looks at nothing; autopilot.decision-sent is what it was sent (detail.chars; detail.nothingEnabled: true when every action on the widget was unavailable \u2014 a gate whose controller said it was asking, such as a failed build the autopilot may not cancel \u2014 so it could only hand it back, which it may then do even after the person asked it to decide it itself; a widget whose controller says it is working (ctx.setWorking) is never sent a decision until it says it is asking, even when an action is enabled or the person resumes the autopilot); autopilot.acted is what it did (detail.action, detail.note, detail.finished); autopilot.handed-back is a decision it gave to the person (detail.reason and detail.trigger: model, no-decision or error); autopilot.paused / autopilot.resumed are mode changes (detail.scope, detail.trigger, detail.decideYourself when the person resumed during a hand-back; detail.trigger is restored when the mode came back with the session after a reload, applied once its flow re-activated, which starts nothing by itself); autopilot.carried is the mode, running or paused, going with the task to the agent that took it over, rather than ending with the flow as on any other change of flow owner: to the target of a hand-off, or back to its caller on the hand-back (detail.from, detail.to, detail.mode); a pause the person chose goes with it, and Stop and Clear still end it; a mode restored for the target of a hand-off applies to its caller when the reload hands back; autopilot.error is the wrapper itself failing; autopilot.refinement counts a refining action against its step \u2014 a written answer (reply) or a widget action declared refines, such as a regenerate with instructions, counted together (detail.action, detail.stepKey, detail.count, detail.limitReached, after which every refining action is withdrawn for that step: not listed, and refused; the decision and every relisting of its actions say how many are left at the step, as a line Refinements left at this step: N, wherever the widget has a refining action); autopilot.handback-presented is the turn-level hand-back, shown when a working turn ended needing the person with no widget to hand over (detail.why: max-iterations, context-exhausted, failure or question; detail.options; detail.choice once answered); autopilot.catch-up is the autopilot being given the whole visible transcript at once (detail.why: first, for its first decision in a flow or after a reload; size, when its own history passed its budget and was rebuilt short \u2014 detail.ownChars; or compacted, when a compaction removed the last message it had seen, so it reads the summary and everything since, short; in each, what it had sent on the user behalf is included and labelled as its own, since its own history does not hold it); autopilot.cost-dropped is autopilot spend that no message carries, so it is NOT in the session total (detail.costUsd and detail.why: handed-back, aborted or stale for a written question it answered that no reply was sent for \u2014 stale when a pause, a resume, a new turn or the end of its flow came before the answer landed, which then went nowhere \u2014 or no-recorder when the driver had no way to book a decision to its interaction); a hand-back trigger can also be budget (the decision passed its spend cap), watchdog (its work went quiet: no ready surface and no heartbeat, or its own decision stalled with no model call landing, which stops that decision) or detached (the autopilot was removed while it held the interaction, which went to the person with no reason shown). To debug one autopilot decision, follow its interactionId: interaction.requested (heldBy), autopilot.claimed, autopilot.decision-sent, autopilot.acted or autopilot.handed-back, interaction.invoked, interaction.resolved (answeredBy), then the next turn of the working agent), interaction.invoked (a claimant ran one of the actions the widget itself exposes, through its hold, the same code the button runs \u2014 detail.claimant, detail.action (an option id, select or reply) and detail.finished, whether that settled the interaction, and when it did not, the run is recorded as an edit; the choice and narration it recorded reach the result of the tool that asked as decidedOnUsersBehalf; the person's own programmatic action, from a host with no DOM to click, carries detail.by: user in place of detail.claimant, and is not recorded as an edit nor as a decision on their behalf, so what it settles is answered by user), interaction.step (a long action run as named steps through runSteps, which survive a reload \u2014 detail.step and detail.outcome: started, done, skipped when the step was already done before a reload, or resumed when it started before a reload and runs again with what it saved part-way, such as a job id it re-attaches to), interaction.checkpoint (the checkpoints a conversation keeps for its long actions: the one in flight, and each one a reload cut, held apart until a request of its own widget type takes it over, so a resumed flow that opens other long widgets first, which save their own, loses nothing \u2014 detail.action: resumed when a reload cut the work and a request of the same widget type that the registration sameWork agrees is the same work took it over, refused when sameWork said a new request is different work (detail.saved is the identity it kept; detail.kept: true, since it stays for a later request of that type, and is offered to every one until one agrees) or when runSteps found the handed-over checkpoint malformed and started fresh (detail.reason, detail.saved), or cleared when its flow ended, once per checkpoint, with detail.cut: true for one a reload cut that nothing took over), context.updated/threshold-crossed (token + cost), headless.attached (a HeadlessSession attached, for a host that drives the assistant with no screen \u2014 detail.label, what the host called itself, when it gave one) and headless.raw-resolved (that host settled an interaction raw, bypassing the widget's own actions: detail.interactionId and detail.component; the result went to the tool as given, so check what the widget would have enforced), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool \u2014 detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.",
|
|
4255
|
+
"Sub-agent meta events: a sub-agent's own turn.retry/turn.error/tool.failed/tool.unresolved events are merged into this same timeline, tagged with `detail.subAgent` \u2014 a `\"<parent> \u203A <sub-agent>\"` breadcrumb that composes when nested (e.g. `\"UI Builder \u203A Planner \u203A Grounding\"`) \u2014 and interleaved by their original timestamps within the subagent.started\u2192completed/failed bracket. These are the per-attempt/per-failure signals that do NOT appear among the sub-agent's (hoisted) messages: a malformed/empty attempt that gets retried produces no message, and the stale-vs-hallucinated split and streak counts live only on the event. A sub-agent's high-volume, message-derivable events (turn.start/turn.end, provider.selected, context.updated) are intentionally NOT merged \u2014 read its hoisted messages for model/tokens/cost and turn-by-turn activity, and the bracketing subagent.* events for the run's span.",
|
|
4256
|
+
"`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, contextTokens/contextLimit/contextUsagePercent for the last call, and the session totals \u2014 sessionCostUsd, sessionTokensConsumed, and the four-bucket sessionUsage lifted to the top of this log), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export \u2014 reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.",
|
|
4257
|
+
"Note the two different scopes in `meta.context`: `contextTokens` is the prompt size of the LAST call (against `contextLimit`, the model context window), while `sessionUsage`/`sessionTokensConsumed` are cumulative BILLED throughput. Every turn resends the conversation, so the cumulative figure counts each turn\u2019s prompt again in the next turn\u2019s and is expected to dwarf the context size \u2014 that is not double-counting.",
|
|
4258
|
+
"kind:'event' type 'routing.decided' \u2014 how ONE user turn was routed, emitted by the orchestrating driver before the agent runs. Only a user turn is routed: a hand-off names its target, so the agents a hand-off runs show up as agent.handoff events, not as more routing.decided. `decision` is why that agent was chosen: 'classified' (the classifier ran and picked one), 'no-match' (it ran and answered -1 \u2014 nothing listed covers the request), 'pinned' (a user pin, an agent named for that one message by the send's `agent` option, or an active stateful flow owned the turn, so no call was made), 'single-candidate'/'no-specialists' (configuration left nothing to decide), 'provider-unavailable' (the registry default exposes no chat()), 'classifier-failed' (every attempt threw \u2014 `error` carries the last one). `to` names the agent and `fallback` says whether that agent is the fallback. `candidates` is what was ON THE BALLOT \u2014 every classifier-eligible specialist \u2014 in ballot order, so `index` \u2014 the classifier's own answer \u2014 indexes it directly. `candidates` is absent on the three decisions that put nothing to a vote \u2014 'pinned', 'single-candidate', 'no-specialists'. It IS present on 'provider-unavailable', which is not an exception worth removing: the ballot was built there, it simply never got sent. `attempts` covers the retry ladder. `durationMs` is on EVERY routing.decided, and is the field to read when a decision looks wrong: the decisions that can reach the fallback ('no-match', 'no-specialists', 'provider-unavailable', 'classifier-failed') are told apart by the clock as much as by their name, because a fallback routing that resolved in tens of milliseconds made no network call at all whatever its `decision` says.",
|
|
4259
|
+
"kind:'event' type 'routing.decided'.`usage` \u2014 what that classifier call cost, in the same four-bucket + costUsd shape as `sessionUsage`, plus `model`. Absent on the decisions that make no call (see 'pinned' and the configuration ones). THIS SPEND IS NOT IN `sessionUsage`: a classifier call never enters the transcript and `sessionUsage` is derived from the transcript, so classifier cost is missing from the session total by construction, not by accident. Sum these events to close that gap \u2014 it is the one component of a session's real spend that the totals cannot see.",
|
|
4260
|
+
"To debug a failure: find the last turn.error or tool.failed, then read upward for the user message, the turn(s), and the agent/provider/state events that led into it. For a turn that went to the wrong agent, read its routing.decided first \u2014 `decision` says whether anything was actually asked."
|
|
4261
|
+
];
|
|
4226
4262
|
|
|
4227
4263
|
// src/utils/delegated-message.ts
|
|
4228
4264
|
var DEFAULT_DELEGATE_FRAMING = "Sent on the user's behalf.";
|
|
@@ -4372,9 +4408,15 @@ function calibrate(history, preambleTokens) {
|
|
|
4372
4408
|
if (heaviest >= 0) estimates[heaviest] += messages - assigned;
|
|
4373
4409
|
return { overhead, total, estimates, scale };
|
|
4374
4410
|
}
|
|
4411
|
+
function isContextMeasurable(history) {
|
|
4412
|
+
return history.some((m) => m.inputTokens != null || m.role === "compacted-summary");
|
|
4413
|
+
}
|
|
4375
4414
|
function estimateMessageTokens(history, preambleTokens) {
|
|
4376
4415
|
return calibrate(history, preambleTokens).estimates;
|
|
4377
4416
|
}
|
|
4417
|
+
function estimateContextTokens(history) {
|
|
4418
|
+
return calibrate(history).total;
|
|
4419
|
+
}
|
|
4378
4420
|
function estimateRequestTokens(storedHistory, requestHistory, preambleTokens) {
|
|
4379
4421
|
const { overhead } = calibrate(storedHistory, preambleTokens);
|
|
4380
4422
|
let requestBaseline = 0;
|
|
@@ -5306,6 +5348,16 @@ var HARVESTED_SUBAGENT_EVENTS = /* @__PURE__ */ new Set([
|
|
|
5306
5348
|
|
|
5307
5349
|
// src/utils/tool-fold.ts
|
|
5308
5350
|
var TOOL_FOLD_SYMBOL = Symbol("toolFold");
|
|
5351
|
+
function expandToolTree(definitions, handlers) {
|
|
5352
|
+
return definitions.map((def) => {
|
|
5353
|
+
const handler = handlers[def.name];
|
|
5354
|
+
const fold = handler ? handler[TOOL_FOLD_SYMBOL] : void 0;
|
|
5355
|
+
if (fold) {
|
|
5356
|
+
return { ...def, tools: expandToolTree(fold.tools, fold.handlers) };
|
|
5357
|
+
}
|
|
5358
|
+
return { ...def };
|
|
5359
|
+
});
|
|
5360
|
+
}
|
|
5309
5361
|
|
|
5310
5362
|
// src/components/chat-driver/fold-controller.ts
|
|
5311
5363
|
var foldOf = (handler) => handler ? handler[TOOL_FOLD_SYMBOL] : void 0;
|
|
@@ -5494,8 +5546,15 @@ function buildDecisionSurface(spec) {
|
|
|
5494
5546
|
}
|
|
5495
5547
|
ids.add(id);
|
|
5496
5548
|
}
|
|
5549
|
+
const extra = spec.actions ?? {};
|
|
5550
|
+
assertActionShapes("exposeDecision", extra);
|
|
5551
|
+
for (const name of Object.keys(extra)) {
|
|
5552
|
+
if (ids.has(name) || spec.multi && name === SELECT_ACTION || spec.freeText && name === REPLY_ACTION) {
|
|
5553
|
+
throw new Error(`exposeDecision: the action "${name}" is also an option or a reserved name.`);
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5497
5556
|
const byIdOrLabel = (key) => typeof key === "string" ? options.find((o) => o.id === key || o.label === key) : void 0;
|
|
5498
|
-
const
|
|
5557
|
+
const builtIn = () => {
|
|
5499
5558
|
const enabled = options.filter(isEnabled);
|
|
5500
5559
|
return [
|
|
5501
5560
|
...options.map((o) => {
|
|
@@ -5519,6 +5578,10 @@ function buildDecisionSurface(spec) {
|
|
|
5519
5578
|
...spec.freeText ? [replyAction()] : []
|
|
5520
5579
|
];
|
|
5521
5580
|
};
|
|
5581
|
+
const actions = () => [
|
|
5582
|
+
...builtIn(),
|
|
5583
|
+
...Object.entries(extra).map(([name, a]) => describeSpecAction(name, a))
|
|
5584
|
+
];
|
|
5522
5585
|
const replyReason = () => unavailableReason(spec.freeTextUnavailable);
|
|
5523
5586
|
const replyAction = () => {
|
|
5524
5587
|
const unavailable = replyReason();
|
|
@@ -5543,7 +5606,7 @@ function buildDecisionSurface(spec) {
|
|
|
5543
5606
|
facts,
|
|
5544
5607
|
...readable,
|
|
5545
5608
|
actions,
|
|
5546
|
-
isReady: () =>
|
|
5609
|
+
isReady: () => builtIn().some((a) => a.finishes && a.enabled),
|
|
5547
5610
|
prepare(name, args) {
|
|
5548
5611
|
if (spec.multi && name === SELECT_ACTION) {
|
|
5549
5612
|
const picked = args?.options;
|
|
@@ -5585,6 +5648,8 @@ function buildDecisionSurface(spec) {
|
|
|
5585
5648
|
}
|
|
5586
5649
|
const option = options.find((o) => o.id === name);
|
|
5587
5650
|
if (!option) {
|
|
5651
|
+
const own = ownAction(extra, name);
|
|
5652
|
+
if (own) return prepareSpecAction(name, own, args);
|
|
5588
5653
|
return { error: "unknown-action", message: `No action "${name}" here.` };
|
|
5589
5654
|
}
|
|
5590
5655
|
const why = reasonFor(option);
|
|
@@ -5631,31 +5696,88 @@ function checkArgs(args, schema) {
|
|
|
5631
5696
|
const wrong = Object.entries(properties).filter(([key, prop]) => record[key] !== void 0 && !matchesType(record[key], prop?.type)).map(([key, prop]) => `${key} should be ${String(prop?.type)}`);
|
|
5632
5697
|
return wrong.length ? `${wrong.join("; ")}.` : void 0;
|
|
5633
5698
|
}
|
|
5634
|
-
function
|
|
5635
|
-
|
|
5699
|
+
function exampleArgs(schema) {
|
|
5700
|
+
const properties = schema.properties ?? {};
|
|
5701
|
+
const required = new Set(schema.required ?? []);
|
|
5702
|
+
const placeholder = (type) => {
|
|
5703
|
+
switch (type) {
|
|
5704
|
+
case "string":
|
|
5705
|
+
return "\u2026";
|
|
5706
|
+
case "number":
|
|
5707
|
+
case "integer":
|
|
5708
|
+
return 0;
|
|
5709
|
+
case "boolean":
|
|
5710
|
+
return false;
|
|
5711
|
+
case "array":
|
|
5712
|
+
return [];
|
|
5713
|
+
case "object":
|
|
5714
|
+
return {};
|
|
5715
|
+
default:
|
|
5716
|
+
return null;
|
|
5717
|
+
}
|
|
5718
|
+
};
|
|
5719
|
+
const names = Object.keys(properties).sort(
|
|
5720
|
+
(a, b) => Number(required.has(b)) - Number(required.has(a))
|
|
5721
|
+
);
|
|
5722
|
+
return Object.fromEntries(names.map((name) => [name, placeholder(properties[name]?.type)]));
|
|
5723
|
+
}
|
|
5724
|
+
var describeSpecAction = (name, a) => {
|
|
5725
|
+
const unavailable = unavailableReason(a.unavailable);
|
|
5726
|
+
return {
|
|
5727
|
+
name,
|
|
5728
|
+
finishes: !!a.finishes,
|
|
5729
|
+
enabled: unavailable === void 0,
|
|
5730
|
+
...unavailable !== void 0 ? { unavailable } : {},
|
|
5731
|
+
description: a.description,
|
|
5732
|
+
...a.text ? { args: "text" } : {},
|
|
5733
|
+
...a.argsSchema ? { args: "fields", argsSchema: a.argsSchema } : {},
|
|
5734
|
+
...a.refines ? { refines: true } : {}
|
|
5735
|
+
};
|
|
5736
|
+
};
|
|
5737
|
+
var prepareSpecAction = (name, action, args) => {
|
|
5738
|
+
const why = unavailableReason(action.unavailable);
|
|
5739
|
+
if (why !== void 0) {
|
|
5740
|
+
return { error: "disabled", message: `"${name}" is not available right now: ${why}.` };
|
|
5741
|
+
}
|
|
5742
|
+
if (action.text) {
|
|
5743
|
+
const text = args?.text;
|
|
5744
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
5745
|
+
return {
|
|
5746
|
+
error: "invalid-args",
|
|
5747
|
+
message: `"${name}" takes its argument as \`text\`, not empty.`
|
|
5748
|
+
};
|
|
5749
|
+
}
|
|
5750
|
+
const trimmed = text.trim();
|
|
5751
|
+
return {
|
|
5752
|
+
choice: `${name}: ${trimmed}`.slice(0, CHOICE_CHARS),
|
|
5753
|
+
finishes: !!action.finishes,
|
|
5754
|
+
run: () => action.run(trimmed)
|
|
5755
|
+
};
|
|
5756
|
+
}
|
|
5757
|
+
const problem = checkArgs(args ?? {}, action.argsSchema);
|
|
5758
|
+
if (problem) return { error: "invalid-args", message: `"${name}": ${problem}` };
|
|
5759
|
+
const shown = args && Object.keys(args).length ? ` ${JSON.stringify(args)}` : "";
|
|
5760
|
+
return {
|
|
5761
|
+
choice: `${name}${shown}`.slice(0, CHOICE_CHARS),
|
|
5762
|
+
finishes: !!action.finishes,
|
|
5763
|
+
run: () => action.run(args ?? {})
|
|
5764
|
+
};
|
|
5765
|
+
};
|
|
5766
|
+
var ownAction = (actions, name) => Object.prototype.hasOwnProperty.call(actions, name) ? actions[name] : void 0;
|
|
5767
|
+
var assertActionShapes = (who, actions) => {
|
|
5768
|
+
for (const [name, action] of Object.entries(actions)) {
|
|
5636
5769
|
if (action.text && action.argsSchema) {
|
|
5637
|
-
throw new Error(
|
|
5638
|
-
`exposeSurface: action "${name}" takes \`text\` or \`argsSchema\`, not both.`
|
|
5639
|
-
);
|
|
5770
|
+
throw new Error(`${who}: action "${name}" takes \`text\` or \`argsSchema\`, not both.`);
|
|
5640
5771
|
}
|
|
5641
5772
|
}
|
|
5773
|
+
};
|
|
5774
|
+
function buildFullSurface(spec) {
|
|
5775
|
+
assertActionShapes("exposeSurface", spec.actions);
|
|
5642
5776
|
const views = spec.views && Object.keys(spec.views).length ? spec.views : void 0;
|
|
5643
5777
|
if (spec.preview !== void 0 && !(views && Object.prototype.hasOwnProperty.call(views, spec.preview))) {
|
|
5644
5778
|
throw new Error(`exposeSurface: the preview "${spec.preview}" is not one of its views.`);
|
|
5645
5779
|
}
|
|
5646
|
-
const actions = () => Object.entries(spec.actions).map(([name, a]) =>
|
|
5647
|
-
const unavailable = unavailableReason(a.unavailable);
|
|
5648
|
-
return {
|
|
5649
|
-
name,
|
|
5650
|
-
finishes: !!a.finishes,
|
|
5651
|
-
enabled: unavailable === void 0,
|
|
5652
|
-
...unavailable !== void 0 ? { unavailable } : {},
|
|
5653
|
-
description: a.description,
|
|
5654
|
-
...a.text ? { args: "text" } : {},
|
|
5655
|
-
...a.argsSchema ? { args: "fields", argsSchema: a.argsSchema } : {},
|
|
5656
|
-
...a.refines ? { refines: true } : {}
|
|
5657
|
-
};
|
|
5658
|
-
});
|
|
5780
|
+
const actions = () => Object.entries(spec.actions).map(([name, a]) => describeSpecAction(name, a));
|
|
5659
5781
|
return {
|
|
5660
5782
|
prompt: spec.prompt,
|
|
5661
5783
|
facts: () => ({}),
|
|
@@ -5671,35 +5793,9 @@ function buildFullSurface(spec) {
|
|
|
5671
5793
|
actions,
|
|
5672
5794
|
isReady: () => actions().some((a) => a.enabled),
|
|
5673
5795
|
prepare(name, args) {
|
|
5674
|
-
const action =
|
|
5796
|
+
const action = ownAction(spec.actions, name);
|
|
5675
5797
|
if (!action) return { error: "unknown-action", message: `No action "${name}" here.` };
|
|
5676
|
-
|
|
5677
|
-
if (why !== void 0) {
|
|
5678
|
-
return { error: "disabled", message: `"${name}" is not available right now: ${why}.` };
|
|
5679
|
-
}
|
|
5680
|
-
if (action.text) {
|
|
5681
|
-
const text = args?.text;
|
|
5682
|
-
if (typeof text !== "string" || !text.trim()) {
|
|
5683
|
-
return {
|
|
5684
|
-
error: "invalid-args",
|
|
5685
|
-
message: `"${name}" takes its argument as \`text\`, not empty.`
|
|
5686
|
-
};
|
|
5687
|
-
}
|
|
5688
|
-
const trimmed = text.trim();
|
|
5689
|
-
return {
|
|
5690
|
-
choice: `${name}: ${trimmed}`.slice(0, CHOICE_CHARS),
|
|
5691
|
-
finishes: !!action.finishes,
|
|
5692
|
-
run: () => action.run(trimmed)
|
|
5693
|
-
};
|
|
5694
|
-
}
|
|
5695
|
-
const problem = checkArgs(args ?? {}, action.argsSchema);
|
|
5696
|
-
if (problem) return { error: "invalid-args", message: `"${name}": ${problem}` };
|
|
5697
|
-
const shown = args && Object.keys(args).length ? ` ${JSON.stringify(args)}` : "";
|
|
5698
|
-
return {
|
|
5699
|
-
choice: `${name}${shown}`.slice(0, CHOICE_CHARS),
|
|
5700
|
-
finishes: !!action.finishes,
|
|
5701
|
-
run: () => action.run(args ?? {})
|
|
5702
|
-
};
|
|
5798
|
+
return prepareSpecAction(name, action, args);
|
|
5703
5799
|
}
|
|
5704
5800
|
};
|
|
5705
5801
|
}
|
|
@@ -6565,6 +6661,66 @@ var InteractionController = class {
|
|
|
6565
6661
|
this.contexts.get(interactionId)?.dispose();
|
|
6566
6662
|
this.contexts.delete(interactionId);
|
|
6567
6663
|
}
|
|
6664
|
+
/**
|
|
6665
|
+
* See `ChatDriver.invokeAsUser`: `hold.invoke`'s sibling for the person (GENC-1660).
|
|
6666
|
+
*
|
|
6667
|
+
* Runs the action the way the widget's own button does, and records nothing against a claimant:
|
|
6668
|
+
* no `settledBy`, no `decision`. So `whoAnswered` credits a settle to the person
|
|
6669
|
+
* (`answeredBy: 'user'`), exactly as a click would be. Refused unless the person holds the
|
|
6670
|
+
* interaction: a claimant that does is not pushed aside, the person takes it back first. An
|
|
6671
|
+
* editing action leaves no edit record either — those mark what a claimant changed on the
|
|
6672
|
+
* person's behalf, and this is the person's own change.
|
|
6673
|
+
*/
|
|
6674
|
+
async invokeAsUser(interactionId, action, args) {
|
|
6675
|
+
const pending = this.pending.get(interactionId);
|
|
6676
|
+
const handle = this.contexts.get(interactionId);
|
|
6677
|
+
if (!pending || !handle) {
|
|
6678
|
+
return { ok: false, error: "not-held", message: "This interaction has already ended." };
|
|
6679
|
+
}
|
|
6680
|
+
if (handle.context.heldBy !== "user") {
|
|
6681
|
+
return {
|
|
6682
|
+
ok: false,
|
|
6683
|
+
error: "not-held",
|
|
6684
|
+
message: "Another agent holds this interaction, so it was not acted on. The person has to take it back first, for example by pausing the autopilot."
|
|
6685
|
+
};
|
|
6686
|
+
}
|
|
6687
|
+
const { surface } = handle;
|
|
6688
|
+
if (!surface) {
|
|
6689
|
+
return { ok: false, error: "no-surface", message: "Nothing has been exposed here yet." };
|
|
6690
|
+
}
|
|
6691
|
+
if (pending.running) {
|
|
6692
|
+
return { ok: false, ...BUSY, actions: surface.actions() };
|
|
6693
|
+
}
|
|
6694
|
+
const prepared = surface.prepare(action, args);
|
|
6695
|
+
if ("error" in prepared) {
|
|
6696
|
+
return { ok: false, ...prepared, actions: surface.actions() };
|
|
6697
|
+
}
|
|
6698
|
+
const token = {};
|
|
6699
|
+
pending.running = { token, finishes: prepared.finishes, note: void 0 };
|
|
6700
|
+
let output;
|
|
6701
|
+
try {
|
|
6702
|
+
const ran = prepared.run();
|
|
6703
|
+
output = isThenable(ran) ? await ran : ran;
|
|
6704
|
+
} catch (e) {
|
|
6705
|
+
logger2.warn(`InteractionController: action "${action}" threw:`, e);
|
|
6706
|
+
return { ok: false, error: "threw", message: e instanceof Error ? e.message : String(e) };
|
|
6707
|
+
} finally {
|
|
6708
|
+
if (pending.running?.token === token) pending.running = void 0;
|
|
6709
|
+
}
|
|
6710
|
+
const finished = !this.pending.has(interactionId) || pending.early !== void 0;
|
|
6711
|
+
recordMetaEvent(this.deps.sessionKey, "interaction.invoked", {
|
|
6712
|
+
interactionId,
|
|
6713
|
+
by: "user",
|
|
6714
|
+
action,
|
|
6715
|
+
finished
|
|
6716
|
+
});
|
|
6717
|
+
return {
|
|
6718
|
+
ok: true,
|
|
6719
|
+
finished,
|
|
6720
|
+
actions: finished ? [] : surface.actions(),
|
|
6721
|
+
...output !== void 0 && !finished ? { output } : {}
|
|
6722
|
+
};
|
|
6723
|
+
}
|
|
6568
6724
|
/**
|
|
6569
6725
|
* See `ChatDriver.recordExternalCost`.
|
|
6570
6726
|
*
|
|
@@ -8509,6 +8665,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
8509
8665
|
addInteractionInterceptor(interceptor) {
|
|
8510
8666
|
return this.interactions.addInterceptor(interceptor);
|
|
8511
8667
|
}
|
|
8668
|
+
/**
|
|
8669
|
+
* {@inheritDoc AiDriver.invokeAsUser}
|
|
8670
|
+
*
|
|
8671
|
+
* @beta
|
|
8672
|
+
*/
|
|
8673
|
+
invokeAsUser(interactionId, action, args) {
|
|
8674
|
+
return this.interactions.invokeAsUser(interactionId, action, args);
|
|
8675
|
+
}
|
|
8512
8676
|
/**
|
|
8513
8677
|
* {@inheritDoc AiDriver.presentInteraction}
|
|
8514
8678
|
*
|
|
@@ -10005,6 +10169,17 @@ var BaseDriverDecorator = class _BaseDriverDecorator extends EventTarget {
|
|
|
10005
10169
|
addInteractionInterceptor(interceptor) {
|
|
10006
10170
|
return this.inner.addInteractionInterceptor?.(interceptor) ?? (() => void 0);
|
|
10007
10171
|
}
|
|
10172
|
+
/**
|
|
10173
|
+
* Forwarded: the interaction belongs to the driver that opened it. An inner driver that cannot
|
|
10174
|
+
* run actions answers as if the interaction had ended.
|
|
10175
|
+
*/
|
|
10176
|
+
invokeAsUser(interactionId, action, args) {
|
|
10177
|
+
return this.inner.invokeAsUser?.(interactionId, action, args) ?? Promise.resolve({
|
|
10178
|
+
ok: false,
|
|
10179
|
+
error: "not-held",
|
|
10180
|
+
message: "This interaction has already ended."
|
|
10181
|
+
});
|
|
10182
|
+
}
|
|
10008
10183
|
presentInteraction(componentName, data, options) {
|
|
10009
10184
|
return this.inner.presentInteraction?.(componentName, data, options) ?? Promise.reject(new Error("presentInteraction: the inner driver cannot show interactions."));
|
|
10010
10185
|
}
|
|
@@ -11301,67 +11476,404 @@ var strictFallbackAgent = {
|
|
|
11301
11476
|
Answer in a single short sentence. Do not elaborate.`
|
|
11302
11477
|
};
|
|
11303
11478
|
|
|
11304
|
-
// src/utils/
|
|
11305
|
-
|
|
11306
|
-
|
|
11479
|
+
// src/utils/message-partition.ts
|
|
11480
|
+
var SESSION_RESTORED_LABEL = "Restored previous conversation";
|
|
11481
|
+
var SESSION_RESUMED_LABEL = "Restored previous conversation. Resuming from where we were interrupted.";
|
|
11482
|
+
var SESSION_RESTORE_FAILED_LABEL = "Couldn't restore the previous conversation";
|
|
11483
|
+
var isSessionRestoredMarker = (m) => m.role === "system-event" && (m.content === SESSION_RESTORED_LABEL || m.content === SESSION_RESUMED_LABEL);
|
|
11484
|
+
var isSessionRestoreFailedMarker = (m) => m.role === "system-event" && m.content === SESSION_RESTORE_FAILED_LABEL;
|
|
11485
|
+
var isSessionNoticeMarker = (m) => isSessionRestoredMarker(m) || isSessionRestoreFailedMarker(m);
|
|
11486
|
+
function filterVisibleMessages(messages, toggles) {
|
|
11487
|
+
return messages.filter((m) => {
|
|
11488
|
+
if (m.role === "compacted-summary") {
|
|
11489
|
+
return true;
|
|
11490
|
+
}
|
|
11491
|
+
if (m.role === "system-event") {
|
|
11492
|
+
return isSessionNoticeMarker(m) ? toggles.showRestoreNotices : toggles.showAgentSwitchIndicator;
|
|
11493
|
+
}
|
|
11494
|
+
if (m.role === "tool") {
|
|
11495
|
+
return false;
|
|
11496
|
+
}
|
|
11497
|
+
if (m.category === "reasoning") {
|
|
11498
|
+
return toggles.showThinkingSteps;
|
|
11499
|
+
}
|
|
11500
|
+
if (m.category === "narration") {
|
|
11501
|
+
return toggles.showNarration;
|
|
11502
|
+
}
|
|
11503
|
+
if (m.thinking && !toggles.showThinkingSteps) {
|
|
11504
|
+
return false;
|
|
11505
|
+
}
|
|
11506
|
+
if (m.role === "assistant" && !m.content?.trim() && !m.toolCalls?.length && !m.interaction) {
|
|
11507
|
+
return false;
|
|
11508
|
+
}
|
|
11509
|
+
const isToolRelated = !!(m.toolCalls?.length || m.toolResult);
|
|
11510
|
+
return !isToolRelated || toggles.showToolCalls;
|
|
11511
|
+
});
|
|
11307
11512
|
}
|
|
11308
|
-
|
|
11309
|
-
|
|
11513
|
+
|
|
11514
|
+
// src/autopilot/decision-message.ts
|
|
11515
|
+
var FALLBACK_DATA_CHARS = 2e3;
|
|
11516
|
+
var READ_CAP_CHARS = 2e4;
|
|
11517
|
+
function capRead(text, max = READ_CAP_CHARS) {
|
|
11518
|
+
if (text.length <= max) return text;
|
|
11519
|
+
return `${text.slice(0, max)}
|
|
11520
|
+
[Cut at ${max} of ${text.length} characters. Ask for less: a narrower view, or a smaller page (an offset or a limit) where the tool takes one.]`;
|
|
11521
|
+
}
|
|
11522
|
+
function widgetAsText(componentName, data, result) {
|
|
11523
|
+
const asText = getInteractionRegistration(componentName)?.asText;
|
|
11524
|
+
if (asText) {
|
|
11525
|
+
try {
|
|
11526
|
+
return asText(data, result);
|
|
11527
|
+
} catch {
|
|
11528
|
+
}
|
|
11529
|
+
}
|
|
11530
|
+
const dump = JSON.stringify(data) ?? "";
|
|
11531
|
+
return `[${componentName}] ${dump.length > FALLBACK_DATA_CHARS ? `${dump.slice(0, FALLBACK_DATA_CHARS)}\u2026` : dump}`;
|
|
11532
|
+
}
|
|
11533
|
+
var NOTHING_ENABLED_LINE = "Nothing here can be done right now: every action is unavailable. Hand it back with hand_back, saying in your reason what is unavailable and why, so the user knows what to do.";
|
|
11534
|
+
var anyActionable = (actions, withholdRefining) => actions.some((a) => a.enabled && !(withholdRefining && a.refines));
|
|
11535
|
+
function renderActions(actions, options = {}) {
|
|
11536
|
+
const numbering = [];
|
|
11537
|
+
const lines = [];
|
|
11538
|
+
for (const action of actions) {
|
|
11539
|
+
if (options.withholdRefining && action.refines) continue;
|
|
11540
|
+
const unavailable = action.enabled ? "" : ` (not available right now${action.unavailable ? `: ${action.unavailable}` : ""})`;
|
|
11541
|
+
if (action.description !== void 0) {
|
|
11542
|
+
const call = action.args === "fields" ? `${action.name}({fields})` : action.args === "text" ? `${action.name}({text})` : `${action.name}()`;
|
|
11543
|
+
const settles = action.finishes ? "settles it" : "does not settle it";
|
|
11544
|
+
lines.push(` ${call} \u2014 ${action.description} (${settles})${unavailable}`);
|
|
11545
|
+
if (action.argsSchema) lines.push(` fields schema: ${JSON.stringify(action.argsSchema)}`);
|
|
11546
|
+
} else if (action.args === "options") {
|
|
11547
|
+
lines.push(` select({options}) \u2014 choose several, by number or label${unavailable}`);
|
|
11548
|
+
} else if (action.args === "text") {
|
|
11549
|
+
lines.push(` reply({text}) \u2014 answer in your own words${unavailable}`);
|
|
11550
|
+
} else {
|
|
11551
|
+
numbering.push(action.name);
|
|
11552
|
+
lines.push(` [${numbering.length}] "${action.label ?? action.name}"${unavailable}`);
|
|
11553
|
+
}
|
|
11554
|
+
}
|
|
11555
|
+
const allSettle = actions.every((a) => a.description === void 0);
|
|
11556
|
+
const actionable = anyActionable(actions, options.withholdRefining);
|
|
11557
|
+
const left = options.refinementsLeft;
|
|
11558
|
+
const refinements = left !== void 0 && Number.isFinite(left) && actions.some((a) => a.refines) ? [
|
|
11559
|
+
`Refinements left at this step: ${left}${left > 0 ? "" : " (written answers and other refining actions are withdrawn)"}`
|
|
11560
|
+
] : [];
|
|
11561
|
+
return {
|
|
11562
|
+
text: [
|
|
11563
|
+
`${allSettle ? "Actions (each settles it)" : "Actions"}:`,
|
|
11564
|
+
...lines,
|
|
11565
|
+
...refinements,
|
|
11566
|
+
...actionable ? [] : [NOTHING_ENABLED_LINE]
|
|
11567
|
+
].join("\n"),
|
|
11568
|
+
numbering,
|
|
11569
|
+
actionable
|
|
11570
|
+
};
|
|
11310
11571
|
}
|
|
11311
|
-
function
|
|
11572
|
+
function renderViews(surface) {
|
|
11573
|
+
const views = Object.entries(surface.views ?? {});
|
|
11574
|
+
if (!views.length) return [];
|
|
11575
|
+
return [
|
|
11576
|
+
"Views (widget_read):",
|
|
11577
|
+
...views.map(([name, description]) => {
|
|
11578
|
+
const shown = name === surface.preview ? " (shown below as Now)" : "";
|
|
11579
|
+
return description ? ` ${name} \u2014 ${description}${shown}` : ` ${name}${shown}`;
|
|
11580
|
+
})
|
|
11581
|
+
];
|
|
11582
|
+
}
|
|
11583
|
+
function renderDecision(opts) {
|
|
11584
|
+
const { surface } = opts;
|
|
11585
|
+
const facts = surface.facts();
|
|
11586
|
+
const actions = renderActions(surface.actions(), {
|
|
11587
|
+
withholdRefining: opts.withholdRefining,
|
|
11588
|
+
refinementsLeft: opts.refinementsLeft
|
|
11589
|
+
});
|
|
11590
|
+
let full = renderViews(surface);
|
|
11591
|
+
if (surface.describe) {
|
|
11592
|
+
let preview = "";
|
|
11593
|
+
if (surface.preview !== void 0) {
|
|
11594
|
+
try {
|
|
11595
|
+
preview = capRead(JSON.stringify(surface.read?.(surface.preview)) ?? "");
|
|
11596
|
+
} catch {
|
|
11597
|
+
preview = "";
|
|
11598
|
+
}
|
|
11599
|
+
}
|
|
11600
|
+
full = [surface.describe(), ...full, ...preview ? [`Now: ${preview}`] : []];
|
|
11601
|
+
}
|
|
11602
|
+
const parts = [
|
|
11603
|
+
`<interaction id="${opts.interactionId}" widget="${opts.componentName}">`,
|
|
11604
|
+
widgetAsText(opts.componentName, opts.data),
|
|
11605
|
+
...full,
|
|
11606
|
+
...Object.keys(facts).length ? [`Facts: ${JSON.stringify(facts)}`] : [],
|
|
11607
|
+
...surface.prompt ? [`Guidance: ${surface.prompt}`] : [],
|
|
11608
|
+
actions.text,
|
|
11609
|
+
// Asked to decide it themselves, with nothing that can be done: handing it back is the only way
|
|
11610
|
+
// out, so it stays open, and the reason tells the person why the autopilot could not.
|
|
11611
|
+
...opts.decideYourself ? [
|
|
11612
|
+
actions.actionable ? "The user handed this one back to you and asked you to use your best judgement: decide it yourself. hand_back is not available." : "The user handed this one back to you and asked you to decide it yourself, but nothing here can be done right now, so hand_back is available after all: use it, and say why you could not decide."
|
|
11613
|
+
] : [],
|
|
11614
|
+
"</interaction>"
|
|
11615
|
+
];
|
|
11616
|
+
return { text: parts.join("\n"), numbering: actions.numbering, actionable: actions.actionable };
|
|
11617
|
+
}
|
|
11618
|
+
var DEFAULT_VISIBILITY = {
|
|
11619
|
+
showToolCalls: false,
|
|
11620
|
+
showThinkingSteps: false,
|
|
11621
|
+
showNarration: false,
|
|
11622
|
+
showAgentSwitchIndicator: false,
|
|
11623
|
+
showRestoreNotices: false
|
|
11624
|
+
};
|
|
11625
|
+
var answeredPhrase = (componentName, resolved, answeredBy, claimant, resolvedItself) => {
|
|
11626
|
+
if (resolved?.status === "timeout") {
|
|
11627
|
+
return "answered by nobody";
|
|
11628
|
+
}
|
|
11629
|
+
if (answeredBy === claimant) return "answered by you";
|
|
11630
|
+
if (resolvedItself && answeredBy === void 0) return "finished";
|
|
11631
|
+
if (answeredBy === void 0 || answeredBy === "user") {
|
|
11632
|
+
return getInteractionRegistration(componentName)?.resolvesItself === true ? "finished" : "answered by the user";
|
|
11633
|
+
}
|
|
11634
|
+
return "answered on the user's behalf";
|
|
11635
|
+
};
|
|
11636
|
+
var SHORT_FORM_RECENT = 20;
|
|
11637
|
+
function visibleTranscript(messages, opts) {
|
|
11638
|
+
const visible = filterVisibleMessages([...messages], DEFAULT_VISIBILITY);
|
|
11639
|
+
const olderThan = opts.short ? visible.length - SHORT_FORM_RECENT : -1;
|
|
11640
|
+
const lines = [];
|
|
11641
|
+
visible.forEach((m, i) => {
|
|
11642
|
+
if (m.interaction) {
|
|
11643
|
+
const { interactionId, componentName, data, resolved, answeredBy, resolvedItself } = m.interaction;
|
|
11644
|
+
if (interactionId === opts.skipInteractionId || !resolved) return;
|
|
11645
|
+
const answered = answeredPhrase(
|
|
11646
|
+
componentName,
|
|
11647
|
+
resolved,
|
|
11648
|
+
answeredBy,
|
|
11649
|
+
opts.claimant,
|
|
11650
|
+
resolvedItself
|
|
11651
|
+
);
|
|
11652
|
+
const text = widgetAsText(componentName, data, resolved);
|
|
11653
|
+
lines.push(`[${componentName}, ${answered}] ${i < olderThan ? text.split("\n")[0] : text}`);
|
|
11654
|
+
return;
|
|
11655
|
+
}
|
|
11656
|
+
if (m.role === "compacted-summary") {
|
|
11657
|
+
lines.push(`[Earlier, summarised] ${m.content ?? ""}`);
|
|
11658
|
+
for (const u of m.compaction?.preservedUserMessages ?? []) {
|
|
11659
|
+
lines.push(
|
|
11660
|
+
u.delegate === opts.claimant && opts.includeOwn ? `You (on the user's behalf, earlier): ${u.content}` : u.delegate ? `User (on their behalf, earlier): ${u.content}` : `User (earlier): ${u.content}`
|
|
11661
|
+
);
|
|
11662
|
+
}
|
|
11663
|
+
return;
|
|
11664
|
+
}
|
|
11665
|
+
if (m.delegate === opts.claimant) {
|
|
11666
|
+
if (!opts.includeOwn || !m.content?.trim()) return;
|
|
11667
|
+
lines.push(
|
|
11668
|
+
m.role === "assistant" ? `You (to the user): ${m.content}` : `You (on the user's behalf): ${m.content}`
|
|
11669
|
+
);
|
|
11670
|
+
return;
|
|
11671
|
+
}
|
|
11672
|
+
if ((m.role === "user" || m.role === "synthetic-user") && m.content?.trim()) {
|
|
11673
|
+
lines.push(m.delegate ? `User (on their behalf): ${m.content}` : `User: ${m.content}`);
|
|
11674
|
+
return;
|
|
11675
|
+
}
|
|
11676
|
+
if (m.role === "assistant" && m.content?.trim()) lines.push(`Assistant: ${m.content}`);
|
|
11677
|
+
});
|
|
11678
|
+
return lines;
|
|
11679
|
+
}
|
|
11680
|
+
|
|
11681
|
+
// src/headless/describe-interaction.ts
|
|
11682
|
+
var describeAction = (action, n) => ({
|
|
11683
|
+
...n !== void 0 ? { n } : {},
|
|
11684
|
+
name: action.name,
|
|
11685
|
+
...action.label !== void 0 ? { label: action.label } : {},
|
|
11686
|
+
...action.description !== void 0 ? { description: action.description } : {},
|
|
11687
|
+
enabled: action.enabled,
|
|
11688
|
+
...action.unavailable !== void 0 ? { unavailable: action.unavailable } : {},
|
|
11689
|
+
...action.args !== void 0 ? { args: action.args } : {},
|
|
11690
|
+
...action.argsSchema !== void 0 ? { argsSchema: action.argsSchema } : {},
|
|
11691
|
+
finishes: action.finishes,
|
|
11692
|
+
...action.refines ? { refines: true } : {}
|
|
11693
|
+
});
|
|
11694
|
+
function describeInteraction(input) {
|
|
11695
|
+
const { ctx, componentName, data } = input;
|
|
11696
|
+
const base = {
|
|
11697
|
+
interactionId: ctx.interactionId,
|
|
11698
|
+
component: componentName,
|
|
11699
|
+
heldBy: ctx.heldBy
|
|
11700
|
+
};
|
|
11701
|
+
const { surface } = ctx;
|
|
11702
|
+
if (!surface) {
|
|
11703
|
+
const hasController = ctx.controller !== void 0;
|
|
11704
|
+
return {
|
|
11705
|
+
...base,
|
|
11706
|
+
controller: hasController,
|
|
11707
|
+
// A controller that has not exposed its surface yet will: wait for it. Without one, the
|
|
11708
|
+
// widget is all there is.
|
|
11709
|
+
ready: !hasController,
|
|
11710
|
+
text: widgetAsText(componentName, data),
|
|
11711
|
+
actions: []
|
|
11712
|
+
};
|
|
11713
|
+
}
|
|
11714
|
+
const rendered = renderDecision({
|
|
11715
|
+
interactionId: ctx.interactionId,
|
|
11716
|
+
componentName,
|
|
11717
|
+
data,
|
|
11718
|
+
surface
|
|
11719
|
+
});
|
|
11720
|
+
const actions = surface.actions().map((a) => {
|
|
11721
|
+
const index = rendered.numbering.indexOf(a.name);
|
|
11722
|
+
return describeAction(a, index >= 0 && a.description === void 0 ? index + 1 : void 0);
|
|
11723
|
+
});
|
|
11724
|
+
let facts;
|
|
11725
|
+
try {
|
|
11726
|
+
const read = surface.facts();
|
|
11727
|
+
if (Object.keys(read).length) facts = { ...read };
|
|
11728
|
+
} catch {
|
|
11729
|
+
}
|
|
11312
11730
|
return {
|
|
11313
|
-
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11731
|
+
...base,
|
|
11732
|
+
controller: true,
|
|
11733
|
+
ready: surface.isReady(),
|
|
11734
|
+
text: rendered.text,
|
|
11735
|
+
actions,
|
|
11736
|
+
...surface.views && Object.keys(surface.views).length ? { views: { ...surface.views } } : {},
|
|
11737
|
+
...facts ? { facts } : {}
|
|
11317
11738
|
};
|
|
11318
11739
|
}
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
}
|
|
11740
|
+
|
|
11741
|
+
// src/state/persistence/diagnostics.ts
|
|
11742
|
+
var FNV_OFFSET_BASIS = 2166136261;
|
|
11743
|
+
var FNV_PRIME = 16777619;
|
|
11744
|
+
var DIGEST_RADIX = 36;
|
|
11745
|
+
function configDigest(json) {
|
|
11746
|
+
let a = FNV_OFFSET_BASIS;
|
|
11747
|
+
let b = FNV_PRIME;
|
|
11748
|
+
for (let i = 0; i < json.length; i += 1) {
|
|
11749
|
+
const c = json.charCodeAt(i);
|
|
11750
|
+
a = Math.imul(a ^ c, FNV_PRIME) >>> 0;
|
|
11751
|
+
b = Math.imul(b ^ c, FNV_OFFSET_BASIS) >>> 0;
|
|
11752
|
+
}
|
|
11753
|
+
return [json.length, a, b].map((n) => n.toString(DIGEST_RADIX)).join("-");
|
|
11754
|
+
}
|
|
11755
|
+
function turnIdentity(entry) {
|
|
11756
|
+
return `turn::${String(entry.turnIndex ?? "")}::${entry.timestamp ?? ""}::${String(entry.agentName ?? "")}`;
|
|
11757
|
+
}
|
|
11758
|
+
var KIND_RANK = { event: 0, turn: 1, message: 2 };
|
|
11759
|
+
var UNKNOWN_KIND_RANK = 99;
|
|
11760
|
+
function assembleDebugLog(entries, readme) {
|
|
11761
|
+
const seenAt = /* @__PURE__ */ new Map();
|
|
11762
|
+
let latestMeta;
|
|
11763
|
+
const timeline = [];
|
|
11764
|
+
for (const entry of entries) {
|
|
11765
|
+
if (entry.kind === "meta-snapshot") {
|
|
11766
|
+
if (!latestMeta || (entry.timestamp ?? "") >= (latestMeta.timestamp ?? "")) {
|
|
11767
|
+
latestMeta = entry;
|
|
11768
|
+
}
|
|
11769
|
+
continue;
|
|
11348
11770
|
}
|
|
11349
|
-
const
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11354
|
-
...bucketsOf(rolled),
|
|
11355
|
-
...m.agentName != null && { agentName: m.agentName },
|
|
11356
|
-
subAgentDepth: depth,
|
|
11357
|
-
...subAgentOf != null && { subAgentOf }
|
|
11358
|
-
});
|
|
11771
|
+
const id = entry.kind === "turn" ? turnIdentity(entry) : JSON.stringify(entry);
|
|
11772
|
+
const at = seenAt.get(id);
|
|
11773
|
+
if (at !== void 0) {
|
|
11774
|
+
if (entry.usage && !timeline[at].usage) timeline[at] = entry;
|
|
11775
|
+
continue;
|
|
11359
11776
|
}
|
|
11360
|
-
|
|
11361
|
-
|
|
11777
|
+
seenAt.set(id, timeline.length);
|
|
11778
|
+
timeline.push(entry);
|
|
11779
|
+
}
|
|
11780
|
+
timeline.sort((a, b) => {
|
|
11781
|
+
const ta = a.timestamp ?? "";
|
|
11782
|
+
const tb = b.timestamp ?? "";
|
|
11783
|
+
if (ta < tb) return -1;
|
|
11784
|
+
if (ta > tb) return 1;
|
|
11785
|
+
return (KIND_RANK[a.kind] ?? UNKNOWN_KIND_RANK) - (KIND_RANK[b.kind] ?? UNKNOWN_KIND_RANK);
|
|
11786
|
+
});
|
|
11787
|
+
const context = latestMeta?.meta?.context;
|
|
11788
|
+
return { readme, sessionUsage: context?.sessionUsage, timeline, meta: latestMeta?.meta };
|
|
11789
|
+
}
|
|
11790
|
+
function withFreshMetaSnapshot(stored, fresh) {
|
|
11791
|
+
return [...stored.filter((e) => e.kind !== "meta-snapshot"), fresh];
|
|
11792
|
+
}
|
|
11793
|
+
|
|
11794
|
+
// src/utils/strip-agent-handlers.ts
|
|
11795
|
+
function stripAgentHandlers(agent) {
|
|
11796
|
+
const serializable = {};
|
|
11797
|
+
for (const [key, value] of Object.entries(agent)) {
|
|
11798
|
+
if (key === "subAgents") continue;
|
|
11799
|
+
if (typeof value === "function") continue;
|
|
11800
|
+
if (value !== null && typeof value === "object" && Object.values(value).some((v) => typeof v === "function")) {
|
|
11801
|
+
continue;
|
|
11362
11802
|
}
|
|
11803
|
+
serializable[key] = value;
|
|
11363
11804
|
}
|
|
11364
|
-
|
|
11805
|
+
if (agent.subAgents?.length) {
|
|
11806
|
+
serializable.subAgents = agent.subAgents.map(stripAgentHandlers);
|
|
11807
|
+
}
|
|
11808
|
+
return serializable;
|
|
11809
|
+
}
|
|
11810
|
+
|
|
11811
|
+
// src/main/meta-snapshot.ts
|
|
11812
|
+
function buildMetaSnapshot(input) {
|
|
11813
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
|
|
11814
|
+
let activeDebugSnapshot;
|
|
11815
|
+
try {
|
|
11816
|
+
activeDebugSnapshot = input.liveActiveAgent?.getDebugSnapshot?.();
|
|
11817
|
+
} catch (e) {
|
|
11818
|
+
activeDebugSnapshot = `<getDebugSnapshot threw: ${e instanceof Error ? e.message : String(e)}>`;
|
|
11819
|
+
}
|
|
11820
|
+
const contextUsagePercent = input.contextTokens != null && input.contextLimit != null && input.contextLimit > 0 ? Math.round(input.contextTokens / input.contextLimit * 100) : void 0;
|
|
11821
|
+
const metaSnapshot = {
|
|
11822
|
+
kind: "meta-snapshot",
|
|
11823
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11824
|
+
meta: {
|
|
11825
|
+
timestamp,
|
|
11826
|
+
host: input.host,
|
|
11827
|
+
// stripAgentHandlers drops every function-valued field (handlers, lifecycle
|
|
11828
|
+
// hooks, onUnresolvedTool, function-form resolvers) AND object handler bags
|
|
11829
|
+
// like an object-form `toolHandlers`, and recurses subAgents — no manual
|
|
11830
|
+
// exclusion list to keep in sync. We only override toolDefinitions
|
|
11831
|
+
// afterwards to expand the fold tree for the log.
|
|
11832
|
+
agentSummary: input.agents?.map((a) => ({
|
|
11833
|
+
...stripAgentHandlers(a),
|
|
11834
|
+
toolDefinitions: Array.isArray(a.toolDefinitions) ? typeof a.toolHandlers === "function" ? (
|
|
11835
|
+
// Static defs + dynamic handlers — can't walk fold tree
|
|
11836
|
+
// because the handler map isn't materialized at log time.
|
|
11837
|
+
a.toolDefinitions
|
|
11838
|
+
) : expandToolTree(a.toolDefinitions, a.toolHandlers ?? {}) : typeof a.toolDefinitions === "function" ? "<dynamic \u2014 resolved per turn>" : []
|
|
11839
|
+
})),
|
|
11840
|
+
activeSystemPrompt: typeof input.activeAgent?.systemPrompt === "function" ? "<dynamic \u2014 resolved per turn>" : input.activeAgent?.systemPrompt,
|
|
11841
|
+
activePrimerHistory: input.activeAgent?.primerHistory,
|
|
11842
|
+
activeFoldStack: input.activeFoldStack,
|
|
11843
|
+
// Context window + cost snapshot. `sessionCostUsd` is the chat-scoped
|
|
11844
|
+
// total (per-message `cost` summed); the transport's lifetime cost is
|
|
11845
|
+
// not included here because it pools other call sites (suggestions,
|
|
11846
|
+
// criteria interpretation, etc.) and would be misleading next to the
|
|
11847
|
+
// chat-only token counts.
|
|
11848
|
+
context: {
|
|
11849
|
+
model: input.activeModel,
|
|
11850
|
+
contextTokens: input.contextTokens,
|
|
11851
|
+
contextLimit: input.contextLimit,
|
|
11852
|
+
contextUsagePercent,
|
|
11853
|
+
sessionCostUsd: input.sessionCostUsd,
|
|
11854
|
+
sessionTokensConsumed: input.sessionTokensConsumed,
|
|
11855
|
+
// The per-bucket split alongside the total. Without it a log shows a large
|
|
11856
|
+
// token count next to a small cost with no way to tell why — the answer is
|
|
11857
|
+
// almost always that most of the prompt was a cache hit.
|
|
11858
|
+
sessionUsage: input.sessionUsage
|
|
11859
|
+
},
|
|
11860
|
+
// Snapshot captured fresh at log-export time — reflects state NOW, which
|
|
11861
|
+
// may have transitioned since the last LLM call.
|
|
11862
|
+
activeDebugSnapshot,
|
|
11863
|
+
debug: input.debugState
|
|
11864
|
+
}
|
|
11865
|
+
};
|
|
11866
|
+
const m = metaSnapshot.meta;
|
|
11867
|
+
metaSnapshot.dedupSignature = configDigest(
|
|
11868
|
+
JSON.stringify({
|
|
11869
|
+
host: m.host,
|
|
11870
|
+
agentSummary: m.agentSummary,
|
|
11871
|
+
activeSystemPrompt: m.activeSystemPrompt,
|
|
11872
|
+
activePrimerHistory: m.activePrimerHistory,
|
|
11873
|
+
activeFoldStack: m.activeFoldStack
|
|
11874
|
+
})
|
|
11875
|
+
);
|
|
11876
|
+
return metaSnapshot;
|
|
11365
11877
|
}
|
|
11366
11878
|
|
|
11367
11879
|
// src/utils/flatten-sub-agent-messages.ts
|
|
@@ -11415,44 +11927,880 @@ function buildTimelineEntries(input) {
|
|
|
11415
11927
|
return [...messageEntries, ...turns, ...eventEntries];
|
|
11416
11928
|
}
|
|
11417
11929
|
|
|
11418
|
-
// src/state/persistence/
|
|
11419
|
-
function
|
|
11420
|
-
|
|
11930
|
+
// src/state/persistence/debug-log-assembly.ts
|
|
11931
|
+
function buildDiagnosticEntries(input) {
|
|
11932
|
+
const { driver, stateKey } = input;
|
|
11933
|
+
const timelineEntries = buildTimelineEntries({
|
|
11934
|
+
turnSnapshots: driver?.getTurnSnapshots?.() ?? [],
|
|
11935
|
+
messages: driver?.getRawHistory?.() ?? input.messages ?? [],
|
|
11936
|
+
metaEvents: stateKey ? getMetaEvents(stateKey) : []
|
|
11937
|
+
});
|
|
11938
|
+
return [
|
|
11939
|
+
...timelineEntries,
|
|
11940
|
+
...driver?.getExternalDiagnostics?.() ?? [],
|
|
11941
|
+
input.metaSnapshot()
|
|
11942
|
+
];
|
|
11943
|
+
}
|
|
11944
|
+
async function lifetimeDebugLog(input) {
|
|
11945
|
+
const { provider, stateKey } = input;
|
|
11946
|
+
if (provider?.loadDiagnostics && stateKey) {
|
|
11947
|
+
try {
|
|
11948
|
+
await input.flush();
|
|
11949
|
+
const stored = await provider.loadDiagnostics(stateKey);
|
|
11950
|
+
if (stored.length) {
|
|
11951
|
+
return assembleDebugLog(
|
|
11952
|
+
withFreshMetaSnapshot(stored, input.metaSnapshot()),
|
|
11953
|
+
DEBUG_LOG_README
|
|
11954
|
+
);
|
|
11955
|
+
}
|
|
11956
|
+
} catch (e) {
|
|
11957
|
+
logger2.error("Diagnostics load failed \u2014 using current-page log:", e);
|
|
11958
|
+
}
|
|
11959
|
+
}
|
|
11960
|
+
return input.fallback();
|
|
11421
11961
|
}
|
|
11422
|
-
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
const
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
|
|
11431
|
-
|
|
11962
|
+
|
|
11963
|
+
// src/headless/headless-debug.ts
|
|
11964
|
+
var DEFAULT_HOST = "headless";
|
|
11965
|
+
function headlessMetaSnapshot(driver, options = {}) {
|
|
11966
|
+
const history = driver.getRawHistory?.() ?? [];
|
|
11967
|
+
const usage = sumUsage(history);
|
|
11968
|
+
const live2 = driver.getActiveAgent();
|
|
11969
|
+
const context = options.context?.();
|
|
11970
|
+
return buildMetaSnapshot({
|
|
11971
|
+
agents: options.agents ? [...options.agents()] : void 0,
|
|
11972
|
+
activeAgent: live2 ? stripAgentHandlers(live2) : void 0,
|
|
11973
|
+
liveActiveAgent: live2,
|
|
11974
|
+
activeFoldStack: driver instanceof ChatDriver ? driver.getActiveFoldNames() : void 0,
|
|
11975
|
+
activeModel: context?.model,
|
|
11976
|
+
contextTokens: isContextMeasurable(history) ? estimateContextTokens(history) : void 0,
|
|
11977
|
+
contextLimit: context?.contextLimit,
|
|
11978
|
+
sessionCostUsd: usage.costUsd,
|
|
11979
|
+
sessionTokensConsumed: totalTokens(usage),
|
|
11980
|
+
sessionUsage: usage,
|
|
11981
|
+
debugState: options.debugState?.(),
|
|
11982
|
+
host: options.host ?? DEFAULT_HOST
|
|
11983
|
+
});
|
|
11984
|
+
}
|
|
11985
|
+
function headlessDiagnosticEntries(driver, stateKey, options = {}) {
|
|
11986
|
+
return buildDiagnosticEntries({
|
|
11987
|
+
driver,
|
|
11988
|
+
stateKey,
|
|
11989
|
+
metaSnapshot: () => headlessMetaSnapshot(driver, options)
|
|
11990
|
+
});
|
|
11991
|
+
}
|
|
11992
|
+
function headlessCurrentDebugLog(driver, stateKey, options = {}) {
|
|
11993
|
+
return assembleDebugLog(headlessDiagnosticEntries(driver, stateKey, options), DEBUG_LOG_README);
|
|
11994
|
+
}
|
|
11995
|
+
|
|
11996
|
+
// src/autopilot/autopilot-session.ts
|
|
11997
|
+
function parseFields(fields) {
|
|
11998
|
+
if (typeof fields === "object") return { ok: true, value: fields };
|
|
11999
|
+
if (typeof fields !== "string") return { ok: false };
|
|
12000
|
+
const fenced = /^\s*```[a-zA-Z]*\s*\n?([\s\S]*?)\n?\s*```\s*$/.exec(fields);
|
|
12001
|
+
try {
|
|
12002
|
+
return { ok: true, value: JSON.parse(fenced ? fenced[1] : fields) };
|
|
12003
|
+
} catch {
|
|
12004
|
+
return { ok: false };
|
|
12005
|
+
}
|
|
12006
|
+
}
|
|
12007
|
+
var byNumberOf = (numbering) => (value) => /^\d+$/.test(value.trim()) ? numbering[Number(value.trim()) - 1] : void 0;
|
|
12008
|
+
function invokeArgsFor(listed, given, byNumber) {
|
|
12009
|
+
if (listed?.args === "text") return { ok: true, args: { text: given.text } };
|
|
12010
|
+
if (given.fields !== void 0 && given.fields !== null && given.fields !== "") {
|
|
12011
|
+
const parsed = parseFields(given.fields);
|
|
12012
|
+
return parsed.ok ? { ok: true, args: parsed.value } : { ok: false };
|
|
12013
|
+
}
|
|
12014
|
+
return {
|
|
12015
|
+
ok: true,
|
|
12016
|
+
args: {
|
|
12017
|
+
...given.options ? { options: given.options.map((o) => byNumber(o) ?? o) } : {},
|
|
12018
|
+
...given.text !== void 0 ? { text: given.text } : {}
|
|
12019
|
+
}
|
|
12020
|
+
};
|
|
12021
|
+
}
|
|
12022
|
+
function fieldsExample(action, schema) {
|
|
12023
|
+
if (!schema) return "Send fields as a JSON object, as text.";
|
|
12024
|
+
const call = JSON.stringify({ action, fields: JSON.stringify(exampleArgs(schema)) });
|
|
12025
|
+
return `Send them as a JSON object inside a string, e.g. widget_act(${call}).`;
|
|
12026
|
+
}
|
|
12027
|
+
|
|
12028
|
+
// src/headless/headless-session.ts
|
|
12029
|
+
var HEADLESS_INTERCEPTOR_ID = "headless";
|
|
12030
|
+
var NOT_A_CLAIMANT = HEADLESS_INTERCEPTOR_ID;
|
|
12031
|
+
var LINE_CAP_CHARS = 4e3;
|
|
12032
|
+
var MAX_EVENTS = 5e3;
|
|
12033
|
+
var TURN_POLL_MS = 50;
|
|
12034
|
+
var MAX_TIMER_MS = 2147483647;
|
|
12035
|
+
var capped = (text) => text.length > LINE_CAP_CHARS ? { text: `${text.slice(0, LINE_CAP_CHARS)}\u2026`, chars: text.length } : { text };
|
|
12036
|
+
var timerMs = (ms) => Number.isNaN(Number(ms)) ? 0 : Math.min(Math.max(0, Number(ms)), MAX_TIMER_MS);
|
|
12037
|
+
var messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
12038
|
+
var isAutopilotLike = (value) => {
|
|
12039
|
+
const v = value;
|
|
12040
|
+
return !!v && typeof v === "object" && typeof v.subscribe === "function" && typeof v.pause === "function" && typeof v.resume === "function" && typeof v.stop === "function" && typeof v.state === "object" && v.state !== null;
|
|
12041
|
+
};
|
|
12042
|
+
var HeadlessSession = class {
|
|
12043
|
+
constructor(driver, options = {}) {
|
|
12044
|
+
this.driver = driver;
|
|
12045
|
+
this.options = options;
|
|
12046
|
+
this.events = [];
|
|
12047
|
+
this.seq = 0;
|
|
12048
|
+
this.tracked = /* @__PURE__ */ new Map();
|
|
12049
|
+
/** What the session did to an interaction itself, for `settled` to say when the driver reports it. */
|
|
12050
|
+
this.origin = /* @__PURE__ */ new Map();
|
|
12051
|
+
this.waiters = /* @__PURE__ */ new Set();
|
|
12052
|
+
this.teardown = [];
|
|
12053
|
+
this.lastProbe = {};
|
|
12054
|
+
this.lastAutopilot = "";
|
|
12055
|
+
this.disposed = false;
|
|
12056
|
+
if (typeof driver.addInteractionInterceptor !== "function") {
|
|
12057
|
+
throw new Error(
|
|
12058
|
+
"HeadlessSession needs a driver that supports interaction interceptors (addInteractionInterceptor), and this one does not."
|
|
12059
|
+
);
|
|
12060
|
+
}
|
|
12061
|
+
this.sessionKey = options.sessionKey ?? options.persistence?.stateKey ?? "";
|
|
12062
|
+
this.allowRaw = options.allowRaw ?? true;
|
|
12063
|
+
this.clock = options.clock ?? Date.now;
|
|
12064
|
+
this.seen = this.history().length;
|
|
12065
|
+
this.base = this.seen;
|
|
12066
|
+
this.lastProbe = this.readProbe();
|
|
12067
|
+
this.teardown.push(
|
|
12068
|
+
driver.addInteractionInterceptor({
|
|
12069
|
+
id: HEADLESS_INTERCEPTOR_ID,
|
|
12070
|
+
onInteractionOpened: (opened) => {
|
|
12071
|
+
this.onOpened(opened);
|
|
12072
|
+
return void 0;
|
|
12073
|
+
}
|
|
12074
|
+
})
|
|
12075
|
+
);
|
|
12076
|
+
this.listen("history-updated", () => this.onHistory());
|
|
12077
|
+
this.listen(
|
|
12078
|
+
"interaction-settled",
|
|
12079
|
+
(e) => this.onSettled(
|
|
12080
|
+
e.detail
|
|
12081
|
+
)
|
|
12082
|
+
);
|
|
12083
|
+
if (options.activityBus) {
|
|
12084
|
+
this.teardown.push(
|
|
12085
|
+
options.activityBus.subscribe("tool-loop-start", () => this.observe()),
|
|
12086
|
+
options.activityBus.subscribe("tool-loop-end", (detail) => {
|
|
12087
|
+
if (detail?.failureReason && this.open) this.open.failureReason = detail.failureReason;
|
|
12088
|
+
this.observe();
|
|
12089
|
+
})
|
|
12090
|
+
);
|
|
12091
|
+
}
|
|
12092
|
+
const autopilot = options.autopilot ?? (isAutopilotLike(driver) ? driver : void 0);
|
|
12093
|
+
this.autopilotHost = autopilot;
|
|
12094
|
+
if (autopilot) {
|
|
12095
|
+
this.lastAutopilot = this.autopilotSignature();
|
|
12096
|
+
this.teardown.push(autopilot.subscribe(() => this.onAutopilot()));
|
|
12097
|
+
}
|
|
12098
|
+
this.persistence = options.persistence?.attach({ driver, sessionKey: this.sessionKey });
|
|
12099
|
+
recordMetaEvent(
|
|
12100
|
+
this.sessionKey,
|
|
12101
|
+
"headless.attached",
|
|
12102
|
+
options.label !== void 0 ? { label: options.label } : void 0
|
|
12103
|
+
);
|
|
12104
|
+
}
|
|
12105
|
+
// ---------------------------------------------------------------------------------------------
|
|
12106
|
+
// The calls
|
|
12107
|
+
// ---------------------------------------------------------------------------------------------
|
|
12108
|
+
/**
|
|
12109
|
+
* Send the person's message, and start the turn. Returns as soon as the turn has started, not
|
|
12110
|
+
* when it ends: a turn includes every interaction parked on the person, so it can run as long as
|
|
12111
|
+
* the journey. Its end is the `turn` event.
|
|
12112
|
+
*
|
|
12113
|
+
* Refused, never thrown, when the message is empty, or a turn is already running.
|
|
12114
|
+
*/
|
|
12115
|
+
send(text, options = {}) {
|
|
12116
|
+
if (this.disposed) return { ok: false, error: "This session has been disposed." };
|
|
12117
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
12118
|
+
return { ok: false, error: "There is nothing to send: the message is empty." };
|
|
12119
|
+
}
|
|
12120
|
+
if (this.turn || this.driver.isBusy()) {
|
|
12121
|
+
return {
|
|
12122
|
+
ok: false,
|
|
12123
|
+
error: "A turn is already running. Wait for it to end (the turn event), or answer what it is waiting on."
|
|
12124
|
+
};
|
|
12125
|
+
}
|
|
12126
|
+
this.closeTurn();
|
|
12127
|
+
const start = this.history().length;
|
|
12128
|
+
this.open = { start };
|
|
12129
|
+
this.startPolling();
|
|
12130
|
+
this.persistence?.notePrompt(text);
|
|
12131
|
+
let running;
|
|
12132
|
+
try {
|
|
12133
|
+
running = this.driver.sendMessage(
|
|
12134
|
+
text,
|
|
12135
|
+
options.attachments,
|
|
12136
|
+
options.agent !== void 0 ? { agent: options.agent } : void 0
|
|
12137
|
+
).then(
|
|
12138
|
+
(result) => {
|
|
12139
|
+
this.turn = void 0;
|
|
12140
|
+
if (this.open) {
|
|
12141
|
+
this.open.reason = result.reason;
|
|
12142
|
+
this.open.failureReason = result.failureReason;
|
|
12143
|
+
}
|
|
12144
|
+
this.observe();
|
|
12145
|
+
},
|
|
12146
|
+
(e) => {
|
|
12147
|
+
this.turn = void 0;
|
|
12148
|
+
this.log({ type: "error", where: "turn", message: messageOf(e) });
|
|
12149
|
+
if (this.open) this.open.reason = "error";
|
|
12150
|
+
this.observe();
|
|
12151
|
+
}
|
|
12152
|
+
);
|
|
12153
|
+
} catch (e) {
|
|
12154
|
+
this.open = void 0;
|
|
12155
|
+
this.stopPolling();
|
|
12156
|
+
return { ok: false, error: messageOf(e) };
|
|
12157
|
+
}
|
|
12158
|
+
this.turn = running;
|
|
12159
|
+
return { ok: true };
|
|
12160
|
+
}
|
|
12161
|
+
/**
|
|
12162
|
+
* Run one of a pending interaction's actions as the person, as a click would (the answer is
|
|
12163
|
+
* credited `answeredBy: 'user'`). `action` is a number from the description's actions, or an
|
|
12164
|
+
* action's name; `args` carry the text, the options (numbers or labels) or the form's fields it
|
|
12165
|
+
* takes. Refused, with the reason and what is enabled, when a claimant holds the interaction,
|
|
12166
|
+
* the action is unknown or disabled, or its arguments do not fit: nothing runs then.
|
|
12167
|
+
*
|
|
12168
|
+
* The result is the driver's `InvokeResult`, plus `description` of the interaction when it is
|
|
12169
|
+
* still open.
|
|
12170
|
+
*/
|
|
12171
|
+
async act(interactionId, action, args = {}) {
|
|
12172
|
+
if (typeof this.driver.invokeAsUser !== "function") {
|
|
12173
|
+
return {
|
|
12174
|
+
ok: false,
|
|
12175
|
+
error: "no-surface",
|
|
12176
|
+
message: "This driver cannot run an action as the person (it has no invokeAsUser)."
|
|
12177
|
+
};
|
|
12178
|
+
}
|
|
12179
|
+
const t = this.tracked.get(interactionId);
|
|
12180
|
+
const requested = String(action ?? "").trim();
|
|
12181
|
+
let name = requested;
|
|
12182
|
+
let invokeArgs = args;
|
|
12183
|
+
if (t?.ctx.surface) {
|
|
12184
|
+
const surface = t.ctx.surface;
|
|
12185
|
+
const numbering = this.describe(t).actions.filter((a) => a.n !== void 0).map((a) => a.name);
|
|
12186
|
+
const byNumber = byNumberOf(numbering);
|
|
12187
|
+
name = byNumber(requested) ?? requested;
|
|
12188
|
+
const listed = surface.actions().find((a) => a.name === name);
|
|
12189
|
+
const built = invokeArgsFor(listed, args, byNumber);
|
|
12190
|
+
if (!built.ok) {
|
|
12191
|
+
return {
|
|
12192
|
+
ok: false,
|
|
12193
|
+
error: "invalid-args",
|
|
12194
|
+
message: `fields is not valid JSON. ${fieldsExample(name, listed?.argsSchema)}`
|
|
12195
|
+
};
|
|
11432
12196
|
}
|
|
11433
|
-
|
|
12197
|
+
invokeArgs = built.args;
|
|
11434
12198
|
}
|
|
11435
|
-
const
|
|
11436
|
-
|
|
11437
|
-
|
|
11438
|
-
|
|
11439
|
-
|
|
12199
|
+
const result = await this.driver.invokeAsUser(interactionId, name, invokeArgs);
|
|
12200
|
+
if (t) this.evaluate(t);
|
|
12201
|
+
const still = this.tracked.get(interactionId);
|
|
12202
|
+
return still && !still.settled ? { ...result, description: this.describe(still) } : result;
|
|
12203
|
+
}
|
|
12204
|
+
/**
|
|
12205
|
+
* Settle a pending interaction raw, with `result` as the widget would have resolved it,
|
|
12206
|
+
* bypassing its actions and everything they check: an escape hatch for a widget with no
|
|
12207
|
+
* controller, or one that cannot be acted on. Logs `settled` with `raw: true` and records
|
|
12208
|
+
* `headless.raw-resolved`. Refused when `allowRaw` is `false`, when the interaction is not
|
|
12209
|
+
* pending, or when a claimant holds it: as for `act`, the person takes it back first.
|
|
12210
|
+
*/
|
|
12211
|
+
resolve(interactionId, result) {
|
|
12212
|
+
if (!this.allowRaw) {
|
|
12213
|
+
return { ok: false, error: "Settling an interaction raw is not allowed in this session." };
|
|
11440
12214
|
}
|
|
11441
|
-
|
|
11442
|
-
|
|
12215
|
+
const t = this.tracked.get(interactionId);
|
|
12216
|
+
if (!t || t.settled) {
|
|
12217
|
+
return { ok: false, error: `No pending interaction "${interactionId}".` };
|
|
12218
|
+
}
|
|
12219
|
+
if (t.ctx.heldBy !== "user") {
|
|
12220
|
+
return {
|
|
12221
|
+
ok: false,
|
|
12222
|
+
error: "Another agent holds this interaction, so it was not resolved. The person has to take it back first, for example by pausing the autopilot."
|
|
12223
|
+
};
|
|
12224
|
+
}
|
|
12225
|
+
this.origin.set(interactionId, { raw: true });
|
|
12226
|
+
try {
|
|
12227
|
+
this.driver.resolveInteraction(interactionId, result);
|
|
12228
|
+
} catch (e) {
|
|
12229
|
+
this.origin.delete(interactionId);
|
|
12230
|
+
return { ok: false, error: messageOf(e) };
|
|
12231
|
+
}
|
|
12232
|
+
recordMetaEvent(this.sessionKey, "headless.raw-resolved", {
|
|
12233
|
+
interactionId,
|
|
12234
|
+
component: t.componentName
|
|
12235
|
+
});
|
|
12236
|
+
return { ok: true };
|
|
11443
12237
|
}
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
12238
|
+
/**
|
|
12239
|
+
* Read from a pending interaction without changing it: one of its views, or (with `tool`) one of
|
|
12240
|
+
* the widget tools given to the session. The text is capped. A view the surface does not list is
|
|
12241
|
+
* refused, naming those it does, as the autopilot's `widget_read` does.
|
|
12242
|
+
*/
|
|
12243
|
+
async read(interactionId, request = {}) {
|
|
12244
|
+
const t = this.tracked.get(interactionId);
|
|
12245
|
+
if (!t || t.settled) return { ok: false, error: `No pending interaction "${interactionId}".` };
|
|
12246
|
+
if (request.tool !== void 0 && request.view !== void 0) {
|
|
12247
|
+
return { ok: false, error: "Ask for a view or a tool, not both." };
|
|
12248
|
+
}
|
|
12249
|
+
if (request.tool !== void 0) return this.readTool(t, request.tool, request.args);
|
|
12250
|
+
const { surface } = t.ctx;
|
|
12251
|
+
if (!surface)
|
|
12252
|
+
return { ok: false, error: "This widget has nothing to read: it has no surface." };
|
|
12253
|
+
const view = typeof request.view === "string" && request.view.trim() ? request.view.trim() : void 0;
|
|
12254
|
+
const names = Object.keys(surface.views ?? {});
|
|
12255
|
+
if (view !== void 0 && names.length && !names.includes(view)) {
|
|
12256
|
+
return { ok: false, error: `No view "${view}" here. Views: ${names.join(", ")}.` };
|
|
12257
|
+
}
|
|
12258
|
+
let state;
|
|
12259
|
+
try {
|
|
12260
|
+
state = surface.read ? surface.read(view) : surface.facts();
|
|
12261
|
+
} catch (e) {
|
|
12262
|
+
return { ok: false, error: `Could not read: ${messageOf(e)}` };
|
|
12263
|
+
}
|
|
12264
|
+
if (state === void 0) {
|
|
12265
|
+
return {
|
|
12266
|
+
ok: false,
|
|
12267
|
+
error: `Nothing to read without a view.${names.length ? ` Views: ${names.join(", ")}.` : ""}`
|
|
12268
|
+
};
|
|
12269
|
+
}
|
|
12270
|
+
return { ok: true, content: capRead(JSON.stringify(state) ?? "null") };
|
|
12271
|
+
}
|
|
12272
|
+
/**
|
|
12273
|
+
* Wait for the log to say something, then return what it said. Resolves as soon as an event in
|
|
12274
|
+
* `until` is in the log after `since` (immediately, when one already is), or at `maxWaitMs` with
|
|
12275
|
+
* status `'working'`. Either way it returns every event after `since`, and `nextSeq`, the cursor
|
|
12276
|
+
* for the next call: poll with it and nothing is lost.
|
|
12277
|
+
*/
|
|
12278
|
+
wait(request) {
|
|
12279
|
+
const since = request.since ?? 0;
|
|
12280
|
+
const { until } = request;
|
|
12281
|
+
if (this.events.some((e) => e.seq > since && wanted(e, until))) {
|
|
12282
|
+
return Promise.resolve(this.waited(since, "ready"));
|
|
12283
|
+
}
|
|
12284
|
+
if (this.disposed) return Promise.resolve(this.waited(since, "closed"));
|
|
12285
|
+
const { signal } = request;
|
|
12286
|
+
if (signal?.aborted) return Promise.resolve(this.waited(since, "working"));
|
|
12287
|
+
return new Promise((resolve) => {
|
|
12288
|
+
const onAbort = () => waiter.finish("working");
|
|
12289
|
+
const waiter = {
|
|
12290
|
+
since,
|
|
12291
|
+
until,
|
|
12292
|
+
finish: (status) => {
|
|
12293
|
+
clearTimeout(timer);
|
|
12294
|
+
signal?.removeEventListener("abort", onAbort);
|
|
12295
|
+
this.waiters.delete(waiter);
|
|
12296
|
+
resolve(this.waited(since, status));
|
|
12297
|
+
}
|
|
12298
|
+
};
|
|
12299
|
+
this.waiters.add(waiter);
|
|
12300
|
+
const timer = setTimeout(() => waiter.finish("working"), timerMs(request.maxWaitMs));
|
|
12301
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
12302
|
+
});
|
|
12303
|
+
}
|
|
12304
|
+
/** Every event logged after `since` (default: all the log keeps), without waiting. */
|
|
12305
|
+
eventsSince(since = 0) {
|
|
12306
|
+
return this.events.filter((e) => e.seq > since);
|
|
12307
|
+
}
|
|
12308
|
+
/**
|
|
12309
|
+
* What is going on now: whether a turn runs, the interaction the person should answer first (and
|
|
12310
|
+
* the others open behind it), the autopilot, what it has cost, and the host's probe.
|
|
12311
|
+
*/
|
|
12312
|
+
state() {
|
|
12313
|
+
const open = [...this.tracked.values()].filter((t) => t.armed && !t.settled);
|
|
12314
|
+
const described = open.map((t) => ({ t, d: this.describe(t) }));
|
|
12315
|
+
const mine = described.find(({ d }) => d.heldBy === "user" && d.ready);
|
|
12316
|
+
const others = described.filter((entry) => entry !== mine).map(({ d }) => ({
|
|
12317
|
+
interactionId: d.interactionId,
|
|
12318
|
+
component: d.component,
|
|
12319
|
+
heldBy: d.heldBy,
|
|
12320
|
+
ready: d.ready
|
|
12321
|
+
}));
|
|
12322
|
+
const host = this.autopilotHost;
|
|
12323
|
+
return {
|
|
12324
|
+
busy: this.turn !== void 0 || this.driver.isBusy() || host?.state.working === true,
|
|
12325
|
+
pending: mine?.d ?? null,
|
|
12326
|
+
others,
|
|
12327
|
+
...host ? {
|
|
12328
|
+
autopilot: {
|
|
12329
|
+
mode: host.state.mode,
|
|
12330
|
+
status: host.state.status,
|
|
12331
|
+
needsUser: host.state.needsUser,
|
|
12332
|
+
working: host.state.working
|
|
12333
|
+
}
|
|
12334
|
+
} : {},
|
|
12335
|
+
cost: sumUsage(this.history()),
|
|
12336
|
+
probe: this.readProbe(),
|
|
12337
|
+
lastSeq: this.seq
|
|
12338
|
+
};
|
|
12339
|
+
}
|
|
12340
|
+
/**
|
|
12341
|
+
* Save the session now: the snapshot, and the diagnostics appended since the last flush, rather
|
|
12342
|
+
* than at the next throttled save. A no-op without `persistence`.
|
|
12343
|
+
*/
|
|
12344
|
+
async flush() {
|
|
12345
|
+
await this.persistence?.flush();
|
|
12346
|
+
}
|
|
12347
|
+
/**
|
|
12348
|
+
* The debug log, the document the browser's Download gives. With `persistence`, the log of the
|
|
12349
|
+
* whole session stitched from the stored stream with a fresh `meta-snapshot` (`lifetimeDebugLog`,
|
|
12350
|
+
* the same code); without, the current page's, from `buildDiagnosticEntries`. Never throws.
|
|
12351
|
+
*/
|
|
12352
|
+
debugLog() {
|
|
12353
|
+
if (this.persistence) return this.persistence.debugLog();
|
|
12354
|
+
return Promise.resolve(
|
|
12355
|
+
headlessCurrentDebugLog(this.driver, this.sessionKey, this.options.debug)
|
|
12356
|
+
);
|
|
12357
|
+
}
|
|
12358
|
+
/** Stop the running turn, when the driver can. */
|
|
12359
|
+
stop() {
|
|
12360
|
+
if (typeof this.driver.cancel !== "function") {
|
|
12361
|
+
return { ok: false, error: "This driver cannot cancel a turn." };
|
|
12362
|
+
}
|
|
12363
|
+
this.driver.cancel();
|
|
12364
|
+
return { ok: true };
|
|
12365
|
+
}
|
|
12366
|
+
/**
|
|
12367
|
+
* Pause, resume or stop the autopilot, when there is one. Pausing hands what it holds to the
|
|
12368
|
+
* person, which then appears as a `decision`.
|
|
12369
|
+
*
|
|
12370
|
+
* `'keepPaused'` is the hand-back banner's "Keep paused": it turns the autopilot's own hand-back
|
|
12371
|
+
* of one interaction into an ordinary pause, so it does not take the question back once it is
|
|
12372
|
+
* answered. `interactionId` names the interaction handed back; omitted, it is the one the
|
|
12373
|
+
* autopilot's own pause is waiting on (`state().autopilot`'s hand-back, the `decision` it left
|
|
12374
|
+
* with the person). Refused, with why, when the autopilot has handed nothing back, when
|
|
12375
|
+
* `interactionId` is not the one it handed back, or when the host cannot keep paused; the host's
|
|
12376
|
+
* own `keepPaused` would do nothing in those cases without saying so.
|
|
12377
|
+
*/
|
|
12378
|
+
autopilot(mode, interactionId) {
|
|
12379
|
+
const host = this.autopilotHost;
|
|
12380
|
+
if (!host) return { ok: false, error: "There is no autopilot on this driver." };
|
|
12381
|
+
try {
|
|
12382
|
+
if (mode === "keepPaused") return this.keepPaused(host, interactionId);
|
|
12383
|
+
host[mode]();
|
|
12384
|
+
} catch (e) {
|
|
12385
|
+
return { ok: false, error: messageOf(e) };
|
|
12386
|
+
}
|
|
12387
|
+
return { ok: true };
|
|
12388
|
+
}
|
|
12389
|
+
keepPaused(host, interactionId) {
|
|
12390
|
+
if (typeof host.keepPaused !== "function") {
|
|
12391
|
+
return { ok: false, error: "This autopilot cannot keep paused." };
|
|
12392
|
+
}
|
|
12393
|
+
const pause = host.state.pause;
|
|
12394
|
+
if (pause?.scope !== "this-interaction" || !pause.interactionId) {
|
|
12395
|
+
return {
|
|
12396
|
+
ok: false,
|
|
12397
|
+
error: "The autopilot has not handed a question back, so there is nothing to keep paused."
|
|
12398
|
+
};
|
|
12399
|
+
}
|
|
12400
|
+
const handedBack = pause.interactionId;
|
|
12401
|
+
if (interactionId !== void 0 && interactionId !== handedBack) {
|
|
12402
|
+
return {
|
|
12403
|
+
ok: false,
|
|
12404
|
+
error: `The autopilot handed back ${handedBack}, not ${interactionId}.`
|
|
12405
|
+
};
|
|
12406
|
+
}
|
|
12407
|
+
host.keepPaused(handedBack);
|
|
12408
|
+
return { ok: true };
|
|
12409
|
+
}
|
|
12410
|
+
/** Detach: remove the interceptor and every subscription, and release anyone waiting. */
|
|
12411
|
+
dispose() {
|
|
12412
|
+
if (this.disposed) return;
|
|
12413
|
+
this.disposed = true;
|
|
12414
|
+
for (const t of this.tracked.values()) this.release(t);
|
|
12415
|
+
clearTimeout(this.quiesceTimer);
|
|
12416
|
+
this.stopPolling();
|
|
12417
|
+
this.persistence?.dispose();
|
|
12418
|
+
for (const off of this.teardown.splice(0)) {
|
|
12419
|
+
try {
|
|
12420
|
+
off();
|
|
12421
|
+
} catch (e) {
|
|
12422
|
+
logger2.warn("HeadlessSession: a teardown threw:", e);
|
|
12423
|
+
}
|
|
12424
|
+
}
|
|
12425
|
+
for (const waiter of this.waiters) waiter.finish("closed");
|
|
12426
|
+
}
|
|
12427
|
+
// ---------------------------------------------------------------------------------------------
|
|
12428
|
+
// Watching the driver
|
|
12429
|
+
// ---------------------------------------------------------------------------------------------
|
|
12430
|
+
listen(type, handler) {
|
|
12431
|
+
this.driver.addEventListener(type, handler);
|
|
12432
|
+
this.teardown.push(() => this.driver.removeEventListener(type, handler));
|
|
12433
|
+
}
|
|
12434
|
+
history() {
|
|
12435
|
+
return this.driver.getRawHistory?.() ?? [];
|
|
12436
|
+
}
|
|
12437
|
+
/** An interaction opened: watch it. Who holds it is not decided yet, so it is reported later. */
|
|
12438
|
+
onOpened(opened) {
|
|
12439
|
+
if (this.disposed) return;
|
|
12440
|
+
const { ctx } = opened;
|
|
12441
|
+
const t = {
|
|
12442
|
+
id: opened.interactionId,
|
|
12443
|
+
ctx,
|
|
12444
|
+
componentName: opened.componentName,
|
|
12445
|
+
data: opened.data,
|
|
12446
|
+
ownerPath: opened.ownerPath,
|
|
12447
|
+
unsubs: [],
|
|
12448
|
+
armed: false,
|
|
12449
|
+
settled: false,
|
|
12450
|
+
autoTried: false
|
|
12451
|
+
};
|
|
12452
|
+
this.tracked.set(t.id, t);
|
|
12453
|
+
t.unsubs.push(ctx.subscribe(() => this.evaluate(t)));
|
|
12454
|
+
t.unsubs.push(ctx.onSurfaceReady(() => this.evaluate(t)));
|
|
12455
|
+
ctx.onDispose(() => this.onEnded(t));
|
|
12456
|
+
queueMicrotask(() => {
|
|
12457
|
+
if (this.disposed || t.settled) return;
|
|
12458
|
+
t.armed = true;
|
|
12459
|
+
this.evaluate(t);
|
|
12460
|
+
});
|
|
12461
|
+
}
|
|
12462
|
+
/** The driver says an interaction settled, by whatever route. */
|
|
12463
|
+
onSettled(detail) {
|
|
12464
|
+
const t = this.tracked.get(detail.interactionId);
|
|
12465
|
+
if (!t || t.settled) return;
|
|
12466
|
+
const origin = this.origin.get(t.id);
|
|
12467
|
+
this.settle(t, {
|
|
12468
|
+
...detail.answeredBy ? { answeredBy: detail.answeredBy } : {},
|
|
12469
|
+
...detail.resolvedItself ? { resolvedItself: true } : {},
|
|
12470
|
+
...origin?.auto ? { auto: true } : {},
|
|
12471
|
+
...origin?.raw ? { raw: true } : {}
|
|
12472
|
+
});
|
|
12473
|
+
}
|
|
12474
|
+
/** The interaction's context was disposed: if nothing said it settled, say it did. */
|
|
12475
|
+
onEnded(t) {
|
|
12476
|
+
if (!t.settled) this.settle(t, {});
|
|
12477
|
+
}
|
|
12478
|
+
settle(t, how) {
|
|
12479
|
+
t.settled = true;
|
|
12480
|
+
if (!this.disposed) {
|
|
12481
|
+
this.log({ type: "settled", interactionId: t.id, component: t.componentName, ...how });
|
|
12482
|
+
}
|
|
12483
|
+
this.release(t);
|
|
12484
|
+
}
|
|
12485
|
+
release(t) {
|
|
12486
|
+
for (const off of t.unsubs.splice(0)) {
|
|
12487
|
+
try {
|
|
12488
|
+
off();
|
|
12489
|
+
} catch {
|
|
12490
|
+
}
|
|
12491
|
+
}
|
|
12492
|
+
t.settled = true;
|
|
12493
|
+
this.tracked.delete(t.id);
|
|
12494
|
+
this.origin.delete(t.id);
|
|
12495
|
+
}
|
|
12496
|
+
/** New messages in the driver's history: report what the person would see of them. */
|
|
12497
|
+
onHistory() {
|
|
12498
|
+
if (this.disposed) return;
|
|
12499
|
+
this.reportMessages();
|
|
12500
|
+
this.observe();
|
|
12501
|
+
}
|
|
12502
|
+
reportMessages() {
|
|
12503
|
+
const history = this.history();
|
|
12504
|
+
if (history.length < this.seen) {
|
|
12505
|
+
this.seen = history.length;
|
|
12506
|
+
return;
|
|
12507
|
+
}
|
|
12508
|
+
const fresh = history.slice(this.seen);
|
|
12509
|
+
this.seen = history.length;
|
|
12510
|
+
const lines = visibleTranscript(
|
|
12511
|
+
fresh.filter((m) => !m.interaction),
|
|
12512
|
+
{ claimant: NOT_A_CLAIMANT }
|
|
12513
|
+
);
|
|
12514
|
+
for (const line of lines) this.log({ type: "message", ...capped(line) });
|
|
12515
|
+
}
|
|
12516
|
+
onAutopilot() {
|
|
12517
|
+
if (this.disposed || !this.autopilotHost) return;
|
|
12518
|
+
const signature = this.autopilotSignature();
|
|
12519
|
+
if (signature === this.lastAutopilot) return;
|
|
12520
|
+
this.lastAutopilot = signature;
|
|
12521
|
+
const { state } = this.autopilotHost;
|
|
12522
|
+
this.log({
|
|
12523
|
+
type: "autopilot",
|
|
12524
|
+
mode: state.mode,
|
|
12525
|
+
status: state.status,
|
|
12526
|
+
needsUser: state.needsUser,
|
|
12527
|
+
working: state.working,
|
|
12528
|
+
...state.pause?.reason ? { reason: state.pause.reason } : {}
|
|
12529
|
+
});
|
|
12530
|
+
}
|
|
12531
|
+
autopilotSignature() {
|
|
12532
|
+
const s = this.autopilotHost?.state;
|
|
12533
|
+
return s ? JSON.stringify([s.mode, s.status, s.needsUser, s.working]) : "";
|
|
12534
|
+
}
|
|
12535
|
+
// ---------------------------------------------------------------------------------------------
|
|
12536
|
+
// Reporting an interaction
|
|
12537
|
+
// ---------------------------------------------------------------------------------------------
|
|
12538
|
+
describe(t) {
|
|
12539
|
+
return describeInteraction({ ctx: t.ctx, componentName: t.componentName, data: t.data });
|
|
12540
|
+
}
|
|
12541
|
+
/**
|
|
12542
|
+
* Report where an interaction stands, when that has changed. A `decision` when the person holds
|
|
12543
|
+
* it and can act; `working` when a claimant holds it or it is not ready yet. A `decision` is
|
|
12544
|
+
* logged again only when its description changed materially: who holds it, whether it is ready
|
|
12545
|
+
* or has a controller, or which actions are enabled. The text and facts alone do not count.
|
|
12546
|
+
*/
|
|
12547
|
+
evaluate(t) {
|
|
12548
|
+
if (this.disposed || !t.armed || t.settled) return;
|
|
12549
|
+
if (this.autoSettle(t)) return;
|
|
12550
|
+
let d;
|
|
12551
|
+
try {
|
|
12552
|
+
d = this.describe(t);
|
|
12553
|
+
} catch (e) {
|
|
12554
|
+
this.log({ type: "error", where: "describe", message: messageOf(e) });
|
|
12555
|
+
return;
|
|
12556
|
+
}
|
|
12557
|
+
if (d.heldBy === "user" && d.ready) {
|
|
12558
|
+
const sig2 = JSON.stringify([d.controller, d.actions.map((a) => [a.name, a.enabled])]);
|
|
12559
|
+
if (t.last?.kind === "decision" && t.last.sig === sig2) return;
|
|
12560
|
+
t.last = { kind: "decision", sig: sig2 };
|
|
12561
|
+
this.log({
|
|
12562
|
+
type: "decision",
|
|
12563
|
+
...d,
|
|
12564
|
+
...t.ownerPath.length ? { ownerPath: t.ownerPath } : {}
|
|
12565
|
+
});
|
|
12566
|
+
return;
|
|
12567
|
+
}
|
|
12568
|
+
const sig = `${d.heldBy}|${d.ready}`;
|
|
12569
|
+
if (t.last?.kind === "working" && t.last.sig === sig) return;
|
|
12570
|
+
t.last = { kind: "working", sig };
|
|
12571
|
+
this.log({
|
|
12572
|
+
type: "working",
|
|
12573
|
+
interactionId: t.id,
|
|
12574
|
+
component: t.componentName,
|
|
12575
|
+
heldBy: d.heldBy
|
|
12576
|
+
});
|
|
12577
|
+
}
|
|
12578
|
+
/**
|
|
12579
|
+
* Settle a widget that settles itself in a browser, as it would on mounting. Once, and only when
|
|
12580
|
+
* the person holds it. Returns whether it settled it.
|
|
12581
|
+
*/
|
|
12582
|
+
autoSettle(t) {
|
|
12583
|
+
const result = this.options.autoSettle?.[t.componentName];
|
|
12584
|
+
if (!result || t.autoTried || t.ctx.heldBy !== "user") return false;
|
|
12585
|
+
t.autoTried = true;
|
|
12586
|
+
try {
|
|
12587
|
+
const value = result(t.data);
|
|
12588
|
+
this.origin.set(t.id, { auto: true });
|
|
12589
|
+
this.driver.resolveInteraction(t.id, value);
|
|
12590
|
+
return true;
|
|
12591
|
+
} catch (e) {
|
|
12592
|
+
this.origin.delete(t.id);
|
|
12593
|
+
this.log({
|
|
12594
|
+
type: "error",
|
|
12595
|
+
where: "auto-settle",
|
|
12596
|
+
message: `${t.componentName}: ${messageOf(e)}`
|
|
12597
|
+
});
|
|
12598
|
+
return false;
|
|
12599
|
+
}
|
|
12600
|
+
}
|
|
12601
|
+
async readTool(t, name, args) {
|
|
12602
|
+
const sets = (this.options.widgetTools ?? []).filter(
|
|
12603
|
+
(s) => s.componentName === t.componentName
|
|
12604
|
+
);
|
|
12605
|
+
const names = sets.flatMap((s) => Object.keys(s.tools)).sort();
|
|
12606
|
+
const spec = sets.map((s) => Object.prototype.hasOwnProperty.call(s.tools, name) ? s.tools[name] : void 0).find((s) => s !== void 0);
|
|
12607
|
+
if (!spec) {
|
|
12608
|
+
return {
|
|
12609
|
+
ok: false,
|
|
12610
|
+
error: names.length ? `No tool "${name}" for the "${t.componentName}" widget. Tools: ${names.join(", ")}.` : `The "${t.componentName}" widget has no tools.`
|
|
12611
|
+
};
|
|
12612
|
+
}
|
|
12613
|
+
const { controller } = t.ctx;
|
|
12614
|
+
if (controller === void 0) {
|
|
12615
|
+
return { ok: false, error: `The "${t.componentName}" widget has nothing to read from.` };
|
|
12616
|
+
}
|
|
12617
|
+
const why = unavailableReason(spec.unavailable, controller);
|
|
12618
|
+
if (why !== void 0) return { ok: false, error: `Not available right now: ${name}: ${why}.` };
|
|
12619
|
+
const problem = checkArgs(args ?? {}, spec.definition.parameters);
|
|
12620
|
+
if (problem) return { ok: false, error: `Not done: ${name}: ${problem}` };
|
|
12621
|
+
try {
|
|
12622
|
+
const output = await spec.run(controller, args ?? {});
|
|
12623
|
+
return {
|
|
12624
|
+
ok: true,
|
|
12625
|
+
content: capRead(typeof output === "string" ? output : JSON.stringify(output) ?? "null")
|
|
12626
|
+
};
|
|
12627
|
+
} catch (e) {
|
|
12628
|
+
return { ok: false, error: `Could not read: ${messageOf(e)}` };
|
|
12629
|
+
}
|
|
12630
|
+
}
|
|
12631
|
+
// ---------------------------------------------------------------------------------------------
|
|
12632
|
+
// The log
|
|
12633
|
+
// ---------------------------------------------------------------------------------------------
|
|
12634
|
+
/**
|
|
12635
|
+
* Notice a turn begin or end, whoever started it. The driver being busy (or a `send` in flight)
|
|
12636
|
+
* is a turn; once it has been idle for a beat the turn is over and is reported, once. The beat
|
|
12637
|
+
* covers the gaps inside one turn, such as a hand-off continuing it, which are a microtask long.
|
|
12638
|
+
* A turn that begins the instant another ends, with no idle between, is reported as one.
|
|
12639
|
+
*/
|
|
12640
|
+
observe() {
|
|
12641
|
+
if (this.disposed) return;
|
|
12642
|
+
if (this.turn || this.driver.isBusy()) {
|
|
12643
|
+
clearTimeout(this.quiesceTimer);
|
|
12644
|
+
this.quiesceTimer = void 0;
|
|
12645
|
+
if (!this.open) {
|
|
12646
|
+
this.open = { start: this.base };
|
|
12647
|
+
this.startPolling();
|
|
12648
|
+
}
|
|
12649
|
+
return;
|
|
12650
|
+
}
|
|
12651
|
+
if (this.open && this.quiesceTimer === void 0) {
|
|
12652
|
+
this.quiesceTimer = setTimeout(() => {
|
|
12653
|
+
this.quiesceTimer = void 0;
|
|
12654
|
+
this.closeTurn();
|
|
12655
|
+
}, 0);
|
|
12656
|
+
}
|
|
12657
|
+
}
|
|
12658
|
+
/** Report the open turn as ended, unless the driver is busy again. */
|
|
12659
|
+
closeTurn() {
|
|
12660
|
+
const { open } = this;
|
|
12661
|
+
if (this.disposed || !open || this.turn || this.driver.isBusy()) return;
|
|
12662
|
+
clearTimeout(this.quiesceTimer);
|
|
12663
|
+
this.quiesceTimer = void 0;
|
|
12664
|
+
this.open = void 0;
|
|
12665
|
+
this.stopPolling();
|
|
12666
|
+
this.logTurn(open.reason ?? "done", open.failureReason, open.start);
|
|
12667
|
+
this.base = this.history().length;
|
|
12668
|
+
}
|
|
12669
|
+
/** Without a shared bus nothing says a turn ended, so while one is open the driver is asked. */
|
|
12670
|
+
startPolling() {
|
|
12671
|
+
if (this.options.activityBus || this.poller !== void 0) return;
|
|
12672
|
+
this.poller = setInterval(() => this.observe(), TURN_POLL_MS);
|
|
12673
|
+
this.poller.unref?.();
|
|
12674
|
+
}
|
|
12675
|
+
stopPolling() {
|
|
12676
|
+
if (this.poller !== void 0) clearInterval(this.poller);
|
|
12677
|
+
this.poller = void 0;
|
|
12678
|
+
}
|
|
12679
|
+
logTurn(reason, failureReason, start) {
|
|
12680
|
+
const text = finalText(this.history().slice(start));
|
|
12681
|
+
this.log({
|
|
12682
|
+
type: "turn",
|
|
12683
|
+
reason,
|
|
12684
|
+
...failureReason ? { failureReason } : {},
|
|
12685
|
+
...text !== void 0 ? capped(text) : {}
|
|
12686
|
+
});
|
|
12687
|
+
}
|
|
12688
|
+
log(event) {
|
|
12689
|
+
this.append(event);
|
|
12690
|
+
this.checkProbe();
|
|
12691
|
+
}
|
|
12692
|
+
append(event) {
|
|
12693
|
+
this.seq += 1;
|
|
12694
|
+
const entry = {
|
|
12695
|
+
...event,
|
|
12696
|
+
seq: this.seq,
|
|
12697
|
+
at: new Date(this.clock()).toISOString()
|
|
12698
|
+
};
|
|
12699
|
+
this.events.push(entry);
|
|
12700
|
+
if (this.events.length > MAX_EVENTS) this.events.splice(0, this.events.length - MAX_EVENTS);
|
|
12701
|
+
for (const waiter of this.waiters) {
|
|
12702
|
+
if (wanted(entry, waiter.until)) waiter.finish("ready");
|
|
12703
|
+
}
|
|
12704
|
+
}
|
|
12705
|
+
readProbe() {
|
|
12706
|
+
try {
|
|
12707
|
+
const read = this.options.probe?.();
|
|
12708
|
+
return read ? { ...read } : {};
|
|
12709
|
+
} catch (e) {
|
|
12710
|
+
logger2.warn("HeadlessSession: the probe threw:", e);
|
|
12711
|
+
return {};
|
|
12712
|
+
}
|
|
12713
|
+
}
|
|
12714
|
+
/** Read the probe again; a change in any top-level value is a `probe` event. */
|
|
12715
|
+
checkProbe() {
|
|
12716
|
+
if (!this.options.probe || this.disposed) return;
|
|
12717
|
+
const now = this.readProbe();
|
|
12718
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(this.lastProbe), ...Object.keys(now)]);
|
|
12719
|
+
const changed = [...keys].filter(
|
|
12720
|
+
(k) => JSON.stringify(this.lastProbe[k]) !== JSON.stringify(now[k])
|
|
12721
|
+
);
|
|
12722
|
+
this.lastProbe = now;
|
|
12723
|
+
if (changed.length) this.append({ type: "probe", probe: now, changed });
|
|
12724
|
+
}
|
|
12725
|
+
waited(since, status) {
|
|
12726
|
+
return { events: this.eventsSince(since), status, nextSeq: this.seq };
|
|
12727
|
+
}
|
|
12728
|
+
};
|
|
12729
|
+
function wanted(event, until) {
|
|
12730
|
+
if (until) return until.includes(event.type);
|
|
12731
|
+
return event.type === "decision" || event.type === "turn" || event.type === "error" || event.type === "autopilot" && event.needsUser;
|
|
11453
12732
|
}
|
|
11454
|
-
function
|
|
11455
|
-
|
|
12733
|
+
function finalText(messages) {
|
|
12734
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
12735
|
+
const m = messages[i];
|
|
12736
|
+
if (m.role === "assistant" && !m.interaction && !m.toolCalls?.length && !m.thinking && m.category !== "reasoning" && m.category !== "narration" && m.content?.trim()) {
|
|
12737
|
+
return m.content;
|
|
12738
|
+
}
|
|
12739
|
+
}
|
|
12740
|
+
return void 0;
|
|
12741
|
+
}
|
|
12742
|
+
|
|
12743
|
+
// src/utils/usage-rows.ts
|
|
12744
|
+
function hasUsage(m) {
|
|
12745
|
+
return m.cost != null || m.externalCostUsd != null || m.inputTokens != null || m.outputTokens != null || m.cacheReadTokens != null || m.cacheWriteTokens != null;
|
|
12746
|
+
}
|
|
12747
|
+
function usageRows(messages) {
|
|
12748
|
+
return collectRows(messages, 0, void 0);
|
|
12749
|
+
}
|
|
12750
|
+
function bucketsOf(usage) {
|
|
12751
|
+
return {
|
|
12752
|
+
uncachedInputTokens: usage.uncachedInputTokens,
|
|
12753
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
12754
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
12755
|
+
outputTokens: usage.outputTokens
|
|
12756
|
+
};
|
|
12757
|
+
}
|
|
12758
|
+
function collectRows(messages, depth, subAgentOf) {
|
|
12759
|
+
const rows = [];
|
|
12760
|
+
for (const m of messages) {
|
|
12761
|
+
if (hasUsage(m)) {
|
|
12762
|
+
const cacheReadTokens = m.cacheReadTokens ?? 0;
|
|
12763
|
+
const cacheWriteTokens = m.cacheWriteTokens ?? 0;
|
|
12764
|
+
rows.push({
|
|
12765
|
+
source: "request",
|
|
12766
|
+
model: m.model,
|
|
12767
|
+
// The driver stamps `ChatMessage.provider` from the resolved provider's own
|
|
12768
|
+
// status, so it is AUTHORITATIVE and covers vendors the model allowlists do
|
|
12769
|
+
// not — a server-proxied `gpt-5` turn is `provider: 'openai'` on the message
|
|
12770
|
+
// and unknown to `vendorOfModel`. Prefer it; fall back to resolving the model
|
|
12771
|
+
// id only for messages persisted before the field existed. Still left
|
|
12772
|
+
// undefined when neither knows, rather than guessed at.
|
|
12773
|
+
provider: m.provider ?? (m.model ? vendorOfModel(m.model) : void 0),
|
|
12774
|
+
...m.cost != null && { costUsd: m.cost },
|
|
12775
|
+
...m.externalCostUsd != null && { externalCostUsd: m.externalCostUsd },
|
|
12776
|
+
// Uncached input is the REMAINDER, matching `sumUsage`: `inputTokens` is the
|
|
12777
|
+
// whole prompt and the cache fields break it down. Clamped for the same
|
|
12778
|
+
// reason — hand-edited history must not drive a total negative.
|
|
12779
|
+
uncachedInputTokens: Math.max(0, (m.inputTokens ?? 0) - cacheReadTokens - cacheWriteTokens),
|
|
12780
|
+
cacheReadTokens,
|
|
12781
|
+
cacheWriteTokens,
|
|
12782
|
+
outputTokens: m.outputTokens ?? 0,
|
|
12783
|
+
...m.agentName != null && { agentName: m.agentName },
|
|
12784
|
+
subAgentDepth: depth,
|
|
12785
|
+
...subAgentOf != null && { subAgentOf }
|
|
12786
|
+
});
|
|
12787
|
+
}
|
|
12788
|
+
const rolled = m.compaction?.rolledUpUsage;
|
|
12789
|
+
if (rolled) {
|
|
12790
|
+
rows.push({
|
|
12791
|
+
source: "compaction",
|
|
12792
|
+
...rolled.costUsd != null && { costUsd: rolled.costUsd },
|
|
12793
|
+
...bucketsOf(rolled),
|
|
12794
|
+
...m.agentName != null && { agentName: m.agentName },
|
|
12795
|
+
subAgentDepth: depth,
|
|
12796
|
+
...subAgentOf != null && { subAgentOf }
|
|
12797
|
+
});
|
|
12798
|
+
}
|
|
12799
|
+
for (const tc of m.toolCalls ?? []) {
|
|
12800
|
+
if (tc.subAgentTrace) rows.push(...collectRows(tc.subAgentTrace, depth + 1, tc.id));
|
|
12801
|
+
}
|
|
12802
|
+
}
|
|
12803
|
+
return rows;
|
|
11456
12804
|
}
|
|
11457
12805
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11458
12806
|
0 && (module.exports = {
|
|
@@ -11476,6 +12824,7 @@ function withFreshMetaSnapshot(stored, fresh) {
|
|
|
11476
12824
|
GEMINI_LONG_CONTEXT_THRESHOLD,
|
|
11477
12825
|
GeminiProvider,
|
|
11478
12826
|
GeminiTransport,
|
|
12827
|
+
HeadlessSession,
|
|
11479
12828
|
MALFORMED_CALL_NUDGE,
|
|
11480
12829
|
MutableAIProviderRegistry,
|
|
11481
12830
|
NOOP_ACTIVITY_BUS,
|
|
@@ -11493,18 +12842,24 @@ function withFreshMetaSnapshot(stored, fresh) {
|
|
|
11493
12842
|
anthropicRatesFor,
|
|
11494
12843
|
anthropicTokenCost,
|
|
11495
12844
|
assembleDebugLog,
|
|
12845
|
+
buildDiagnosticEntries,
|
|
11496
12846
|
buildTimelineEntries,
|
|
11497
12847
|
clearSession,
|
|
11498
12848
|
defineAgent,
|
|
11499
12849
|
defineAutopilot,
|
|
11500
12850
|
defineStatefulAgent,
|
|
12851
|
+
describeInteraction,
|
|
11501
12852
|
emptyUsage,
|
|
11502
12853
|
friendlyFallbackAgent,
|
|
11503
12854
|
geminiRatesFor,
|
|
11504
12855
|
geminiTokenCost,
|
|
11505
12856
|
getMetaEvents,
|
|
12857
|
+
headlessCurrentDebugLog,
|
|
12858
|
+
headlessDiagnosticEntries,
|
|
12859
|
+
headlessMetaSnapshot,
|
|
11506
12860
|
isObservableAIProviderRegistry,
|
|
11507
12861
|
isTerminalAIServiceRefusal,
|
|
12862
|
+
lifetimeDebugLog,
|
|
11508
12863
|
messageUsage,
|
|
11509
12864
|
resolveAITiers,
|
|
11510
12865
|
restoreMachine,
|