@mitralab.io/platform-sdk 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/index.cjs +504 -14
- package/dist/index.js +504 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -246,7 +246,9 @@ unsubscribe()
|
|
|
246
246
|
session.close()
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
-
Open an existing task with `session({ taskId })`. The default `auto` transport refreshes before connecting, opens `/copilot/ws/tasks/{taskId}`, and
|
|
249
|
+
Open an existing task with `session({ taskId })`. The default `auto` transport refreshes before connecting, asks the Copilot where the chat is served, and opens whichever channel it names: the box that runs the agent when one is offered, `/copilot/ws/tasks/{taskId}` otherwise. The choice belongs to the server, never to application configuration, so a Copilot that stops offering the box leaves every application on the Copilot socket without a republish. Opening the box never asks it to replay, whether the conversation is new to the session or is being opened again after an idle close: what an idle conversation missed is history, which the application loads over REST. A box socket that drops in the middle of a turn is redialed by the SDK itself, with a bounded backoff (1, 2, 4, 8 and 16 seconds) and the replay from the last position seen on that socket, so the answer keeps streaming; a position taken on one box is forgotten when the Copilot points the conversation to another box; replayed `textChunk` frames reach the `delta` event like live text. While that happens the session emits `raw` events of type `channelReconnecting` (payload `attempt`, `maxAttempts`, `reason`) and `channelConnected` (payload `attempt`); the session `status` stays `streaming`, because Core has no reconnecting status. The disconnect reaches Core, and with it the `error` event, only when the attempts run out or the Copilot no longer offers the box. A drop with no turn in flight, a channel taken over by another tab (close code 4409), and any close of the Copilot socket the session did not ask for are reported at once, whatever the code, so the next send reopens the channel instead of waiting on a socket that is gone. Recovery through persisted history plus the HTTP/SSE channel is unchanged. Set `transport: "http"` when WebSockets are unavailable. Messages sent during a turn enter a FIFO queue with a maximum of 10 items; the session also exposes edit, remove, clear, approval, cancel, history, close, and typed events.
|
|
250
|
+
|
|
251
|
+
A prompt sent while the browser says it is offline (`navigator.onLine === false`) does not enter Core at all, because Core opens the channel and reads the turn baseline before the prompt goes out and every one of those requests would fail first: the session keeps the prompt in an outbox, emits a `raw` event of type `inputUnsent` (payload `attempt` 0, `reason`, `waitingForOnline` true) and hands it to Core, in order, when the browser fires `online`; the turn starts then. A prompt whose `POST /inputs` went out but got no response at all is kept the same way: the session sends the same request again when the browser fires `online` or on a bounded backoff (2, 5, 10, 20 and 40 seconds), while the turn stays `streaming`, with `inputUnsent` events (payload `attempt`, `reason`, `waitingForOnline`, `retryInMs` when a timer is armed) and, once it goes through, `inputSent` (payload `attempts`). A response from the server, an error included, is never retried: the prompt fails through the `error` event as before. A session closed with a prompt still waiting emits `error` with code `INPUT_UNSENT` before it goes quiet, and `sendAndWait` rejects. The retry cannot tell a request the server never received from one whose response was lost on the way back, so a prompt may reach the server twice in that case; the Copilot's `/inputs` accepts no client message id yet. The public `agentTasks.sendInput` primitive is not held back: it fails immediately, as it always did.
|
|
250
252
|
|
|
251
253
|
API keys and removal accept `ANTHROPIC` or `OPENAI`. OAuth accepts only `ANTHROPIC`; device authorization accepts only `OPENAI`. The facade enforces those producer-supported pairs in TypeScript and at runtime.
|
|
252
254
|
|
package/dist/index.cjs
CHANGED
|
@@ -1460,8 +1460,230 @@ var QueriesModule = class {
|
|
|
1460
1460
|
// src/modules/agent-tasks.ts
|
|
1461
1461
|
var import_sdk_core8 = require("@mitralab.io/sdk-core");
|
|
1462
1462
|
|
|
1463
|
+
// src/modules/agent-outbox.ts
|
|
1464
|
+
var OUTBOX_DELAYS_MS = [2e3, 5e3, 1e4, 2e4, 4e4];
|
|
1465
|
+
var browserNetwork = {
|
|
1466
|
+
isOffline: () => globalThis.navigator?.onLine === false,
|
|
1467
|
+
onOnline: (listener) => {
|
|
1468
|
+
const target = globalThis;
|
|
1469
|
+
if (typeof target.addEventListener !== "function") return () => void 0;
|
|
1470
|
+
target.addEventListener("online", listener);
|
|
1471
|
+
return () => target.removeEventListener?.("online", listener);
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
function sendGotNoResponse(error) {
|
|
1475
|
+
return error instanceof TypeError;
|
|
1476
|
+
}
|
|
1477
|
+
function reasonOf(error) {
|
|
1478
|
+
return error instanceof Error ? error.message : String(error);
|
|
1479
|
+
}
|
|
1480
|
+
function neverSent(pending) {
|
|
1481
|
+
return new Error(
|
|
1482
|
+
`Agent prompt was never sent: the session closed while it waited for the network (${pending} pending).`
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
var AgentInputOutbox = class {
|
|
1486
|
+
constructor(tasks, announce, network = browserNetwork) {
|
|
1487
|
+
this.tasks = tasks;
|
|
1488
|
+
this.announce = announce;
|
|
1489
|
+
this.network = network;
|
|
1490
|
+
}
|
|
1491
|
+
held = /* @__PURE__ */ new Map();
|
|
1492
|
+
gated = /* @__PURE__ */ new Map();
|
|
1493
|
+
async sendInput(taskId, input) {
|
|
1494
|
+
try {
|
|
1495
|
+
await this.tasks.sendInput(taskId, input);
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
if (input.type !== "message" || !sendGotNoResponse(error)) throw error;
|
|
1498
|
+
await this.hold(taskId, input, error);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
/** The session is closing: a prompt still waiting is reported, never dropped in silence. */
|
|
1502
|
+
abandon(taskId) {
|
|
1503
|
+
const pending = this.held.get(taskId);
|
|
1504
|
+
if (!pending?.length) return;
|
|
1505
|
+
this.held.delete(taskId);
|
|
1506
|
+
this.drop(taskId, pending);
|
|
1507
|
+
}
|
|
1508
|
+
/** True when the browser says there is no network at all. */
|
|
1509
|
+
get offline() {
|
|
1510
|
+
return this.network.isOffline();
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Keeps a prompt out of the core until the browser reports the network is back, then hands it
|
|
1514
|
+
* over through `deliver`. Prompts held for one session leave in the order they arrived. The
|
|
1515
|
+
* promise settles when the prompt is handed over, or rejects if the session closes first.
|
|
1516
|
+
*/
|
|
1517
|
+
holdUntilOnline(session, taskId, deliver) {
|
|
1518
|
+
return new Promise((resolve, reject) => {
|
|
1519
|
+
const prompt = { resolve, reject, disarm: () => void 0 };
|
|
1520
|
+
this.gated.set(session, [...this.gated.get(session) ?? [], prompt]);
|
|
1521
|
+
if (taskId) {
|
|
1522
|
+
this.announce(taskId, {
|
|
1523
|
+
type: "inputUnsent",
|
|
1524
|
+
payload: { attempt: 0, reason: "The browser is offline.", waitingForOnline: true },
|
|
1525
|
+
timestamp: Date.now()
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
const offOnline = this.network.onOnline(() => {
|
|
1529
|
+
prompt.disarm();
|
|
1530
|
+
const rest = (this.gated.get(session) ?? []).filter((candidate) => candidate !== prompt);
|
|
1531
|
+
if (rest.length) this.gated.set(session, rest);
|
|
1532
|
+
else this.gated.delete(session);
|
|
1533
|
+
deliver();
|
|
1534
|
+
resolve();
|
|
1535
|
+
});
|
|
1536
|
+
prompt.disarm = () => {
|
|
1537
|
+
offOnline();
|
|
1538
|
+
prompt.disarm = () => void 0;
|
|
1539
|
+
};
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
/** The session is closing with prompts that never entered the core: same report as `abandon`. */
|
|
1543
|
+
abandonGated(session, taskId) {
|
|
1544
|
+
const pending = this.gated.get(session);
|
|
1545
|
+
if (!pending?.length) return;
|
|
1546
|
+
this.gated.delete(session);
|
|
1547
|
+
this.drop(taskId, pending);
|
|
1548
|
+
}
|
|
1549
|
+
drop(taskId, pending) {
|
|
1550
|
+
const error = neverSent(pending.length);
|
|
1551
|
+
if (taskId) {
|
|
1552
|
+
this.announce(taskId, {
|
|
1553
|
+
type: "error",
|
|
1554
|
+
payload: { code: "INPUT_UNSENT", message: error.message },
|
|
1555
|
+
timestamp: Date.now()
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
for (const prompt of pending) {
|
|
1559
|
+
prompt.disarm();
|
|
1560
|
+
prompt.reject(error);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
hold(taskId, input, cause) {
|
|
1564
|
+
return new Promise((resolve, reject) => {
|
|
1565
|
+
const prompt = { input, attempts: 1, resolve, reject, disarm: () => void 0 };
|
|
1566
|
+
this.held.set(taskId, [...this.held.get(taskId) ?? [], prompt]);
|
|
1567
|
+
this.schedule(taskId, prompt, cause);
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
schedule(taskId, prompt, cause) {
|
|
1571
|
+
const waitingForOnline = this.network.isOffline();
|
|
1572
|
+
const retryInMs = waitingForOnline ? void 0 : OUTBOX_DELAYS_MS[prompt.attempts - 1];
|
|
1573
|
+
if (!waitingForOnline && retryInMs === void 0) {
|
|
1574
|
+
this.release(taskId, prompt);
|
|
1575
|
+
prompt.reject(new Error(
|
|
1576
|
+
`Agent prompt was not sent after ${prompt.attempts} attempts: ${reasonOf(cause)}`
|
|
1577
|
+
));
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
this.announce(taskId, {
|
|
1581
|
+
type: "inputUnsent",
|
|
1582
|
+
payload: {
|
|
1583
|
+
attempt: prompt.attempts,
|
|
1584
|
+
reason: reasonOf(cause),
|
|
1585
|
+
waitingForOnline,
|
|
1586
|
+
...retryInMs === void 0 ? {} : { retryInMs }
|
|
1587
|
+
},
|
|
1588
|
+
timestamp: Date.now()
|
|
1589
|
+
});
|
|
1590
|
+
const retry = () => {
|
|
1591
|
+
prompt.disarm();
|
|
1592
|
+
void this.retry(taskId, prompt);
|
|
1593
|
+
};
|
|
1594
|
+
const timer = retryInMs === void 0 ? null : globalThis.setTimeout(retry, retryInMs);
|
|
1595
|
+
const offOnline = this.network.onOnline(retry);
|
|
1596
|
+
prompt.disarm = () => {
|
|
1597
|
+
if (timer !== null) globalThis.clearTimeout(timer);
|
|
1598
|
+
offOnline();
|
|
1599
|
+
prompt.disarm = () => void 0;
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
async retry(taskId, prompt) {
|
|
1603
|
+
prompt.attempts += 1;
|
|
1604
|
+
try {
|
|
1605
|
+
await this.tasks.sendInput(taskId, prompt.input);
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
if (!this.isHeld(taskId, prompt)) return;
|
|
1608
|
+
if (!sendGotNoResponse(error)) {
|
|
1609
|
+
this.release(taskId, prompt);
|
|
1610
|
+
prompt.reject(error instanceof Error ? error : new Error(String(error)));
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
this.schedule(taskId, prompt, error);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
if (!this.isHeld(taskId, prompt)) return;
|
|
1617
|
+
this.release(taskId, prompt);
|
|
1618
|
+
this.announce(taskId, {
|
|
1619
|
+
type: "inputSent",
|
|
1620
|
+
payload: { attempts: prompt.attempts },
|
|
1621
|
+
timestamp: Date.now()
|
|
1622
|
+
});
|
|
1623
|
+
prompt.resolve();
|
|
1624
|
+
}
|
|
1625
|
+
isHeld(taskId, prompt) {
|
|
1626
|
+
return this.held.get(taskId)?.includes(prompt) ?? false;
|
|
1627
|
+
}
|
|
1628
|
+
release(taskId, prompt) {
|
|
1629
|
+
const rest = (this.held.get(taskId) ?? []).filter((candidate) => candidate !== prompt);
|
|
1630
|
+
if (rest.length) this.held.set(taskId, rest);
|
|
1631
|
+
else this.held.delete(taskId);
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
function holdSendsWhileOffline(session, outbox) {
|
|
1635
|
+
const passThrough = (prompt) => !outbox.offline || !prompt.trim() || session.status === "closed";
|
|
1636
|
+
const gated = {
|
|
1637
|
+
send: (prompt, options) => {
|
|
1638
|
+
if (passThrough(prompt)) {
|
|
1639
|
+
session.send(prompt, options);
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
void outbox.holdUntilOnline(session, session.taskId, () => session.send(prompt, options)).catch(() => void 0);
|
|
1643
|
+
},
|
|
1644
|
+
sendAndWait: (prompt, options) => {
|
|
1645
|
+
if (passThrough(prompt)) return session.sendAndWait(prompt, options);
|
|
1646
|
+
let turn;
|
|
1647
|
+
return outbox.holdUntilOnline(session, session.taskId, () => {
|
|
1648
|
+
turn = session.sendAndWait(prompt, options);
|
|
1649
|
+
}).then(() => turn);
|
|
1650
|
+
},
|
|
1651
|
+
close: () => {
|
|
1652
|
+
outbox.abandonGated(session, session.taskId);
|
|
1653
|
+
session.close();
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
return new Proxy(session, {
|
|
1657
|
+
get: (target, key) => key in gated ? gated[key] : Reflect.get(target, key, target)
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1463
1661
|
// src/modules/agent-session.ts
|
|
1464
1662
|
var CONNECT_TIMEOUT_MS = 15e3;
|
|
1663
|
+
var CHANNEL_BOOT_TIMEOUT_MS = 9e4;
|
|
1664
|
+
var CHANNEL_BOOT_RETRY_MS = 2e3;
|
|
1665
|
+
var SILENCE_TIMEOUT_MS = 6e4;
|
|
1666
|
+
var RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 16e3];
|
|
1667
|
+
var SUPERSEDED_CLOSE_CODE = 4409;
|
|
1668
|
+
var SilenceWatchdog = class {
|
|
1669
|
+
constructor(onSilence) {
|
|
1670
|
+
this.onSilence = onSilence;
|
|
1671
|
+
}
|
|
1672
|
+
timer = null;
|
|
1673
|
+
touch() {
|
|
1674
|
+
this.clear();
|
|
1675
|
+
this.timer = globalThis.setTimeout(this.onSilence, SILENCE_TIMEOUT_MS);
|
|
1676
|
+
}
|
|
1677
|
+
clear() {
|
|
1678
|
+
if (this.timer !== null) {
|
|
1679
|
+
globalThis.clearTimeout(this.timer);
|
|
1680
|
+
this.timer = null;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
function silenceError(transport) {
|
|
1685
|
+
return new Error(`Agent ${transport} went silent for ${SILENCE_TIMEOUT_MS / 1e3}s.`);
|
|
1686
|
+
}
|
|
1465
1687
|
function stripBearer2(token) {
|
|
1466
1688
|
return token.replace(/^Bearer\s+/i, "");
|
|
1467
1689
|
}
|
|
@@ -1480,6 +1702,37 @@ function expectEvent(value) {
|
|
|
1480
1702
|
...typeof event.sequence === "number" ? { sequence: event.sequence } : {}
|
|
1481
1703
|
};
|
|
1482
1704
|
}
|
|
1705
|
+
function isSameGateway(candidate, apiUrl) {
|
|
1706
|
+
try {
|
|
1707
|
+
const target = new URL(candidate);
|
|
1708
|
+
if (target.protocol !== "ws:" && target.protocol !== "wss:") return false;
|
|
1709
|
+
return target.host === new URL(apiUrl).host;
|
|
1710
|
+
} catch {
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
function boxAddress(wsUrl) {
|
|
1715
|
+
const url = new URL(wsUrl);
|
|
1716
|
+
return `${url.origin}${url.pathname}`;
|
|
1717
|
+
}
|
|
1718
|
+
function toDirectChannel(body, apiUrl) {
|
|
1719
|
+
if (typeof body !== "object" || body === null) return null;
|
|
1720
|
+
const channel = body;
|
|
1721
|
+
if (typeof channel.wsUrl !== "string" || !isSameGateway(channel.wsUrl, apiUrl)) return null;
|
|
1722
|
+
return {
|
|
1723
|
+
wsUrl: channel.wsUrl,
|
|
1724
|
+
lastSequence: typeof channel.lastSequence === "number" && channel.lastSequence > 0 ? channel.lastSequence : 0
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
function sleep(ms, signal) {
|
|
1728
|
+
return new Promise((resolve) => {
|
|
1729
|
+
const timer = globalThis.setTimeout(resolve, ms);
|
|
1730
|
+
signal?.addEventListener("abort", () => {
|
|
1731
|
+
globalThis.clearTimeout(timer);
|
|
1732
|
+
resolve();
|
|
1733
|
+
}, { once: true });
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1483
1736
|
function parseEvent(raw) {
|
|
1484
1737
|
if (typeof raw !== "string") return expectEvent(raw);
|
|
1485
1738
|
try {
|
|
@@ -1488,8 +1741,25 @@ function parseEvent(raw) {
|
|
|
1488
1741
|
return null;
|
|
1489
1742
|
}
|
|
1490
1743
|
}
|
|
1491
|
-
function
|
|
1492
|
-
|
|
1744
|
+
function asDelta(event) {
|
|
1745
|
+
if (event.type !== "textChunk") return event;
|
|
1746
|
+
const kind = asObject(event.payload)?.kind;
|
|
1747
|
+
return { ...event, type: kind === "thinking" ? "thinking" : "textDelta" };
|
|
1748
|
+
}
|
|
1749
|
+
var TURN_FRAME_TYPES = /* @__PURE__ */ new Set(["textDelta", "thinking", "toolCall", "toolResult"]);
|
|
1750
|
+
var TURN_END_REASONS = /* @__PURE__ */ new Set(["stop", "endTurn", "interrupted"]);
|
|
1751
|
+
function turnAfter(event, inTurn) {
|
|
1752
|
+
if (TURN_FRAME_TYPES.has(event.type)) return true;
|
|
1753
|
+
if (event.type === "error") return false;
|
|
1754
|
+
if (event.type === "stepFinish") {
|
|
1755
|
+
const payload = asObject(event.payload);
|
|
1756
|
+
if (TURN_END_REASONS.has(payload?.reason)) return false;
|
|
1757
|
+
return asObject(payload?.lifecycle)?.interruptTerminal !== true;
|
|
1758
|
+
}
|
|
1759
|
+
return inTurn;
|
|
1760
|
+
}
|
|
1761
|
+
function channelEvent(type, payload) {
|
|
1762
|
+
return { type, payload, timestamp: Date.now() };
|
|
1493
1763
|
}
|
|
1494
1764
|
var BrowserAgentTaskEventSource = class {
|
|
1495
1765
|
constructor(auth, apiUrl) {
|
|
@@ -1498,6 +1768,18 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1498
1768
|
}
|
|
1499
1769
|
apiUrl;
|
|
1500
1770
|
sseFallbackTasks = /* @__PURE__ */ new Set();
|
|
1771
|
+
// Tasks whose last WebSocket was the box itself. A box socket that closes says nothing about
|
|
1772
|
+
// WebSockets: the box went idle or the channel was superseded, and the answer is to ask the
|
|
1773
|
+
// copilot for the channel again. Only the copilot's own socket failing sends a task to SSE.
|
|
1774
|
+
directTasks = /* @__PURE__ */ new Set();
|
|
1775
|
+
/**
|
|
1776
|
+
* Ate onde esta sessao ja viu o log da caixa, por conversa, e de qual caixa esse log e. So o
|
|
1777
|
+
* redial no meio de um turno pede repeticao a partir daqui; abrir a conversa de novo nunca
|
|
1778
|
+
* pede. O cursor vale para uma caixa: outra caixa tem outro log, e um cursor da anterior
|
|
1779
|
+
* repetiria turnos antigos como se fossem texto ao vivo.
|
|
1780
|
+
*/
|
|
1781
|
+
boxCursors = /* @__PURE__ */ new Map();
|
|
1782
|
+
boxAddresses = /* @__PURE__ */ new Map();
|
|
1501
1783
|
async open(taskId, observer, signal, transport = "auto") {
|
|
1502
1784
|
if (transport === "http") return this.openSse(taskId, observer, signal);
|
|
1503
1785
|
if (transport === "websocket") return this.openWebSocket(taskId, observer, signal);
|
|
@@ -1508,13 +1790,13 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1508
1790
|
const connection = await this.openWebSocket(taskId, {
|
|
1509
1791
|
...observer,
|
|
1510
1792
|
onDisconnect: (error) => {
|
|
1511
|
-
this.sseFallbackTasks.add(taskId);
|
|
1793
|
+
if (!this.directTasks.has(taskId)) this.sseFallbackTasks.add(taskId);
|
|
1512
1794
|
observer.onDisconnect(error);
|
|
1513
1795
|
}
|
|
1514
1796
|
}, signal);
|
|
1515
1797
|
return this.wrapAutoConnection(taskId, connection);
|
|
1516
1798
|
} catch {
|
|
1517
|
-
this.sseFallbackTasks.add(taskId);
|
|
1799
|
+
if (!this.directTasks.has(taskId)) this.sseFallbackTasks.add(taskId);
|
|
1518
1800
|
return this.wrapAutoConnection(taskId, await this.openSse(taskId, observer, signal));
|
|
1519
1801
|
}
|
|
1520
1802
|
}
|
|
@@ -1522,6 +1804,9 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1522
1804
|
return {
|
|
1523
1805
|
close: () => {
|
|
1524
1806
|
this.sseFallbackTasks.delete(taskId);
|
|
1807
|
+
this.directTasks.delete(taskId);
|
|
1808
|
+
this.boxCursors.delete(taskId);
|
|
1809
|
+
this.boxAddresses.delete(taskId);
|
|
1525
1810
|
connection.close();
|
|
1526
1811
|
}
|
|
1527
1812
|
};
|
|
@@ -1534,13 +1819,161 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1534
1819
|
}
|
|
1535
1820
|
return token;
|
|
1536
1821
|
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Pergunta ao copilot onde esta conversa e servida. Nada aqui e fatal: uma recusa, uma
|
|
1824
|
+
* resposta que nao entendemos ou uma rede que falhou significam apenas que a conversa segue
|
|
1825
|
+
* pelo socket do copilot, que e como toda conversa era servida antes da caixa existir.
|
|
1826
|
+
*/
|
|
1827
|
+
requestDirectChannel(taskId, token, signal) {
|
|
1828
|
+
return this.askDirectChannel(taskId, token, signal).catch(() => null);
|
|
1829
|
+
}
|
|
1830
|
+
/** Like `requestDirectChannel`, but a request the network lost is thrown, not a refusal. */
|
|
1831
|
+
async askDirectChannel(taskId, token, signal) {
|
|
1832
|
+
const url = `${this.apiUrl}/copilot/api/v1/tasks/${encodeURIComponent(taskId)}/channel`;
|
|
1833
|
+
const deadline = Date.now() + CHANNEL_BOOT_TIMEOUT_MS;
|
|
1834
|
+
for (; ; ) {
|
|
1835
|
+
if (signal?.aborted) return null;
|
|
1836
|
+
const response = await globalThis.fetch(url, {
|
|
1837
|
+
method: "POST",
|
|
1838
|
+
headers: { Authorization: `Bearer ${stripBearer2(token)}` },
|
|
1839
|
+
...signal ? { signal } : {}
|
|
1840
|
+
});
|
|
1841
|
+
if (response.status === 202) {
|
|
1842
|
+
if (Date.now() >= deadline) return null;
|
|
1843
|
+
await sleep(CHANNEL_BOOT_RETRY_MS, signal);
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1846
|
+
if (!response.ok) return null;
|
|
1847
|
+
try {
|
|
1848
|
+
return toDirectChannel(await response.json(), this.apiUrl);
|
|
1849
|
+
} catch {
|
|
1850
|
+
return null;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1537
1854
|
async openWebSocket(taskId, observer, signal) {
|
|
1538
1855
|
if (typeof globalThis.WebSocket !== "function") {
|
|
1539
1856
|
throw new TypeError("WebSocket is not available.");
|
|
1540
1857
|
}
|
|
1541
1858
|
if (signal?.aborted) throw signal.reason ?? new Error("Agent WebSocket connection aborted.");
|
|
1542
1859
|
const token = await this.requireFreshToken();
|
|
1860
|
+
const direct = await this.requestDirectChannel(taskId, token, signal);
|
|
1861
|
+
if (direct) this.directTasks.add(taskId);
|
|
1862
|
+
else this.directTasks.delete(taskId);
|
|
1863
|
+
if (direct) return this.openDirect(taskId, direct, observer, signal);
|
|
1543
1864
|
const url = `${this.apiUrl.replace(/^http/i, "ws")}/copilot/ws/tasks/${encodeURIComponent(taskId)}?token=${encodeURIComponent(stripBearer2(token))}`;
|
|
1865
|
+
const socket = await this.dial(url, {
|
|
1866
|
+
signal,
|
|
1867
|
+
onFrame: (event) => observer.onEvent(event),
|
|
1868
|
+
onLost: (error) => observer.onDisconnect(error)
|
|
1869
|
+
});
|
|
1870
|
+
return { close: () => socket.close() };
|
|
1871
|
+
}
|
|
1872
|
+
/** Forgets the cursor when the copilot points the conversation to a box other than the last dialed. */
|
|
1873
|
+
adoptBox(taskId, channel) {
|
|
1874
|
+
const box = boxAddress(channel.wsUrl);
|
|
1875
|
+
if (this.boxAddresses.get(taskId) === box) return;
|
|
1876
|
+
this.boxAddresses.set(taskId, box);
|
|
1877
|
+
this.boxCursors.delete(taskId);
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* The box path. A drop in the middle of a turn is redialed from here, with the replay the
|
|
1881
|
+
* box offers, so the core keeps one connection and one stream: reporting the drop instead
|
|
1882
|
+
* would make it reconcile the turn from persisted history, which is the screen freezing and
|
|
1883
|
+
* the rest of the answer landing at once. The core hears a disconnect only when the redial
|
|
1884
|
+
* gives up, when the copilot no longer offers the box, or when there is no turn to resume:
|
|
1885
|
+
* the sandbox pauses an idle box, and dialing it again would wake it for nobody.
|
|
1886
|
+
*
|
|
1887
|
+
* Opening never asks for a replay, whether the conversation is new to this session or was
|
|
1888
|
+
* open before. What an idle conversation missed is history, which the app loads by REST. Beta
|
|
1889
|
+
* 2026-09-14: a tab open overnight had its box replaced, and the open that followed replayed
|
|
1890
|
+
* the old box's cursor against the new log, which put every old turn on screen as live text.
|
|
1891
|
+
*/
|
|
1892
|
+
async openDirect(taskId, channel, observer, signal) {
|
|
1893
|
+
this.adoptBox(taskId, channel);
|
|
1894
|
+
this.boxCursors.set(taskId, channel.lastSequence);
|
|
1895
|
+
const link = new AbortController();
|
|
1896
|
+
const onAbort = () => link.abort(signal?.reason);
|
|
1897
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1898
|
+
let inTurn = false;
|
|
1899
|
+
let current = null;
|
|
1900
|
+
const onFrame = (event) => {
|
|
1901
|
+
if (typeof event.sequence === "number") {
|
|
1902
|
+
const seen = this.boxCursors.get(taskId) ?? 0;
|
|
1903
|
+
if (event.sequence > seen) this.boxCursors.set(taskId, event.sequence);
|
|
1904
|
+
}
|
|
1905
|
+
inTurn = turnAfter(event, inTurn);
|
|
1906
|
+
observer.onEvent(event);
|
|
1907
|
+
};
|
|
1908
|
+
const onLost = (error, code) => {
|
|
1909
|
+
current = null;
|
|
1910
|
+
if (link.signal.aborted) return;
|
|
1911
|
+
if (!inTurn || code === SUPERSEDED_CLOSE_CODE) {
|
|
1912
|
+
observer.onDisconnect(error);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
void this.redial(taskId, error, link.signal, observer, { onFrame, onLost }).then((socket) => {
|
|
1916
|
+
current = socket;
|
|
1917
|
+
});
|
|
1918
|
+
};
|
|
1919
|
+
current = await this.dial(channel.wsUrl, { signal: link.signal, onFrame, onLost });
|
|
1920
|
+
return {
|
|
1921
|
+
close: () => {
|
|
1922
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1923
|
+
link.abort();
|
|
1924
|
+
current?.close();
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
async redial(taskId, cause, signal, observer, handlers) {
|
|
1929
|
+
let lastError = cause;
|
|
1930
|
+
for (const [index, delayMs] of RECONNECT_DELAYS_MS.entries()) {
|
|
1931
|
+
const attempt = index + 1;
|
|
1932
|
+
observer.onEvent(channelEvent("channelReconnecting", {
|
|
1933
|
+
attempt,
|
|
1934
|
+
maxAttempts: RECONNECT_DELAYS_MS.length,
|
|
1935
|
+
reason: lastError.message
|
|
1936
|
+
}));
|
|
1937
|
+
await sleep(delayMs, signal);
|
|
1938
|
+
if (signal.aborted) return null;
|
|
1939
|
+
const outcome = await this.reopenOnce(taskId, signal, handlers);
|
|
1940
|
+
if (signal.aborted) return null;
|
|
1941
|
+
if (outcome.kind === "socket") {
|
|
1942
|
+
observer.onEvent(channelEvent("channelConnected", { attempt }));
|
|
1943
|
+
return outcome.socket;
|
|
1944
|
+
}
|
|
1945
|
+
if (outcome.kind === "refused") {
|
|
1946
|
+
observer.onDisconnect(new Error(
|
|
1947
|
+
`The copilot no longer offers the box channel (after: ${cause.message})`
|
|
1948
|
+
));
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
lastError = outcome.error;
|
|
1952
|
+
}
|
|
1953
|
+
observer.onDisconnect(new Error(
|
|
1954
|
+
`Agent box channel could not be reopened after ${RECONNECT_DELAYS_MS.length} attempts: ${lastError.message}`
|
|
1955
|
+
));
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
// One attempt to get the box back: a fresh token, the channel request, the dial. A failure is
|
|
1959
|
+
// returned rather than thrown, so the caller decides between another attempt and giving up.
|
|
1960
|
+
async reopenOnce(taskId, signal, handlers) {
|
|
1961
|
+
try {
|
|
1962
|
+
const token = await this.requireFreshToken();
|
|
1963
|
+
const channel = await this.askDirectChannel(taskId, token, signal);
|
|
1964
|
+
if (signal.aborted) return { kind: "failed", error: new Error("Agent box redial aborted.") };
|
|
1965
|
+
if (!channel) return { kind: "refused" };
|
|
1966
|
+
this.adoptBox(taskId, channel);
|
|
1967
|
+
const replayFrom = this.boxCursors.get(taskId);
|
|
1968
|
+
if (replayFrom === void 0) this.boxCursors.set(taskId, channel.lastSequence);
|
|
1969
|
+
const socket = await this.dial(channel.wsUrl, { ...handlers, signal, replayFrom });
|
|
1970
|
+
return { kind: "socket", socket };
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
return { kind: "failed", error: error instanceof Error ? error : new Error(String(error)) };
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
dial(url, options) {
|
|
1976
|
+
const { signal, replayFrom } = options;
|
|
1544
1977
|
return new Promise((resolve, reject) => {
|
|
1545
1978
|
const socket = new globalThis.WebSocket(url);
|
|
1546
1979
|
let opened = false;
|
|
@@ -1559,9 +1992,17 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1559
1992
|
socket.close();
|
|
1560
1993
|
reject(error);
|
|
1561
1994
|
};
|
|
1995
|
+
const watchdog = new SilenceWatchdog(() => {
|
|
1996
|
+
if (intentionalClose) return;
|
|
1997
|
+
intentionalClose = true;
|
|
1998
|
+
removeAbortListener();
|
|
1999
|
+
options.onLost(silenceError("WebSocket"));
|
|
2000
|
+
socket.close(1e3, "Client closed");
|
|
2001
|
+
});
|
|
1562
2002
|
const close = () => {
|
|
1563
2003
|
if (intentionalClose) return;
|
|
1564
2004
|
intentionalClose = true;
|
|
2005
|
+
watchdog.clear();
|
|
1565
2006
|
removeAbortListener();
|
|
1566
2007
|
socket.close(1e3, "Client closed");
|
|
1567
2008
|
};
|
|
@@ -1581,24 +2022,33 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1581
2022
|
opened = true;
|
|
1582
2023
|
settled = true;
|
|
1583
2024
|
globalThis.clearTimeout(timer);
|
|
2025
|
+
watchdog.touch();
|
|
2026
|
+
if (replayFrom !== void 0) {
|
|
2027
|
+
try {
|
|
2028
|
+
socket.send(JSON.stringify({ type: "replay", fromSequence: replayFrom }));
|
|
2029
|
+
} catch {
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
1584
2032
|
resolve({ close });
|
|
1585
2033
|
};
|
|
1586
2034
|
socket.onerror = () => {
|
|
1587
2035
|
if (!opened) rejectHandshake(new Error("Failed to connect to the Agent WebSocket."));
|
|
1588
2036
|
};
|
|
1589
2037
|
socket.onmessage = (message) => {
|
|
2038
|
+
watchdog.touch();
|
|
1590
2039
|
const event = parseEvent(message.data);
|
|
1591
|
-
if (event)
|
|
2040
|
+
if (event) options.onFrame(asDelta(event));
|
|
1592
2041
|
};
|
|
1593
2042
|
socket.onclose = (event) => {
|
|
1594
2043
|
globalThis.clearTimeout(timer);
|
|
2044
|
+
watchdog.clear();
|
|
1595
2045
|
removeAbortListener();
|
|
1596
2046
|
if (!opened) {
|
|
1597
2047
|
rejectHandshake(new Error(`Agent WebSocket closed during handshake (${event.code}).`));
|
|
1598
2048
|
return;
|
|
1599
2049
|
}
|
|
1600
|
-
if (!intentionalClose
|
|
1601
|
-
|
|
2050
|
+
if (!intentionalClose) {
|
|
2051
|
+
options.onLost(new Error(`Agent WebSocket closed (${event.code}).`), event.code);
|
|
1602
2052
|
}
|
|
1603
2053
|
};
|
|
1604
2054
|
});
|
|
@@ -1634,7 +2084,7 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1634
2084
|
disconnected = true;
|
|
1635
2085
|
observer.onDisconnect(error);
|
|
1636
2086
|
};
|
|
1637
|
-
void this.readSse(response.body, observer, abort
|
|
2087
|
+
void this.readSse(response.body, observer, abort).then(() => disconnect()).catch((error) => disconnect(error)).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
1638
2088
|
return {
|
|
1639
2089
|
close: () => {
|
|
1640
2090
|
if (intentionalClose) return;
|
|
@@ -1644,13 +2094,28 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1644
2094
|
}
|
|
1645
2095
|
};
|
|
1646
2096
|
}
|
|
1647
|
-
async readSse(body, observer,
|
|
2097
|
+
async readSse(body, observer, abort) {
|
|
1648
2098
|
const reader = body.getReader();
|
|
1649
2099
|
const decoder = new TextDecoder();
|
|
1650
2100
|
let buffer = "";
|
|
2101
|
+
let breakSilence;
|
|
2102
|
+
const silence = new Promise((_, reject) => {
|
|
2103
|
+
breakSilence = reject;
|
|
2104
|
+
});
|
|
2105
|
+
const watchdog = new SilenceWatchdog(() => breakSilence(silenceError("SSE stream")));
|
|
1651
2106
|
try {
|
|
1652
|
-
|
|
1653
|
-
|
|
2107
|
+
watchdog.touch();
|
|
2108
|
+
while (!abort.signal.aborted) {
|
|
2109
|
+
let chunk;
|
|
2110
|
+
try {
|
|
2111
|
+
chunk = await Promise.race([reader.read(), silence]);
|
|
2112
|
+
} catch (error) {
|
|
2113
|
+
abort.abort();
|
|
2114
|
+
await reader.cancel().catch(() => void 0);
|
|
2115
|
+
throw error;
|
|
2116
|
+
}
|
|
2117
|
+
watchdog.touch();
|
|
2118
|
+
const { done, value } = chunk;
|
|
1654
2119
|
if (done) break;
|
|
1655
2120
|
buffer += decoder.decode(value, { stream: true });
|
|
1656
2121
|
let separator = /\r?\n\r?\n/.exec(buffer);
|
|
@@ -1664,6 +2129,7 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1664
2129
|
}
|
|
1665
2130
|
}
|
|
1666
2131
|
} finally {
|
|
2132
|
+
watchdog.clear();
|
|
1667
2133
|
reader.releaseLock();
|
|
1668
2134
|
}
|
|
1669
2135
|
}
|
|
@@ -1672,11 +2138,35 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1672
2138
|
// src/modules/agent-tasks.ts
|
|
1673
2139
|
function createBrowserAgentTasksModule(httpClient, auth, apiUrl) {
|
|
1674
2140
|
const tasks = (0, import_sdk_core8.createAgentTasksModule)(httpClient, coreErrors);
|
|
2141
|
+
const source = new BrowserAgentTaskEventSource(auth, apiUrl);
|
|
2142
|
+
const observers = /* @__PURE__ */ new Map();
|
|
2143
|
+
const outbox = new AgentInputOutbox(tasks, (taskId, event) => observers.get(taskId)?.onEvent(event));
|
|
2144
|
+
const eventSource = {
|
|
2145
|
+
async open(taskId, observer, signal, transport) {
|
|
2146
|
+
observers.set(taskId, observer);
|
|
2147
|
+
let connection;
|
|
2148
|
+
try {
|
|
2149
|
+
connection = await source.open(taskId, observer, signal, transport);
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
observers.delete(taskId);
|
|
2152
|
+
throw error;
|
|
2153
|
+
}
|
|
2154
|
+
return {
|
|
2155
|
+
close: () => {
|
|
2156
|
+
outbox.abandon(taskId);
|
|
2157
|
+
observers.delete(taskId);
|
|
2158
|
+
connection.close();
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
1675
2163
|
const manager = (0, import_sdk_core8.createAgentTaskSessionManager)({
|
|
1676
|
-
tasks,
|
|
1677
|
-
eventSource
|
|
2164
|
+
tasks: { ...tasks, sendInput: (taskId, input) => outbox.sendInput(taskId, input) },
|
|
2165
|
+
eventSource
|
|
2166
|
+
});
|
|
2167
|
+
return (0, import_sdk_core8.withAgentTaskSessions)(tasks, {
|
|
2168
|
+
session: (options) => holdSendsWhileOffline(manager.session(options), outbox)
|
|
1678
2169
|
});
|
|
1679
|
-
return (0, import_sdk_core8.withAgentTaskSessions)(tasks, manager);
|
|
1680
2170
|
}
|
|
1681
2171
|
|
|
1682
2172
|
// src/modules/agent-credentials.ts
|
package/dist/index.js
CHANGED
|
@@ -1423,8 +1423,230 @@ import {
|
|
|
1423
1423
|
withAgentTaskSessions
|
|
1424
1424
|
} from "@mitralab.io/sdk-core";
|
|
1425
1425
|
|
|
1426
|
+
// src/modules/agent-outbox.ts
|
|
1427
|
+
var OUTBOX_DELAYS_MS = [2e3, 5e3, 1e4, 2e4, 4e4];
|
|
1428
|
+
var browserNetwork = {
|
|
1429
|
+
isOffline: () => globalThis.navigator?.onLine === false,
|
|
1430
|
+
onOnline: (listener) => {
|
|
1431
|
+
const target = globalThis;
|
|
1432
|
+
if (typeof target.addEventListener !== "function") return () => void 0;
|
|
1433
|
+
target.addEventListener("online", listener);
|
|
1434
|
+
return () => target.removeEventListener?.("online", listener);
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
function sendGotNoResponse(error) {
|
|
1438
|
+
return error instanceof TypeError;
|
|
1439
|
+
}
|
|
1440
|
+
function reasonOf(error) {
|
|
1441
|
+
return error instanceof Error ? error.message : String(error);
|
|
1442
|
+
}
|
|
1443
|
+
function neverSent(pending) {
|
|
1444
|
+
return new Error(
|
|
1445
|
+
`Agent prompt was never sent: the session closed while it waited for the network (${pending} pending).`
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
var AgentInputOutbox = class {
|
|
1449
|
+
constructor(tasks, announce, network = browserNetwork) {
|
|
1450
|
+
this.tasks = tasks;
|
|
1451
|
+
this.announce = announce;
|
|
1452
|
+
this.network = network;
|
|
1453
|
+
}
|
|
1454
|
+
held = /* @__PURE__ */ new Map();
|
|
1455
|
+
gated = /* @__PURE__ */ new Map();
|
|
1456
|
+
async sendInput(taskId, input) {
|
|
1457
|
+
try {
|
|
1458
|
+
await this.tasks.sendInput(taskId, input);
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
if (input.type !== "message" || !sendGotNoResponse(error)) throw error;
|
|
1461
|
+
await this.hold(taskId, input, error);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
/** The session is closing: a prompt still waiting is reported, never dropped in silence. */
|
|
1465
|
+
abandon(taskId) {
|
|
1466
|
+
const pending = this.held.get(taskId);
|
|
1467
|
+
if (!pending?.length) return;
|
|
1468
|
+
this.held.delete(taskId);
|
|
1469
|
+
this.drop(taskId, pending);
|
|
1470
|
+
}
|
|
1471
|
+
/** True when the browser says there is no network at all. */
|
|
1472
|
+
get offline() {
|
|
1473
|
+
return this.network.isOffline();
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Keeps a prompt out of the core until the browser reports the network is back, then hands it
|
|
1477
|
+
* over through `deliver`. Prompts held for one session leave in the order they arrived. The
|
|
1478
|
+
* promise settles when the prompt is handed over, or rejects if the session closes first.
|
|
1479
|
+
*/
|
|
1480
|
+
holdUntilOnline(session, taskId, deliver) {
|
|
1481
|
+
return new Promise((resolve, reject) => {
|
|
1482
|
+
const prompt = { resolve, reject, disarm: () => void 0 };
|
|
1483
|
+
this.gated.set(session, [...this.gated.get(session) ?? [], prompt]);
|
|
1484
|
+
if (taskId) {
|
|
1485
|
+
this.announce(taskId, {
|
|
1486
|
+
type: "inputUnsent",
|
|
1487
|
+
payload: { attempt: 0, reason: "The browser is offline.", waitingForOnline: true },
|
|
1488
|
+
timestamp: Date.now()
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
const offOnline = this.network.onOnline(() => {
|
|
1492
|
+
prompt.disarm();
|
|
1493
|
+
const rest = (this.gated.get(session) ?? []).filter((candidate) => candidate !== prompt);
|
|
1494
|
+
if (rest.length) this.gated.set(session, rest);
|
|
1495
|
+
else this.gated.delete(session);
|
|
1496
|
+
deliver();
|
|
1497
|
+
resolve();
|
|
1498
|
+
});
|
|
1499
|
+
prompt.disarm = () => {
|
|
1500
|
+
offOnline();
|
|
1501
|
+
prompt.disarm = () => void 0;
|
|
1502
|
+
};
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
/** The session is closing with prompts that never entered the core: same report as `abandon`. */
|
|
1506
|
+
abandonGated(session, taskId) {
|
|
1507
|
+
const pending = this.gated.get(session);
|
|
1508
|
+
if (!pending?.length) return;
|
|
1509
|
+
this.gated.delete(session);
|
|
1510
|
+
this.drop(taskId, pending);
|
|
1511
|
+
}
|
|
1512
|
+
drop(taskId, pending) {
|
|
1513
|
+
const error = neverSent(pending.length);
|
|
1514
|
+
if (taskId) {
|
|
1515
|
+
this.announce(taskId, {
|
|
1516
|
+
type: "error",
|
|
1517
|
+
payload: { code: "INPUT_UNSENT", message: error.message },
|
|
1518
|
+
timestamp: Date.now()
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
for (const prompt of pending) {
|
|
1522
|
+
prompt.disarm();
|
|
1523
|
+
prompt.reject(error);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
hold(taskId, input, cause) {
|
|
1527
|
+
return new Promise((resolve, reject) => {
|
|
1528
|
+
const prompt = { input, attempts: 1, resolve, reject, disarm: () => void 0 };
|
|
1529
|
+
this.held.set(taskId, [...this.held.get(taskId) ?? [], prompt]);
|
|
1530
|
+
this.schedule(taskId, prompt, cause);
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
schedule(taskId, prompt, cause) {
|
|
1534
|
+
const waitingForOnline = this.network.isOffline();
|
|
1535
|
+
const retryInMs = waitingForOnline ? void 0 : OUTBOX_DELAYS_MS[prompt.attempts - 1];
|
|
1536
|
+
if (!waitingForOnline && retryInMs === void 0) {
|
|
1537
|
+
this.release(taskId, prompt);
|
|
1538
|
+
prompt.reject(new Error(
|
|
1539
|
+
`Agent prompt was not sent after ${prompt.attempts} attempts: ${reasonOf(cause)}`
|
|
1540
|
+
));
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
this.announce(taskId, {
|
|
1544
|
+
type: "inputUnsent",
|
|
1545
|
+
payload: {
|
|
1546
|
+
attempt: prompt.attempts,
|
|
1547
|
+
reason: reasonOf(cause),
|
|
1548
|
+
waitingForOnline,
|
|
1549
|
+
...retryInMs === void 0 ? {} : { retryInMs }
|
|
1550
|
+
},
|
|
1551
|
+
timestamp: Date.now()
|
|
1552
|
+
});
|
|
1553
|
+
const retry = () => {
|
|
1554
|
+
prompt.disarm();
|
|
1555
|
+
void this.retry(taskId, prompt);
|
|
1556
|
+
};
|
|
1557
|
+
const timer = retryInMs === void 0 ? null : globalThis.setTimeout(retry, retryInMs);
|
|
1558
|
+
const offOnline = this.network.onOnline(retry);
|
|
1559
|
+
prompt.disarm = () => {
|
|
1560
|
+
if (timer !== null) globalThis.clearTimeout(timer);
|
|
1561
|
+
offOnline();
|
|
1562
|
+
prompt.disarm = () => void 0;
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
async retry(taskId, prompt) {
|
|
1566
|
+
prompt.attempts += 1;
|
|
1567
|
+
try {
|
|
1568
|
+
await this.tasks.sendInput(taskId, prompt.input);
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
if (!this.isHeld(taskId, prompt)) return;
|
|
1571
|
+
if (!sendGotNoResponse(error)) {
|
|
1572
|
+
this.release(taskId, prompt);
|
|
1573
|
+
prompt.reject(error instanceof Error ? error : new Error(String(error)));
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
this.schedule(taskId, prompt, error);
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (!this.isHeld(taskId, prompt)) return;
|
|
1580
|
+
this.release(taskId, prompt);
|
|
1581
|
+
this.announce(taskId, {
|
|
1582
|
+
type: "inputSent",
|
|
1583
|
+
payload: { attempts: prompt.attempts },
|
|
1584
|
+
timestamp: Date.now()
|
|
1585
|
+
});
|
|
1586
|
+
prompt.resolve();
|
|
1587
|
+
}
|
|
1588
|
+
isHeld(taskId, prompt) {
|
|
1589
|
+
return this.held.get(taskId)?.includes(prompt) ?? false;
|
|
1590
|
+
}
|
|
1591
|
+
release(taskId, prompt) {
|
|
1592
|
+
const rest = (this.held.get(taskId) ?? []).filter((candidate) => candidate !== prompt);
|
|
1593
|
+
if (rest.length) this.held.set(taskId, rest);
|
|
1594
|
+
else this.held.delete(taskId);
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
function holdSendsWhileOffline(session, outbox) {
|
|
1598
|
+
const passThrough = (prompt) => !outbox.offline || !prompt.trim() || session.status === "closed";
|
|
1599
|
+
const gated = {
|
|
1600
|
+
send: (prompt, options) => {
|
|
1601
|
+
if (passThrough(prompt)) {
|
|
1602
|
+
session.send(prompt, options);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
void outbox.holdUntilOnline(session, session.taskId, () => session.send(prompt, options)).catch(() => void 0);
|
|
1606
|
+
},
|
|
1607
|
+
sendAndWait: (prompt, options) => {
|
|
1608
|
+
if (passThrough(prompt)) return session.sendAndWait(prompt, options);
|
|
1609
|
+
let turn;
|
|
1610
|
+
return outbox.holdUntilOnline(session, session.taskId, () => {
|
|
1611
|
+
turn = session.sendAndWait(prompt, options);
|
|
1612
|
+
}).then(() => turn);
|
|
1613
|
+
},
|
|
1614
|
+
close: () => {
|
|
1615
|
+
outbox.abandonGated(session, session.taskId);
|
|
1616
|
+
session.close();
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
return new Proxy(session, {
|
|
1620
|
+
get: (target, key) => key in gated ? gated[key] : Reflect.get(target, key, target)
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1426
1624
|
// src/modules/agent-session.ts
|
|
1427
1625
|
var CONNECT_TIMEOUT_MS = 15e3;
|
|
1626
|
+
var CHANNEL_BOOT_TIMEOUT_MS = 9e4;
|
|
1627
|
+
var CHANNEL_BOOT_RETRY_MS = 2e3;
|
|
1628
|
+
var SILENCE_TIMEOUT_MS = 6e4;
|
|
1629
|
+
var RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 16e3];
|
|
1630
|
+
var SUPERSEDED_CLOSE_CODE = 4409;
|
|
1631
|
+
var SilenceWatchdog = class {
|
|
1632
|
+
constructor(onSilence) {
|
|
1633
|
+
this.onSilence = onSilence;
|
|
1634
|
+
}
|
|
1635
|
+
timer = null;
|
|
1636
|
+
touch() {
|
|
1637
|
+
this.clear();
|
|
1638
|
+
this.timer = globalThis.setTimeout(this.onSilence, SILENCE_TIMEOUT_MS);
|
|
1639
|
+
}
|
|
1640
|
+
clear() {
|
|
1641
|
+
if (this.timer !== null) {
|
|
1642
|
+
globalThis.clearTimeout(this.timer);
|
|
1643
|
+
this.timer = null;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
function silenceError(transport) {
|
|
1648
|
+
return new Error(`Agent ${transport} went silent for ${SILENCE_TIMEOUT_MS / 1e3}s.`);
|
|
1649
|
+
}
|
|
1428
1650
|
function stripBearer2(token) {
|
|
1429
1651
|
return token.replace(/^Bearer\s+/i, "");
|
|
1430
1652
|
}
|
|
@@ -1443,6 +1665,37 @@ function expectEvent(value) {
|
|
|
1443
1665
|
...typeof event.sequence === "number" ? { sequence: event.sequence } : {}
|
|
1444
1666
|
};
|
|
1445
1667
|
}
|
|
1668
|
+
function isSameGateway(candidate, apiUrl) {
|
|
1669
|
+
try {
|
|
1670
|
+
const target = new URL(candidate);
|
|
1671
|
+
if (target.protocol !== "ws:" && target.protocol !== "wss:") return false;
|
|
1672
|
+
return target.host === new URL(apiUrl).host;
|
|
1673
|
+
} catch {
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function boxAddress(wsUrl) {
|
|
1678
|
+
const url = new URL(wsUrl);
|
|
1679
|
+
return `${url.origin}${url.pathname}`;
|
|
1680
|
+
}
|
|
1681
|
+
function toDirectChannel(body, apiUrl) {
|
|
1682
|
+
if (typeof body !== "object" || body === null) return null;
|
|
1683
|
+
const channel = body;
|
|
1684
|
+
if (typeof channel.wsUrl !== "string" || !isSameGateway(channel.wsUrl, apiUrl)) return null;
|
|
1685
|
+
return {
|
|
1686
|
+
wsUrl: channel.wsUrl,
|
|
1687
|
+
lastSequence: typeof channel.lastSequence === "number" && channel.lastSequence > 0 ? channel.lastSequence : 0
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
function sleep(ms, signal) {
|
|
1691
|
+
return new Promise((resolve) => {
|
|
1692
|
+
const timer = globalThis.setTimeout(resolve, ms);
|
|
1693
|
+
signal?.addEventListener("abort", () => {
|
|
1694
|
+
globalThis.clearTimeout(timer);
|
|
1695
|
+
resolve();
|
|
1696
|
+
}, { once: true });
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1446
1699
|
function parseEvent(raw) {
|
|
1447
1700
|
if (typeof raw !== "string") return expectEvent(raw);
|
|
1448
1701
|
try {
|
|
@@ -1451,8 +1704,25 @@ function parseEvent(raw) {
|
|
|
1451
1704
|
return null;
|
|
1452
1705
|
}
|
|
1453
1706
|
}
|
|
1454
|
-
function
|
|
1455
|
-
|
|
1707
|
+
function asDelta(event) {
|
|
1708
|
+
if (event.type !== "textChunk") return event;
|
|
1709
|
+
const kind = asObject(event.payload)?.kind;
|
|
1710
|
+
return { ...event, type: kind === "thinking" ? "thinking" : "textDelta" };
|
|
1711
|
+
}
|
|
1712
|
+
var TURN_FRAME_TYPES = /* @__PURE__ */ new Set(["textDelta", "thinking", "toolCall", "toolResult"]);
|
|
1713
|
+
var TURN_END_REASONS = /* @__PURE__ */ new Set(["stop", "endTurn", "interrupted"]);
|
|
1714
|
+
function turnAfter(event, inTurn) {
|
|
1715
|
+
if (TURN_FRAME_TYPES.has(event.type)) return true;
|
|
1716
|
+
if (event.type === "error") return false;
|
|
1717
|
+
if (event.type === "stepFinish") {
|
|
1718
|
+
const payload = asObject(event.payload);
|
|
1719
|
+
if (TURN_END_REASONS.has(payload?.reason)) return false;
|
|
1720
|
+
return asObject(payload?.lifecycle)?.interruptTerminal !== true;
|
|
1721
|
+
}
|
|
1722
|
+
return inTurn;
|
|
1723
|
+
}
|
|
1724
|
+
function channelEvent(type, payload) {
|
|
1725
|
+
return { type, payload, timestamp: Date.now() };
|
|
1456
1726
|
}
|
|
1457
1727
|
var BrowserAgentTaskEventSource = class {
|
|
1458
1728
|
constructor(auth, apiUrl) {
|
|
@@ -1461,6 +1731,18 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1461
1731
|
}
|
|
1462
1732
|
apiUrl;
|
|
1463
1733
|
sseFallbackTasks = /* @__PURE__ */ new Set();
|
|
1734
|
+
// Tasks whose last WebSocket was the box itself. A box socket that closes says nothing about
|
|
1735
|
+
// WebSockets: the box went idle or the channel was superseded, and the answer is to ask the
|
|
1736
|
+
// copilot for the channel again. Only the copilot's own socket failing sends a task to SSE.
|
|
1737
|
+
directTasks = /* @__PURE__ */ new Set();
|
|
1738
|
+
/**
|
|
1739
|
+
* Ate onde esta sessao ja viu o log da caixa, por conversa, e de qual caixa esse log e. So o
|
|
1740
|
+
* redial no meio de um turno pede repeticao a partir daqui; abrir a conversa de novo nunca
|
|
1741
|
+
* pede. O cursor vale para uma caixa: outra caixa tem outro log, e um cursor da anterior
|
|
1742
|
+
* repetiria turnos antigos como se fossem texto ao vivo.
|
|
1743
|
+
*/
|
|
1744
|
+
boxCursors = /* @__PURE__ */ new Map();
|
|
1745
|
+
boxAddresses = /* @__PURE__ */ new Map();
|
|
1464
1746
|
async open(taskId, observer, signal, transport = "auto") {
|
|
1465
1747
|
if (transport === "http") return this.openSse(taskId, observer, signal);
|
|
1466
1748
|
if (transport === "websocket") return this.openWebSocket(taskId, observer, signal);
|
|
@@ -1471,13 +1753,13 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1471
1753
|
const connection = await this.openWebSocket(taskId, {
|
|
1472
1754
|
...observer,
|
|
1473
1755
|
onDisconnect: (error) => {
|
|
1474
|
-
this.sseFallbackTasks.add(taskId);
|
|
1756
|
+
if (!this.directTasks.has(taskId)) this.sseFallbackTasks.add(taskId);
|
|
1475
1757
|
observer.onDisconnect(error);
|
|
1476
1758
|
}
|
|
1477
1759
|
}, signal);
|
|
1478
1760
|
return this.wrapAutoConnection(taskId, connection);
|
|
1479
1761
|
} catch {
|
|
1480
|
-
this.sseFallbackTasks.add(taskId);
|
|
1762
|
+
if (!this.directTasks.has(taskId)) this.sseFallbackTasks.add(taskId);
|
|
1481
1763
|
return this.wrapAutoConnection(taskId, await this.openSse(taskId, observer, signal));
|
|
1482
1764
|
}
|
|
1483
1765
|
}
|
|
@@ -1485,6 +1767,9 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1485
1767
|
return {
|
|
1486
1768
|
close: () => {
|
|
1487
1769
|
this.sseFallbackTasks.delete(taskId);
|
|
1770
|
+
this.directTasks.delete(taskId);
|
|
1771
|
+
this.boxCursors.delete(taskId);
|
|
1772
|
+
this.boxAddresses.delete(taskId);
|
|
1488
1773
|
connection.close();
|
|
1489
1774
|
}
|
|
1490
1775
|
};
|
|
@@ -1497,13 +1782,161 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1497
1782
|
}
|
|
1498
1783
|
return token;
|
|
1499
1784
|
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Pergunta ao copilot onde esta conversa e servida. Nada aqui e fatal: uma recusa, uma
|
|
1787
|
+
* resposta que nao entendemos ou uma rede que falhou significam apenas que a conversa segue
|
|
1788
|
+
* pelo socket do copilot, que e como toda conversa era servida antes da caixa existir.
|
|
1789
|
+
*/
|
|
1790
|
+
requestDirectChannel(taskId, token, signal) {
|
|
1791
|
+
return this.askDirectChannel(taskId, token, signal).catch(() => null);
|
|
1792
|
+
}
|
|
1793
|
+
/** Like `requestDirectChannel`, but a request the network lost is thrown, not a refusal. */
|
|
1794
|
+
async askDirectChannel(taskId, token, signal) {
|
|
1795
|
+
const url = `${this.apiUrl}/copilot/api/v1/tasks/${encodeURIComponent(taskId)}/channel`;
|
|
1796
|
+
const deadline = Date.now() + CHANNEL_BOOT_TIMEOUT_MS;
|
|
1797
|
+
for (; ; ) {
|
|
1798
|
+
if (signal?.aborted) return null;
|
|
1799
|
+
const response = await globalThis.fetch(url, {
|
|
1800
|
+
method: "POST",
|
|
1801
|
+
headers: { Authorization: `Bearer ${stripBearer2(token)}` },
|
|
1802
|
+
...signal ? { signal } : {}
|
|
1803
|
+
});
|
|
1804
|
+
if (response.status === 202) {
|
|
1805
|
+
if (Date.now() >= deadline) return null;
|
|
1806
|
+
await sleep(CHANNEL_BOOT_RETRY_MS, signal);
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
if (!response.ok) return null;
|
|
1810
|
+
try {
|
|
1811
|
+
return toDirectChannel(await response.json(), this.apiUrl);
|
|
1812
|
+
} catch {
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1500
1817
|
async openWebSocket(taskId, observer, signal) {
|
|
1501
1818
|
if (typeof globalThis.WebSocket !== "function") {
|
|
1502
1819
|
throw new TypeError("WebSocket is not available.");
|
|
1503
1820
|
}
|
|
1504
1821
|
if (signal?.aborted) throw signal.reason ?? new Error("Agent WebSocket connection aborted.");
|
|
1505
1822
|
const token = await this.requireFreshToken();
|
|
1823
|
+
const direct = await this.requestDirectChannel(taskId, token, signal);
|
|
1824
|
+
if (direct) this.directTasks.add(taskId);
|
|
1825
|
+
else this.directTasks.delete(taskId);
|
|
1826
|
+
if (direct) return this.openDirect(taskId, direct, observer, signal);
|
|
1506
1827
|
const url = `${this.apiUrl.replace(/^http/i, "ws")}/copilot/ws/tasks/${encodeURIComponent(taskId)}?token=${encodeURIComponent(stripBearer2(token))}`;
|
|
1828
|
+
const socket = await this.dial(url, {
|
|
1829
|
+
signal,
|
|
1830
|
+
onFrame: (event) => observer.onEvent(event),
|
|
1831
|
+
onLost: (error) => observer.onDisconnect(error)
|
|
1832
|
+
});
|
|
1833
|
+
return { close: () => socket.close() };
|
|
1834
|
+
}
|
|
1835
|
+
/** Forgets the cursor when the copilot points the conversation to a box other than the last dialed. */
|
|
1836
|
+
adoptBox(taskId, channel) {
|
|
1837
|
+
const box = boxAddress(channel.wsUrl);
|
|
1838
|
+
if (this.boxAddresses.get(taskId) === box) return;
|
|
1839
|
+
this.boxAddresses.set(taskId, box);
|
|
1840
|
+
this.boxCursors.delete(taskId);
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* The box path. A drop in the middle of a turn is redialed from here, with the replay the
|
|
1844
|
+
* box offers, so the core keeps one connection and one stream: reporting the drop instead
|
|
1845
|
+
* would make it reconcile the turn from persisted history, which is the screen freezing and
|
|
1846
|
+
* the rest of the answer landing at once. The core hears a disconnect only when the redial
|
|
1847
|
+
* gives up, when the copilot no longer offers the box, or when there is no turn to resume:
|
|
1848
|
+
* the sandbox pauses an idle box, and dialing it again would wake it for nobody.
|
|
1849
|
+
*
|
|
1850
|
+
* Opening never asks for a replay, whether the conversation is new to this session or was
|
|
1851
|
+
* open before. What an idle conversation missed is history, which the app loads by REST. Beta
|
|
1852
|
+
* 2026-09-14: a tab open overnight had its box replaced, and the open that followed replayed
|
|
1853
|
+
* the old box's cursor against the new log, which put every old turn on screen as live text.
|
|
1854
|
+
*/
|
|
1855
|
+
async openDirect(taskId, channel, observer, signal) {
|
|
1856
|
+
this.adoptBox(taskId, channel);
|
|
1857
|
+
this.boxCursors.set(taskId, channel.lastSequence);
|
|
1858
|
+
const link = new AbortController();
|
|
1859
|
+
const onAbort = () => link.abort(signal?.reason);
|
|
1860
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1861
|
+
let inTurn = false;
|
|
1862
|
+
let current = null;
|
|
1863
|
+
const onFrame = (event) => {
|
|
1864
|
+
if (typeof event.sequence === "number") {
|
|
1865
|
+
const seen = this.boxCursors.get(taskId) ?? 0;
|
|
1866
|
+
if (event.sequence > seen) this.boxCursors.set(taskId, event.sequence);
|
|
1867
|
+
}
|
|
1868
|
+
inTurn = turnAfter(event, inTurn);
|
|
1869
|
+
observer.onEvent(event);
|
|
1870
|
+
};
|
|
1871
|
+
const onLost = (error, code) => {
|
|
1872
|
+
current = null;
|
|
1873
|
+
if (link.signal.aborted) return;
|
|
1874
|
+
if (!inTurn || code === SUPERSEDED_CLOSE_CODE) {
|
|
1875
|
+
observer.onDisconnect(error);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
void this.redial(taskId, error, link.signal, observer, { onFrame, onLost }).then((socket) => {
|
|
1879
|
+
current = socket;
|
|
1880
|
+
});
|
|
1881
|
+
};
|
|
1882
|
+
current = await this.dial(channel.wsUrl, { signal: link.signal, onFrame, onLost });
|
|
1883
|
+
return {
|
|
1884
|
+
close: () => {
|
|
1885
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1886
|
+
link.abort();
|
|
1887
|
+
current?.close();
|
|
1888
|
+
}
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
async redial(taskId, cause, signal, observer, handlers) {
|
|
1892
|
+
let lastError = cause;
|
|
1893
|
+
for (const [index, delayMs] of RECONNECT_DELAYS_MS.entries()) {
|
|
1894
|
+
const attempt = index + 1;
|
|
1895
|
+
observer.onEvent(channelEvent("channelReconnecting", {
|
|
1896
|
+
attempt,
|
|
1897
|
+
maxAttempts: RECONNECT_DELAYS_MS.length,
|
|
1898
|
+
reason: lastError.message
|
|
1899
|
+
}));
|
|
1900
|
+
await sleep(delayMs, signal);
|
|
1901
|
+
if (signal.aborted) return null;
|
|
1902
|
+
const outcome = await this.reopenOnce(taskId, signal, handlers);
|
|
1903
|
+
if (signal.aborted) return null;
|
|
1904
|
+
if (outcome.kind === "socket") {
|
|
1905
|
+
observer.onEvent(channelEvent("channelConnected", { attempt }));
|
|
1906
|
+
return outcome.socket;
|
|
1907
|
+
}
|
|
1908
|
+
if (outcome.kind === "refused") {
|
|
1909
|
+
observer.onDisconnect(new Error(
|
|
1910
|
+
`The copilot no longer offers the box channel (after: ${cause.message})`
|
|
1911
|
+
));
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
lastError = outcome.error;
|
|
1915
|
+
}
|
|
1916
|
+
observer.onDisconnect(new Error(
|
|
1917
|
+
`Agent box channel could not be reopened after ${RECONNECT_DELAYS_MS.length} attempts: ${lastError.message}`
|
|
1918
|
+
));
|
|
1919
|
+
return null;
|
|
1920
|
+
}
|
|
1921
|
+
// One attempt to get the box back: a fresh token, the channel request, the dial. A failure is
|
|
1922
|
+
// returned rather than thrown, so the caller decides between another attempt and giving up.
|
|
1923
|
+
async reopenOnce(taskId, signal, handlers) {
|
|
1924
|
+
try {
|
|
1925
|
+
const token = await this.requireFreshToken();
|
|
1926
|
+
const channel = await this.askDirectChannel(taskId, token, signal);
|
|
1927
|
+
if (signal.aborted) return { kind: "failed", error: new Error("Agent box redial aborted.") };
|
|
1928
|
+
if (!channel) return { kind: "refused" };
|
|
1929
|
+
this.adoptBox(taskId, channel);
|
|
1930
|
+
const replayFrom = this.boxCursors.get(taskId);
|
|
1931
|
+
if (replayFrom === void 0) this.boxCursors.set(taskId, channel.lastSequence);
|
|
1932
|
+
const socket = await this.dial(channel.wsUrl, { ...handlers, signal, replayFrom });
|
|
1933
|
+
return { kind: "socket", socket };
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
return { kind: "failed", error: error instanceof Error ? error : new Error(String(error)) };
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
dial(url, options) {
|
|
1939
|
+
const { signal, replayFrom } = options;
|
|
1507
1940
|
return new Promise((resolve, reject) => {
|
|
1508
1941
|
const socket = new globalThis.WebSocket(url);
|
|
1509
1942
|
let opened = false;
|
|
@@ -1522,9 +1955,17 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1522
1955
|
socket.close();
|
|
1523
1956
|
reject(error);
|
|
1524
1957
|
};
|
|
1958
|
+
const watchdog = new SilenceWatchdog(() => {
|
|
1959
|
+
if (intentionalClose) return;
|
|
1960
|
+
intentionalClose = true;
|
|
1961
|
+
removeAbortListener();
|
|
1962
|
+
options.onLost(silenceError("WebSocket"));
|
|
1963
|
+
socket.close(1e3, "Client closed");
|
|
1964
|
+
});
|
|
1525
1965
|
const close = () => {
|
|
1526
1966
|
if (intentionalClose) return;
|
|
1527
1967
|
intentionalClose = true;
|
|
1968
|
+
watchdog.clear();
|
|
1528
1969
|
removeAbortListener();
|
|
1529
1970
|
socket.close(1e3, "Client closed");
|
|
1530
1971
|
};
|
|
@@ -1544,24 +1985,33 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1544
1985
|
opened = true;
|
|
1545
1986
|
settled = true;
|
|
1546
1987
|
globalThis.clearTimeout(timer);
|
|
1988
|
+
watchdog.touch();
|
|
1989
|
+
if (replayFrom !== void 0) {
|
|
1990
|
+
try {
|
|
1991
|
+
socket.send(JSON.stringify({ type: "replay", fromSequence: replayFrom }));
|
|
1992
|
+
} catch {
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1547
1995
|
resolve({ close });
|
|
1548
1996
|
};
|
|
1549
1997
|
socket.onerror = () => {
|
|
1550
1998
|
if (!opened) rejectHandshake(new Error("Failed to connect to the Agent WebSocket."));
|
|
1551
1999
|
};
|
|
1552
2000
|
socket.onmessage = (message) => {
|
|
2001
|
+
watchdog.touch();
|
|
1553
2002
|
const event = parseEvent(message.data);
|
|
1554
|
-
if (event)
|
|
2003
|
+
if (event) options.onFrame(asDelta(event));
|
|
1555
2004
|
};
|
|
1556
2005
|
socket.onclose = (event) => {
|
|
1557
2006
|
globalThis.clearTimeout(timer);
|
|
2007
|
+
watchdog.clear();
|
|
1558
2008
|
removeAbortListener();
|
|
1559
2009
|
if (!opened) {
|
|
1560
2010
|
rejectHandshake(new Error(`Agent WebSocket closed during handshake (${event.code}).`));
|
|
1561
2011
|
return;
|
|
1562
2012
|
}
|
|
1563
|
-
if (!intentionalClose
|
|
1564
|
-
|
|
2013
|
+
if (!intentionalClose) {
|
|
2014
|
+
options.onLost(new Error(`Agent WebSocket closed (${event.code}).`), event.code);
|
|
1565
2015
|
}
|
|
1566
2016
|
};
|
|
1567
2017
|
});
|
|
@@ -1597,7 +2047,7 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1597
2047
|
disconnected = true;
|
|
1598
2048
|
observer.onDisconnect(error);
|
|
1599
2049
|
};
|
|
1600
|
-
void this.readSse(response.body, observer, abort
|
|
2050
|
+
void this.readSse(response.body, observer, abort).then(() => disconnect()).catch((error) => disconnect(error)).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
1601
2051
|
return {
|
|
1602
2052
|
close: () => {
|
|
1603
2053
|
if (intentionalClose) return;
|
|
@@ -1607,13 +2057,28 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1607
2057
|
}
|
|
1608
2058
|
};
|
|
1609
2059
|
}
|
|
1610
|
-
async readSse(body, observer,
|
|
2060
|
+
async readSse(body, observer, abort) {
|
|
1611
2061
|
const reader = body.getReader();
|
|
1612
2062
|
const decoder = new TextDecoder();
|
|
1613
2063
|
let buffer = "";
|
|
2064
|
+
let breakSilence;
|
|
2065
|
+
const silence = new Promise((_, reject) => {
|
|
2066
|
+
breakSilence = reject;
|
|
2067
|
+
});
|
|
2068
|
+
const watchdog = new SilenceWatchdog(() => breakSilence(silenceError("SSE stream")));
|
|
1614
2069
|
try {
|
|
1615
|
-
|
|
1616
|
-
|
|
2070
|
+
watchdog.touch();
|
|
2071
|
+
while (!abort.signal.aborted) {
|
|
2072
|
+
let chunk;
|
|
2073
|
+
try {
|
|
2074
|
+
chunk = await Promise.race([reader.read(), silence]);
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
abort.abort();
|
|
2077
|
+
await reader.cancel().catch(() => void 0);
|
|
2078
|
+
throw error;
|
|
2079
|
+
}
|
|
2080
|
+
watchdog.touch();
|
|
2081
|
+
const { done, value } = chunk;
|
|
1617
2082
|
if (done) break;
|
|
1618
2083
|
buffer += decoder.decode(value, { stream: true });
|
|
1619
2084
|
let separator = /\r?\n\r?\n/.exec(buffer);
|
|
@@ -1627,6 +2092,7 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1627
2092
|
}
|
|
1628
2093
|
}
|
|
1629
2094
|
} finally {
|
|
2095
|
+
watchdog.clear();
|
|
1630
2096
|
reader.releaseLock();
|
|
1631
2097
|
}
|
|
1632
2098
|
}
|
|
@@ -1635,11 +2101,35 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1635
2101
|
// src/modules/agent-tasks.ts
|
|
1636
2102
|
function createBrowserAgentTasksModule(httpClient, auth, apiUrl) {
|
|
1637
2103
|
const tasks = createAgentTasksModule(httpClient, coreErrors);
|
|
2104
|
+
const source = new BrowserAgentTaskEventSource(auth, apiUrl);
|
|
2105
|
+
const observers = /* @__PURE__ */ new Map();
|
|
2106
|
+
const outbox = new AgentInputOutbox(tasks, (taskId, event) => observers.get(taskId)?.onEvent(event));
|
|
2107
|
+
const eventSource = {
|
|
2108
|
+
async open(taskId, observer, signal, transport) {
|
|
2109
|
+
observers.set(taskId, observer);
|
|
2110
|
+
let connection;
|
|
2111
|
+
try {
|
|
2112
|
+
connection = await source.open(taskId, observer, signal, transport);
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
observers.delete(taskId);
|
|
2115
|
+
throw error;
|
|
2116
|
+
}
|
|
2117
|
+
return {
|
|
2118
|
+
close: () => {
|
|
2119
|
+
outbox.abandon(taskId);
|
|
2120
|
+
observers.delete(taskId);
|
|
2121
|
+
connection.close();
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
1638
2126
|
const manager = createAgentTaskSessionManager({
|
|
1639
|
-
tasks,
|
|
1640
|
-
eventSource
|
|
2127
|
+
tasks: { ...tasks, sendInput: (taskId, input) => outbox.sendInput(taskId, input) },
|
|
2128
|
+
eventSource
|
|
2129
|
+
});
|
|
2130
|
+
return withAgentTaskSessions(tasks, {
|
|
2131
|
+
session: (options) => holdSendsWhileOffline(manager.session(options), outbox)
|
|
1641
2132
|
});
|
|
1642
|
-
return withAgentTaskSessions(tasks, manager);
|
|
1643
2133
|
}
|
|
1644
2134
|
|
|
1645
2135
|
// src/modules/agent-credentials.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mitralab.io/platform-sdk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "JavaScript/TypeScript SDK for building apps on the Mitra Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"url": "https://github.com/mitralab-dev/mitra-platform-sdk/issues"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@mitralab.io/sdk-core": "0.2.
|
|
65
|
+
"@mitralab.io/sdk-core": "0.2.2",
|
|
66
66
|
"mitra-interactions-sdk": "1.0.60-beta.48"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|