@slopus/happy-agent-base 0.0.4 → 0.0.5
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 +19 -0
- package/dist/Agent.d.ts.map +1 -1
- package/dist/Agent.js +46 -18
- package/dist/Agent.js.map +1 -1
- package/dist/AgentBase.d.ts +26 -0
- package/dist/AgentBase.d.ts.map +1 -1
- package/dist/AgentBase.js +195 -29
- package/dist/AgentBase.js.map +1 -1
- package/dist/AgentBaseHooks.d.ts +148 -15
- package/dist/AgentBaseHooks.d.ts.map +1 -1
- package/dist/AgentContexts.d.ts +19 -5
- package/dist/AgentContexts.d.ts.map +1 -1
- package/dist/AgentContexts.js +26 -6
- package/dist/AgentContexts.js.map +1 -1
- package/dist/AgentFeature.d.ts +36 -5
- package/dist/AgentFeature.d.ts.map +1 -1
- package/dist/AgentPermissionMode.d.ts +27 -0
- package/dist/AgentPermissionMode.d.ts.map +1 -0
- package/dist/AgentPermissionMode.js +48 -0
- package/dist/AgentPermissionMode.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/AgentBase.js
CHANGED
|
@@ -2,10 +2,11 @@ import { areProviderModelsCompatible } from "@slopus/happy-providers";
|
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import { Value } from "@sinclair/typebox/value";
|
|
4
4
|
import { asyncLock, createContextNamespace, deterministicStringify, withLifetime, } from "@steve.kite/stdlib";
|
|
5
|
-
import { withAgentContext, withAgentKV, withAgentRunKV } from "./AgentContexts.js";
|
|
5
|
+
import { withAgentContext, withAgentKV, withAgentPermissionMode, withAgentRunKV, } from "./AgentContexts.js";
|
|
6
6
|
import { taskContextBeforeToolCall, withAgentTaskContext } from "./AgentTaskContext.js";
|
|
7
7
|
import { AgentKV } from "./AgentKV.js";
|
|
8
8
|
import { AGENT_BASE_PENDING_KEY, agentBasePendingStateOf, } from "./AgentBasePending.js";
|
|
9
|
+
import { DEFAULT_AGENT_PERMISSION_MODE, isAgentPermissionMode, } from "./AgentPermissionMode.js";
|
|
9
10
|
import { AgentProviders } from "./AgentProviders.js";
|
|
10
11
|
/** Race winner when an abort interrupts a wait on the stream or a running tool. */
|
|
11
12
|
const ABORTED = Symbol("aborted");
|
|
@@ -103,6 +104,20 @@ const INSIDE_CLOSE_REPORT_MS = 15;
|
|
|
103
104
|
* capability released when the hook returns, so it cannot be retained to bypass the lock later.
|
|
104
105
|
* A failing handoff rejects an incompatible switch outright rather than costing the history.
|
|
105
106
|
*
|
|
107
|
+
* ## Permission modes
|
|
108
|
+
*
|
|
109
|
+
* How much of the machine the agent may touch travels with its messages, exactly like its model,
|
|
110
|
+
* and takes effect when the message is consumed rather than when it is queued: a response and the
|
|
111
|
+
* tools it dispatched are already running under the mode they were started with, and are left to
|
|
112
|
+
* finish under it. The mode is durable, so a restart resumes in the mode the conversation reached,
|
|
113
|
+
* and it is carried on every context the agent derives, so a hook or a tool reads what it is
|
|
114
|
+
* running under rather than being told.
|
|
115
|
+
*
|
|
116
|
+
* The loop enforces nothing. It has no idea what any particular tool touches, and a runtime that
|
|
117
|
+
* guessed would be wrong about tools it has never seen. Enforcement belongs to the features and
|
|
118
|
+
* tools that do know; the loop's whole part is to carry the mode, make its changes durable, and
|
|
119
|
+
* report them.
|
|
120
|
+
*
|
|
106
121
|
* ## Recovery
|
|
107
122
|
*
|
|
108
123
|
* Whether a restart owes a response is decided by the last durable record: a consumed message, a
|
|
@@ -200,6 +215,11 @@ export class AgentBase {
|
|
|
200
215
|
#effort;
|
|
201
216
|
/** The service tier in force. */
|
|
202
217
|
#serviceTier;
|
|
218
|
+
/**
|
|
219
|
+
* How much of the machine the agent may touch. Durable, so a restart resumes in the mode the
|
|
220
|
+
* conversation reached, and carried on every context the agent derives.
|
|
221
|
+
*/
|
|
222
|
+
#permissionMode;
|
|
203
223
|
/** The single set of hooks the run is observed by and its configuration extended from. */
|
|
204
224
|
#hooks;
|
|
205
225
|
/**
|
|
@@ -401,6 +421,7 @@ export class AgentBase {
|
|
|
401
421
|
this.#model = options.model;
|
|
402
422
|
this.#effort = options.effort;
|
|
403
423
|
this.#serviceTier = options.serviceTier;
|
|
424
|
+
this.#permissionMode = options.permissionMode ?? DEFAULT_AGENT_PERMISSION_MODE;
|
|
404
425
|
this.#kv = new AgentKV(this.#persistence, `kv.${options.id}.`);
|
|
405
426
|
this.#runKV = this.#kv.scoped("run");
|
|
406
427
|
// Everything the agent does — hooks and tool executions included — runs on a context
|
|
@@ -415,14 +436,19 @@ export class AgentBase {
|
|
|
415
436
|
* the selection changes.
|
|
416
437
|
*/
|
|
417
438
|
#deriveCtx() {
|
|
418
|
-
const ctx = withAgentContext(this.#baseCtx,
|
|
439
|
+
const ctx = withAgentContext(this.#baseCtx, this.#selection());
|
|
440
|
+
return withAgentRunKV(withAgentKV(ctx, this.#kv), this.#runKV);
|
|
441
|
+
}
|
|
442
|
+
/** Everything about what the agent is currently running on, as one value to carry. */
|
|
443
|
+
#selection() {
|
|
444
|
+
return {
|
|
419
445
|
id: this.id,
|
|
420
446
|
provider: this.#providerId,
|
|
421
447
|
model: this.#model,
|
|
422
448
|
effort: this.#effort,
|
|
423
449
|
serviceTier: this.#serviceTier,
|
|
424
|
-
|
|
425
|
-
|
|
450
|
+
permissionMode: this.#permissionMode,
|
|
451
|
+
};
|
|
426
452
|
}
|
|
427
453
|
/**
|
|
428
454
|
* Whether the agent has anything left to do. This is the only thing about an agent's state
|
|
@@ -1209,9 +1235,9 @@ export class AgentBase {
|
|
|
1209
1235
|
this.#emit({ type: "done", state: "cancelled" });
|
|
1210
1236
|
break;
|
|
1211
1237
|
}
|
|
1212
|
-
let injected = await this.#consumeQueue(this.#steering, this.#steeringMode, "steering
|
|
1238
|
+
let injected = await this.#consumeQueue(this.#steering, this.#steeringMode, "steering");
|
|
1213
1239
|
if (!injected && !needsInference) {
|
|
1214
|
-
injected = await this.#consumeQueue(this.#sends, this.#sendMode, "send
|
|
1240
|
+
injected = await this.#consumeQueue(this.#sends, this.#sendMode, "send");
|
|
1215
1241
|
}
|
|
1216
1242
|
// Nothing to answer — a start() on an idle history, or the queues ran dry.
|
|
1217
1243
|
if (!injected && !needsInference)
|
|
@@ -1457,6 +1483,10 @@ export class AgentBase {
|
|
|
1457
1483
|
await this.#recordTransaction(lockCtx, async (txCtx) => {
|
|
1458
1484
|
for (const result of results) {
|
|
1459
1485
|
await this.#appendRecord(txCtx, { type: "tool", message: result });
|
|
1486
|
+
// A result the conversation records is a result the hook sees, however
|
|
1487
|
+
// little of a run produced it. A hook that fails here leaves the calls
|
|
1488
|
+
// unsettled, which is what lets a later attempt answer them properly.
|
|
1489
|
+
await this.#invokeToolTransactHook(txCtx, result.callId, this.#hooks.afterToolCallTransact, result);
|
|
1460
1490
|
}
|
|
1461
1491
|
});
|
|
1462
1492
|
this.#messages.push(...results);
|
|
@@ -1521,9 +1551,17 @@ export class AgentBase {
|
|
|
1521
1551
|
* Move the oldest queued message — or, in "all" mode, every queued message — into the main
|
|
1522
1552
|
* context store and the in-memory history. The moves run in one transaction, so a message
|
|
1523
1553
|
* is never durable in both stores or neither, and memory changes only after the commit.
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1554
|
+
*
|
|
1555
|
+
* What the consumption has to announce is announced once the lock has been released. A hook
|
|
1556
|
+
* told a message has landed may perfectly well answer by sending another one, and doing that
|
|
1557
|
+
* while this still held the store lock would be the hook waiting for its own caller.
|
|
1558
|
+
*/
|
|
1559
|
+
async #consumeQueue(queue, mode, kind) {
|
|
1560
|
+
const prefix = `${kind}.`;
|
|
1561
|
+
/** Filled in once the consumption has committed, and reported after the lock is released. */
|
|
1562
|
+
const accepted = [];
|
|
1563
|
+
let permissionChange;
|
|
1564
|
+
const consumed = await this.#persistenceLock.runInLock(this.#ctx, async (lockCtx) => {
|
|
1527
1565
|
if (queue.length === 0)
|
|
1528
1566
|
return false;
|
|
1529
1567
|
// The durable queue, not memory, decides what is left to consume after a restart.
|
|
@@ -1542,6 +1580,7 @@ export class AgentBase {
|
|
|
1542
1580
|
let model = this.#model;
|
|
1543
1581
|
let effort = this.#effort;
|
|
1544
1582
|
let serviceTier = this.#serviceTier;
|
|
1583
|
+
let permissionMode = this.#permissionMode;
|
|
1545
1584
|
let changed = false;
|
|
1546
1585
|
for (const entry of batch) {
|
|
1547
1586
|
if (entry.options.provider !== undefined) {
|
|
@@ -1560,7 +1599,17 @@ export class AgentBase {
|
|
|
1560
1599
|
serviceTier = entry.options.serviceTier;
|
|
1561
1600
|
changed = true;
|
|
1562
1601
|
}
|
|
1602
|
+
if (entry.options.permissionMode !== undefined) {
|
|
1603
|
+
permissionMode = entry.options.permissionMode;
|
|
1604
|
+
changed = true;
|
|
1605
|
+
}
|
|
1563
1606
|
}
|
|
1607
|
+
// The mode the messages make effective, kept apart from the rest because it is the one
|
|
1608
|
+
// setting with hooks of its own: a change is announced, and what a feature concludes
|
|
1609
|
+
// from it commits with the message that carried it.
|
|
1610
|
+
const modeChange = permissionMode === this.#permissionMode
|
|
1611
|
+
? undefined
|
|
1612
|
+
: { previousMode: this.#permissionMode, mode: permissionMode };
|
|
1564
1613
|
// A provider or model change is checked against the provider-model compatibility
|
|
1565
1614
|
// matrix. An incompatible change resets the conversation: the history is erased
|
|
1566
1615
|
// completely, the old provider session is destroyed, and the `modelChanged` hook
|
|
@@ -1610,6 +1659,7 @@ export class AgentBase {
|
|
|
1610
1659
|
model,
|
|
1611
1660
|
effort,
|
|
1612
1661
|
serviceTier,
|
|
1662
|
+
permissionMode,
|
|
1613
1663
|
}), committed.signal);
|
|
1614
1664
|
const changeCtx = withAgentRunKV(withAgentKV(changeLifetime, this.#kv), this.#runKV);
|
|
1615
1665
|
try {
|
|
@@ -1670,13 +1720,28 @@ export class AgentBase {
|
|
|
1670
1720
|
...(model === undefined ? {} : { model }),
|
|
1671
1721
|
...(effort === undefined ? {} : { effort }),
|
|
1672
1722
|
...(serviceTier === undefined ? {} : { serviceTier }),
|
|
1723
|
+
permissionMode,
|
|
1673
1724
|
});
|
|
1674
1725
|
}
|
|
1675
1726
|
// Consuming a message is precisely the act that makes an inference owed, so
|
|
1676
1727
|
// the two commit as one. A crash cannot land between them and leave a
|
|
1677
1728
|
// message in the conversation that nothing remembers having to answer.
|
|
1678
1729
|
await this.#recordPending(txCtx, { stage: "inference" });
|
|
1730
|
+
// Last, so a hook writing its own account of the consumption sees a transaction
|
|
1731
|
+
// holding all of it. The mode comes before the messages: it is what they were
|
|
1732
|
+
// said under, and a listener recording them wants to know that first.
|
|
1733
|
+
const selection = { provider, model, effort, serviceTier, permissionMode };
|
|
1734
|
+
if (modeChange !== undefined) {
|
|
1735
|
+
await this.#invokeTransactHook(txCtx, selection, this.#hooks.permissionModeChangedTransact, modeChange);
|
|
1736
|
+
}
|
|
1737
|
+
for (const entry of batch) {
|
|
1738
|
+
await this.#invokeTransactHook(txCtx, selection, this.#hooks.messageAcceptedTransact, { kind, message: entry.message });
|
|
1739
|
+
}
|
|
1679
1740
|
});
|
|
1741
|
+
// Committed: from here the messages are part of the conversation, so what has to be
|
|
1742
|
+
// announced about them is decided now and reported once the lock is released.
|
|
1743
|
+
permissionChange = modeChange;
|
|
1744
|
+
accepted.push(...batch.map((entry) => ({ kind, message: entry.message })));
|
|
1680
1745
|
queue.splice(0, count);
|
|
1681
1746
|
if (reset) {
|
|
1682
1747
|
this.#messages = injected === undefined ? [] : [injected];
|
|
@@ -1696,10 +1761,31 @@ export class AgentBase {
|
|
|
1696
1761
|
this.#model = model;
|
|
1697
1762
|
this.#effort = effort;
|
|
1698
1763
|
this.#serviceTier = serviceTier;
|
|
1764
|
+
this.#permissionMode = permissionMode;
|
|
1699
1765
|
this.#ctx = this.#deriveCtx();
|
|
1700
1766
|
}
|
|
1701
1767
|
return true;
|
|
1702
1768
|
});
|
|
1769
|
+
// Outside the lock, and on the agent's own context, which now carries whatever these
|
|
1770
|
+
// messages made effective.
|
|
1771
|
+
if (permissionChange !== undefined) {
|
|
1772
|
+
await this.#invokeHook(this.#hooks.permissionModeChanged, permissionChange);
|
|
1773
|
+
}
|
|
1774
|
+
for (const message of accepted) {
|
|
1775
|
+
await this.#invokeHook(this.#hooks.messageAccepted, message);
|
|
1776
|
+
}
|
|
1777
|
+
return consumed;
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Call a hook that writes inside the consumption's transaction, on a context carrying the
|
|
1781
|
+
* selection those messages made effective rather than the one they replaced. Its failure is
|
|
1782
|
+
* not contained: it rolls the whole consumption back, leaving the messages queued.
|
|
1783
|
+
*/
|
|
1784
|
+
async #invokeTransactHook(txCtx, selection, hook, argument) {
|
|
1785
|
+
if (hook === undefined)
|
|
1786
|
+
return;
|
|
1787
|
+
const hookCtx = withAgentContext(txCtx, { id: this.id, ...selection });
|
|
1788
|
+
await this.#withTransactionalContext(hookCtx, (liveCtx) => hook(liveCtx, argument));
|
|
1703
1789
|
}
|
|
1704
1790
|
/**
|
|
1705
1791
|
* Replace the in-memory state with the durable one. The persistence lock guarantees every
|
|
@@ -1738,6 +1824,13 @@ export class AgentBase {
|
|
|
1738
1824
|
this.#model = persisted.model;
|
|
1739
1825
|
this.#effort = persisted.effort;
|
|
1740
1826
|
this.#serviceTier = persisted.serviceTier;
|
|
1827
|
+
// The permission mode is the one setting whose absence is not a decision: a record
|
|
1828
|
+
// written before any message carried a mode says nothing about it, and a value
|
|
1829
|
+
// that is not a mode at all says nothing either. Both keep the mode the agent was
|
|
1830
|
+
// built with rather than running under something nothing can interpret.
|
|
1831
|
+
if (isAgentPermissionMode(persisted.permissionMode)) {
|
|
1832
|
+
this.#permissionMode = persisted.permissionMode;
|
|
1833
|
+
}
|
|
1741
1834
|
this.#ctx = this.#deriveCtx();
|
|
1742
1835
|
}
|
|
1743
1836
|
this.#pendingTools = pendingTools.map(({ key, value }) => ({
|
|
@@ -1782,6 +1875,11 @@ export class AgentBase {
|
|
|
1782
1875
|
// then never find calls owed with no record of a run owing them, nor a run
|
|
1783
1876
|
// recorded as running tools that were never written.
|
|
1784
1877
|
await this.#recordPending(txCtx, { stage: "tools" });
|
|
1878
|
+
// Last, so a hook noting a call about to happen sees a transaction holding
|
|
1879
|
+
// the whole batch it belongs to.
|
|
1880
|
+
for (const entry of entries) {
|
|
1881
|
+
await this.#invokeToolTransactHook(txCtx, entry.call.callId, this.#hooks.beforeToolCallTransact, entry.call);
|
|
1882
|
+
}
|
|
1785
1883
|
}));
|
|
1786
1884
|
}
|
|
1787
1885
|
else {
|
|
@@ -1817,6 +1915,7 @@ export class AgentBase {
|
|
|
1817
1915
|
// stays: that is the tool's state, not the batch's bookkeeping, and
|
|
1818
1916
|
// an owner may still want to read what a finished call recorded.
|
|
1819
1917
|
await this.#persistence.deleteValue(txCtx, entry.key);
|
|
1918
|
+
await this.#invokeToolTransactHook(txCtx, entry.call.callId, this.#hooks.afterToolCallTransact, result);
|
|
1820
1919
|
});
|
|
1821
1920
|
this.#messages.push(result);
|
|
1822
1921
|
committed += 1;
|
|
@@ -1913,6 +2012,30 @@ export class AgentBase {
|
|
|
1913
2012
|
#toolKey(index, callId) {
|
|
1914
2013
|
return `tool.${String(index).padStart(6, "0")}.${callId}`;
|
|
1915
2014
|
}
|
|
2015
|
+
/**
|
|
2016
|
+
* The scope one call owns: state persists under its own call ID, never in another call's
|
|
2017
|
+
* scope, and the task context ends where the call was made. The execution and all four tool
|
|
2018
|
+
* hooks share it, so what one of them writes about a call is where the others look for it.
|
|
2019
|
+
*/
|
|
2020
|
+
#callScoped(ctx, callId) {
|
|
2021
|
+
return withAgentTaskContext(withAgentRunKV(withAgentKV(ctx, this.#kv.scoped("call", callId)), this.#runKV.scoped("call", callId)), taskContextBeforeToolCall(this.#messages, callId));
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Call a tool hook that writes inside a transaction of its own call's. The lifetime ends with
|
|
2025
|
+
* the callback, so a context kept afterwards cannot outlive the transaction it belongs to.
|
|
2026
|
+
* Its failure is not contained: it rolls that transaction back.
|
|
2027
|
+
*/
|
|
2028
|
+
async #invokeToolTransactHook(txCtx, callId, hook, argument) {
|
|
2029
|
+
if (hook === undefined)
|
|
2030
|
+
return;
|
|
2031
|
+
const lifetime = new AbortController();
|
|
2032
|
+
try {
|
|
2033
|
+
await hook(this.#callScoped(withLifetime(txCtx, lifetime.signal), callId), argument);
|
|
2034
|
+
}
|
|
2035
|
+
finally {
|
|
2036
|
+
lifetime.abort();
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
1916
2039
|
/**
|
|
1917
2040
|
* Run one tool call; every failure becomes an error tool result instead of an exception.
|
|
1918
2041
|
* The context carries the turn's abort signal as its lifetime, so a running tool can
|
|
@@ -1942,34 +2065,77 @@ export class AgentBase {
|
|
|
1942
2065
|
if (tool.parameters !== undefined && !Value.Check(tool.parameters, args)) {
|
|
1943
2066
|
return failure(`The arguments for "${call.name}" did not match its schema.`);
|
|
1944
2067
|
}
|
|
2068
|
+
const callCtx = this.#callScoped(ctx, call.callId);
|
|
2069
|
+
// From here the call is one the two tool hooks bracket: a tool that exists, a call that
|
|
2070
|
+
// finished, and arguments its schema accepts. A call refused before that reaches neither
|
|
2071
|
+
// hook, because there is nothing yet to decide about or to report.
|
|
2072
|
+
let ran = tool;
|
|
2073
|
+
let ranArguments = args;
|
|
2074
|
+
let outcome;
|
|
1945
2075
|
try {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2076
|
+
const decision = await this.#hooks.beforeToolCall?.(callCtx, {
|
|
2077
|
+
callId: call.callId,
|
|
2078
|
+
tool,
|
|
2079
|
+
arguments: args,
|
|
2080
|
+
});
|
|
2081
|
+
if (decision?.type === "answer") {
|
|
2082
|
+
// The hook answered the model itself, so the tool never runs and there is no
|
|
2083
|
+
// structured result — only what the model is told.
|
|
2084
|
+
outcome = {
|
|
1953
2085
|
callId: call.callId,
|
|
1954
2086
|
tool,
|
|
1955
2087
|
arguments: args,
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2088
|
+
content: [...decision.content],
|
|
2089
|
+
isError: decision.isError === true,
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
else {
|
|
2093
|
+
if (decision?.tool !== undefined)
|
|
2094
|
+
ran = decision.tool;
|
|
2095
|
+
if (decision?.arguments !== undefined)
|
|
2096
|
+
ranArguments = decision.arguments;
|
|
2097
|
+
// An amended call is validated again: the schema that mattered is the one belonging
|
|
2098
|
+
// to the tool that is about to run, on the arguments it is about to receive.
|
|
2099
|
+
if ((ran !== tool || ranArguments !== args) &&
|
|
2100
|
+
ran.parameters !== undefined &&
|
|
2101
|
+
!Value.Check(ran.parameters, ranArguments)) {
|
|
2102
|
+
throw new Error(`The arguments for "${ran.name}" did not match its schema.`);
|
|
2103
|
+
}
|
|
2104
|
+
const runCtx = decision?.permissionMode === undefined
|
|
2105
|
+
? callCtx
|
|
2106
|
+
: withAgentPermissionMode(callCtx, decision.permissionMode);
|
|
2107
|
+
const result = await ran.execute(runCtx, ranArguments);
|
|
2108
|
+
if (!Value.Check(ran.returnType, result)) {
|
|
2109
|
+
throw new Error(`Tool "${ran.name}" returned an invalid result.`);
|
|
2110
|
+
}
|
|
2111
|
+
outcome = {
|
|
2112
|
+
callId: call.callId,
|
|
2113
|
+
tool: ran,
|
|
2114
|
+
arguments: ranArguments,
|
|
2115
|
+
content: [...ran.toLLM(result)],
|
|
2116
|
+
isError: ran.isError?.(result) === true,
|
|
2117
|
+
result,
|
|
2118
|
+
};
|
|
1960
2119
|
}
|
|
1961
|
-
const content = tool.toLLM(result);
|
|
1962
|
-
const isError = tool.isError?.(result) === true;
|
|
1963
|
-
return {
|
|
1964
|
-
role: "tool",
|
|
1965
|
-
callId: call.callId,
|
|
1966
|
-
content: [...content],
|
|
1967
|
-
...(isError ? { isError: true } : {}),
|
|
1968
|
-
};
|
|
1969
2120
|
}
|
|
1970
2121
|
catch (error) {
|
|
1971
|
-
|
|
2122
|
+
outcome = {
|
|
2123
|
+
callId: call.callId,
|
|
2124
|
+
tool: ran,
|
|
2125
|
+
arguments: ranArguments,
|
|
2126
|
+
content: [
|
|
2127
|
+
{ type: "text", text: error instanceof Error ? error.message : String(error) },
|
|
2128
|
+
],
|
|
2129
|
+
isError: true,
|
|
2130
|
+
};
|
|
1972
2131
|
}
|
|
2132
|
+
await this.#invokeHookOn(callCtx, this.#hooks.afterToolCall, outcome);
|
|
2133
|
+
return {
|
|
2134
|
+
role: "tool",
|
|
2135
|
+
callId: call.callId,
|
|
2136
|
+
content: outcome.content,
|
|
2137
|
+
...(outcome.isError ? { isError: true } : {}),
|
|
2138
|
+
};
|
|
1973
2139
|
}
|
|
1974
2140
|
/**
|
|
1975
2141
|
* A key that sorts after every entry the queue already holds. The order comes from the store
|