@stratusagent/core 0.13.1 → 0.13.3
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/index.d.ts +125 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +259 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -425,6 +425,37 @@ const originForSession = (tool, session, input) => {
|
|
|
425
425
|
const reported = tool.originFor?.(session, input);
|
|
426
426
|
return reported === undefined ? undefined : originOf(reported);
|
|
427
427
|
};
|
|
428
|
+
/** The longest a tool's `trustBeforeCall` may take before the call is judged as reading `external`. */
|
|
429
|
+
export const TRUST_BEFORE_CALL_TIMEOUT_MS = 2_000;
|
|
430
|
+
/**
|
|
431
|
+
* A tool's `trustBeforeCall` answer, bounded. A throw, a rejection, a label
|
|
432
|
+
* nobody recognises or no answer in time reads `external`: the call is then
|
|
433
|
+
* about to read something nobody can vouch for, and judging it as such is
|
|
434
|
+
* the safe direction. Nothing, for a tool that answers nothing.
|
|
435
|
+
*/
|
|
436
|
+
const trustBeforeCallOf = async (tool, session) => {
|
|
437
|
+
let timer;
|
|
438
|
+
try {
|
|
439
|
+
const label = await Promise.race([
|
|
440
|
+
Promise.resolve().then(() => tool.trustBeforeCall?.(session)),
|
|
441
|
+
new Promise((_, reject) => {
|
|
442
|
+
// Not unref'd: cleared below either way, and an unref'd bound lets a
|
|
443
|
+
// process with nothing else running exit mid-turn.
|
|
444
|
+
timer = setTimeout(() => reject(new Error('trustBeforeCall timed out')), TRUST_BEFORE_CALL_TIMEOUT_MS);
|
|
445
|
+
}),
|
|
446
|
+
]);
|
|
447
|
+
if (label === undefined) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
return isTrustLevel(label) ? label : 'external';
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return 'external';
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
clearTimeout(timer);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
428
459
|
/**
|
|
429
460
|
* Whether an agent's `tools:` allowlist permits a tool name.
|
|
430
461
|
*
|
|
@@ -1895,6 +1926,15 @@ export const promptTextOf = (message, options = {}) => {
|
|
|
1895
1926
|
const framed = `(overheard, not addressed to you)\n${quoted}`;
|
|
1896
1927
|
return options.latest === true ? `${framed}\n\n${UNADDRESSED_TURN_NOTE}` : framed;
|
|
1897
1928
|
};
|
|
1929
|
+
/**
|
|
1930
|
+
* What separates an overheard message's framed text from its images. The
|
|
1931
|
+
* contract every provider keeps for a message with `overheard` and
|
|
1932
|
+
* images: the text from `promptTextOf` first, then this, then the images,
|
|
1933
|
+
* so pixels never reach a model ahead of the frame that says nobody spoke
|
|
1934
|
+
* to it, and read as that message's attachments rather than as something
|
|
1935
|
+
* the agent was sent. Like the frame, it states the fact, not advice.
|
|
1936
|
+
*/
|
|
1937
|
+
export const OVERHEARD_IMAGES_NOTE = '(attached to the overheard message above, not addressed to you)';
|
|
1898
1938
|
/**
|
|
1899
1939
|
* Whether the turn a session is on is one nobody asked for: its newest
|
|
1900
1940
|
* user message is overheard, which only `addressed: false` produces at
|
|
@@ -2751,6 +2791,64 @@ const MEMORY_RECENT_INTRO = 'Recently remembered:';
|
|
|
2751
2791
|
// renders one item per line.
|
|
2752
2792
|
const renderMemoryTopicLine = (topic) => `- ${escapeControlCharacters(topic.name)} (${topic.count} fact${topic.count === 1 ? '' : 's'},`
|
|
2753
2793
|
+ ` last ${escapeControlCharacters(topic.lastUpdatedAt.slice(0, 10))})`;
|
|
2794
|
+
// A date alone is UTC midnight by specification, so it is unambiguous
|
|
2795
|
+
// wherever the daemon runs. A *time* without an offset is not: it reads as
|
|
2796
|
+
// the host's local zone, so the same requested bound would become a
|
|
2797
|
+
// different instant in Los Angeles than in UTC, and a fact would activate
|
|
2798
|
+
// or expire at a deployment-dependent hour. So the offset is required
|
|
2799
|
+
// whenever a time is given.
|
|
2800
|
+
const ISO_8601_INSTANT = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2}))?$/;
|
|
2801
|
+
/**
|
|
2802
|
+
* Whether the `YYYY-MM-DD` a bound opens with is a day that exists.
|
|
2803
|
+
*
|
|
2804
|
+
* `Date.parse` normalizes rather than refusing: `2026-02-30` parses fine and
|
|
2805
|
+
* becomes March 2nd, and `2026-02-29` becomes March 1st in a non-leap year.
|
|
2806
|
+
* The regex cannot see it either — both are ISO-*shaped*. A validity window
|
|
2807
|
+
* is the field where that matters most and shows least: the fact starts or
|
|
2808
|
+
* stops being true days from where the writer asked, and the record says
|
|
2809
|
+
* only what it was normalized to.
|
|
2810
|
+
*
|
|
2811
|
+
* Checked on the calendar components as written, never by comparing the
|
|
2812
|
+
* normalized instant back to the string — that was the first attempt at
|
|
2813
|
+
* this and it rejected `2026-04-01T23:00:00-05:00`, whose UTC date is
|
|
2814
|
+
* legitimately the 2nd. An offset is allowed to move the day; February 30th
|
|
2815
|
+
* is not a day.
|
|
2816
|
+
*
|
|
2817
|
+
* `setUTCFullYear` rather than `Date.UTC`, which maps a two-digit year into
|
|
2818
|
+
* the 1900s — the regex admits `0026-02-28`, and that is a real date.
|
|
2819
|
+
*/
|
|
2820
|
+
const isRealCalendarDate = (raw) => {
|
|
2821
|
+
const [year, month, day] = raw.slice(0, 10).split('-').map(Number);
|
|
2822
|
+
const probe = new Date(0);
|
|
2823
|
+
probe.setUTCFullYear(year, month - 1, day);
|
|
2824
|
+
return probe.getUTCFullYear() === year && probe.getUTCMonth() === month - 1 && probe.getUTCDate() === day;
|
|
2825
|
+
};
|
|
2826
|
+
/**
|
|
2827
|
+
* Whether a stored time is an ISO-8601 instant that means one thing
|
|
2828
|
+
* everywhere: the shape above, a day that exists, and a value that parses.
|
|
2829
|
+
* The one reading of that rule, for `memory.remember`'s validity bounds and
|
|
2830
|
+
* for the times the memory block shows.
|
|
2831
|
+
*/
|
|
2832
|
+
export const isIsoInstant = (raw) => ISO_8601_INSTANT.test(raw) && !Number.isNaN(Date.parse(raw)) && isRealCalendarDate(raw);
|
|
2833
|
+
/**
|
|
2834
|
+
* A stored time as `YYYY-MM-DD HH:MMZ`, or nothing when it isn't an
|
|
2835
|
+
* unambiguous instant (`isIsoInstant`): `Date` would turn `2026-02-30` into
|
|
2836
|
+
* March 2 and read an offset-less time in the host's zone. Rebuilt from the
|
|
2837
|
+
* parsed instant rather than sliced from the string, so a hand-edited
|
|
2838
|
+
* `createdAt` can only ever render as a time, never as text.
|
|
2839
|
+
*/
|
|
2840
|
+
const utcMinute = (value) => {
|
|
2841
|
+
if (typeof value !== 'string' || !isIsoInstant(value)) {
|
|
2842
|
+
return undefined;
|
|
2843
|
+
}
|
|
2844
|
+
const iso = new Date(value).toISOString();
|
|
2845
|
+
// An offset can carry year 0000 or 9999 across a UTC year boundary, where
|
|
2846
|
+
// toISOString switches to the six-digit form (Codex P2 4349257583).
|
|
2847
|
+
if (!/^\d{4}-/.test(iso)) {
|
|
2848
|
+
return undefined;
|
|
2849
|
+
}
|
|
2850
|
+
return `${iso.slice(0, 10)} ${iso.slice(11, 16)}Z`;
|
|
2851
|
+
};
|
|
2754
2852
|
/**
|
|
2755
2853
|
* The memory block: the pinned core, the topic index, and the recency tail,
|
|
2756
2854
|
* each grouped into trust regions, joined into **one** section.
|
|
@@ -2773,7 +2871,17 @@ export const renderMemorySection = (memory) => {
|
|
|
2773
2871
|
}
|
|
2774
2872
|
const injection = asMemoryInjection(memory);
|
|
2775
2873
|
const blocks = [];
|
|
2776
|
-
|
|
2874
|
+
// Each fact says when it was recorded, to the minute: a fact from three
|
|
2875
|
+
// days ago otherwise reads exactly like one from this turn, and live
|
|
2876
|
+
// state (a PR, a deploy) changes within a day. The model is shown
|
|
2877
|
+
// `createdAt` here because it never called recall for these. Stored
|
|
2878
|
+
// times only, never the clock: on the default Anthropic model this
|
|
2879
|
+
// section rides in the system block, which must change only when a
|
|
2880
|
+
// memory does, or every turn's thinking is dropped as a prefix mismatch.
|
|
2881
|
+
const fact = (entry) => {
|
|
2882
|
+
const at = utcMinute(entry.createdAt);
|
|
2883
|
+
return `- ${at ? `(${at}) ` : ''}${escapeControlCharacters(entry.content)}`;
|
|
2884
|
+
};
|
|
2777
2885
|
const push = (intro, regions) => {
|
|
2778
2886
|
if (regions.length > 0) {
|
|
2779
2887
|
blocks.push(`${intro}\n\n${regions.join('\n\n')}`);
|
|
@@ -2805,6 +2913,86 @@ export const renderSkillsSection = (skills) => {
|
|
|
2805
2913
|
});
|
|
2806
2914
|
return `You have skills — procedures for doing particular tasks well. When one is relevant to the task at hand, load its full instructions with the ${SKILL_READ_TOOL_NAME} tool (pass the id) and follow them; the one-line descriptions here are pointers, not the procedures themselves:\n${lines.join('\n')}`;
|
|
2807
2915
|
};
|
|
2916
|
+
/**
|
|
2917
|
+
* How hard a Claude model works on a turn, lowest first: the API's
|
|
2918
|
+
* `output_config.effort` and the Agent SDK's `effort` option. A soul's
|
|
2919
|
+
* `effort:` sets it per agent; unset sends nothing, which leaves each
|
|
2920
|
+
* model on its own default.
|
|
2921
|
+
*/
|
|
2922
|
+
export const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
2923
|
+
export const isEffortLevel = (value) => typeof value === 'string' && EFFORT_LEVELS.includes(value);
|
|
2924
|
+
/**
|
|
2925
|
+
* The effort levels a Claude model takes, or `undefined` for a model this
|
|
2926
|
+
* table does not know (a proxy's name, a model newer than this release).
|
|
2927
|
+
* An empty list is a model known to take no effort at all.
|
|
2928
|
+
*
|
|
2929
|
+
* Kept to families the effort docs name. A model missing here is sent the
|
|
2930
|
+
* configured value as written and reported as unverified, never as applied.
|
|
2931
|
+
*/
|
|
2932
|
+
/**
|
|
2933
|
+
* A Claude model id with its other established names folded away: a
|
|
2934
|
+
* Bedrock id (`anthropic.` behind any region or `global.` qualifier, a
|
|
2935
|
+
* `-v1:0` suffix) and a Vertex one (`@` and a date), lowercased. Every
|
|
2936
|
+
* check on a model family goes through this, so one that recognizes a
|
|
2937
|
+
* name and another that misses it cannot disagree about the same model.
|
|
2938
|
+
*/
|
|
2939
|
+
export const claudeModelBase = (model) => model.trim().toLowerCase().replace(/^(?:[a-z0-9-]+\.)*anthropic\./, '').split('@')[0].replace(/-v\d+(?::\d+)?$/, '');
|
|
2940
|
+
/**
|
|
2941
|
+
* The highest effort a Claude model takes with thinking disabled, for the
|
|
2942
|
+
* models the effort docs name a ceiling for (Claude Opus 5 and Claude
|
|
2943
|
+
* Haiku 5.5: `high`; above it the API answers 400). `undefined` is no
|
|
2944
|
+
* known ceiling. Kept beside `effortLevelsFor` so both read one id.
|
|
2945
|
+
*/
|
|
2946
|
+
export const disabledThinkingEffortCeiling = (model) => {
|
|
2947
|
+
const id = claudeModelBase(model);
|
|
2948
|
+
return /^claude-opus-5(-|$)/.test(id) || /^claude-haiku-5-5(-|$)/.test(id) ? 'high' : undefined;
|
|
2949
|
+
};
|
|
2950
|
+
export const effortLevelsFor = (model) => {
|
|
2951
|
+
// Missed, Opus 4.6 under its Bedrock name asked for xhigh would be sent
|
|
2952
|
+
// xhigh as unverified and refused.
|
|
2953
|
+
const id = claudeModelBase(model);
|
|
2954
|
+
if (/^claude-(opus|sonnet|fable|mythos)-5(-|$)/.test(id) || /^claude-haiku-5-5(-|$)/.test(id) || /^claude-opus-4-[78](-|$)/.test(id)) {
|
|
2955
|
+
return EFFORT_LEVELS;
|
|
2956
|
+
}
|
|
2957
|
+
// Mythos Preview takes max but not xhigh, like the 4.6 models.
|
|
2958
|
+
if (/^claude-(opus|sonnet)-4-6(-|$)/.test(id) || /^claude-mythos-preview(-|$)/.test(id)) {
|
|
2959
|
+
return ['low', 'medium', 'high', 'max'];
|
|
2960
|
+
}
|
|
2961
|
+
if (/^claude-opus-4-5(-|$)/.test(id)) {
|
|
2962
|
+
return ['low', 'medium', 'high'];
|
|
2963
|
+
}
|
|
2964
|
+
if (/^claude-(haiku-4-5|sonnet-4-5|(sonnet|opus)-4(-20\d{6})?$|opus-4-[01](-|$)|3)/.test(id)) {
|
|
2965
|
+
return [];
|
|
2966
|
+
}
|
|
2967
|
+
return undefined;
|
|
2968
|
+
};
|
|
2969
|
+
export const resolveEffort = (model, requested) => {
|
|
2970
|
+
const levels = effortLevelsFor(model);
|
|
2971
|
+
if (levels === undefined) {
|
|
2972
|
+
return { requested, effort: requested, status: 'unverified' };
|
|
2973
|
+
}
|
|
2974
|
+
if (levels.includes(requested)) {
|
|
2975
|
+
return { requested, effort: requested, status: 'applied' };
|
|
2976
|
+
}
|
|
2977
|
+
const below = EFFORT_LEVELS.slice(0, EFFORT_LEVELS.indexOf(requested)).filter((level) => levels.includes(level));
|
|
2978
|
+
const lowered = below.at(-1);
|
|
2979
|
+
return lowered === undefined
|
|
2980
|
+
? { requested, status: 'unsupported' }
|
|
2981
|
+
: { requested, effort: lowered, status: 'lowered' };
|
|
2982
|
+
};
|
|
2983
|
+
/** One line for logs and diagnostics: `high`, `high (asked xhigh; claude-opus-4-6 has no xhigh)`. */
|
|
2984
|
+
export const describeEffort = (model, resolution) => {
|
|
2985
|
+
switch (resolution.status) {
|
|
2986
|
+
case 'applied':
|
|
2987
|
+
return resolution.requested;
|
|
2988
|
+
case 'lowered':
|
|
2989
|
+
return `${resolution.effort} (asked ${resolution.requested}; ${model} has no ${resolution.requested})`;
|
|
2990
|
+
case 'unsupported':
|
|
2991
|
+
return `not sent (asked ${resolution.requested}; ${model} takes no effort setting)`;
|
|
2992
|
+
case 'unverified':
|
|
2993
|
+
return `${resolution.requested} (sent; ${model} is not a model Stratus knows the effort levels of)`;
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2808
2996
|
/**
|
|
2809
2997
|
* The language agents write in when nothing says otherwise: American
|
|
2810
2998
|
* English. A soul's `language:` and the config file's `language` override
|
|
@@ -3226,16 +3414,6 @@ export class AllowAllApprovalPolicy {
|
|
|
3226
3414
|
* written after read the same.
|
|
3227
3415
|
*/
|
|
3228
3416
|
const userImages = (images) => images !== undefined && images.length > 0 ? { images } : {};
|
|
3229
|
-
/**
|
|
3230
|
-
* See `RunInput.addressed`: an image reaches the model as pixels, before
|
|
3231
|
-
* and outside the text that frames what was said as somebody else's, so
|
|
3232
|
-
* a turn nobody asked for takes none.
|
|
3233
|
-
*/
|
|
3234
|
-
const refuseUnaddressedImages = (input) => {
|
|
3235
|
-
if (input.addressed === false && input.images !== undefined && input.images.length > 0) {
|
|
3236
|
-
throw new Error('A turn nobody asked for cannot carry images: an image enters the prompt ahead of the frame that marks the message as somebody else\'s. Name the attachment in the message instead.');
|
|
3237
|
-
}
|
|
3238
|
-
};
|
|
3239
3417
|
/**
|
|
3240
3418
|
* Thrown by a provider when the request did not fit the model's context
|
|
3241
3419
|
* window — the transcript, not the answer, is what was too long.
|
|
@@ -3561,7 +3739,6 @@ export class AgentRunner {
|
|
|
3561
3739
|
await this.plugins.loadAll({ bus: this.bus, tools: this.tools });
|
|
3562
3740
|
}
|
|
3563
3741
|
async run(input) {
|
|
3564
|
-
refuseUnaddressedImages(input);
|
|
3565
3742
|
// The sender's trust is the turn's, not the session's: it is read here
|
|
3566
3743
|
// and kept out of the persisted metadata, where a later reader would
|
|
3567
3744
|
// take the first sender's label for every turn that follows.
|
|
@@ -3648,7 +3825,6 @@ export class AgentRunner {
|
|
|
3648
3825
|
}
|
|
3649
3826
|
}
|
|
3650
3827
|
async resume(input) {
|
|
3651
|
-
refuseUnaddressedImages(input);
|
|
3652
3828
|
const session = await this.store.get(input.sessionId);
|
|
3653
3829
|
if (!session) {
|
|
3654
3830
|
throw new Error(`Session not found: ${input.sessionId}`);
|
|
@@ -4575,6 +4751,19 @@ export class AgentRunner {
|
|
|
4575
4751
|
if (problem) {
|
|
4576
4752
|
return rejected(`Invalid input for ${call.toolName}: ${problem}. Fix the call and try again.`);
|
|
4577
4753
|
}
|
|
4754
|
+
// A call the tool already knows it will refuse, answered here for the
|
|
4755
|
+
// same reason: nobody should be asked to approve it. A hook that
|
|
4756
|
+
// throws is no answer, and the call goes on to be judged as before.
|
|
4757
|
+
let refusal;
|
|
4758
|
+
try {
|
|
4759
|
+
refusal = await tool.refusalFor?.(call.input, session);
|
|
4760
|
+
}
|
|
4761
|
+
catch {
|
|
4762
|
+
refusal = undefined;
|
|
4763
|
+
}
|
|
4764
|
+
if (typeof refusal === 'string' && refusal.trim().length > 0) {
|
|
4765
|
+
return rejected(refusal.trim());
|
|
4766
|
+
}
|
|
4578
4767
|
}
|
|
4579
4768
|
// Only a call that can actually be held for a human is checkpointed:
|
|
4580
4769
|
// a `safe` one is never asked about, and writing a record for it would
|
|
@@ -4617,17 +4806,52 @@ export class AgentRunner {
|
|
|
4617
4806
|
* unaffected.
|
|
4618
4807
|
*/
|
|
4619
4808
|
let originWhenJudged;
|
|
4809
|
+
// What the call may read, lowered into the session before the policy
|
|
4810
|
+
// reads the session's label, so the decision covers this call and not
|
|
4811
|
+
// only the next one. Asked again after an approval, because a human's
|
|
4812
|
+
// answer can take minutes and the label can drop in that time; one
|
|
4813
|
+
// re-decision, then a refusal, so a label that keeps moving cannot
|
|
4814
|
+
// hold the call in a loop of questions.
|
|
4815
|
+
const lowerBeforeCall = async () => {
|
|
4816
|
+
if (!tool.trustBeforeCall) {
|
|
4817
|
+
return undefined;
|
|
4818
|
+
}
|
|
4819
|
+
const label = await trustBeforeCallOf(tool, session);
|
|
4820
|
+
if (label !== undefined) {
|
|
4821
|
+
await this.taint(session, label, call.toolName);
|
|
4822
|
+
}
|
|
4823
|
+
return label;
|
|
4824
|
+
};
|
|
4620
4825
|
let approved;
|
|
4826
|
+
let decidedTrust = sessionTrustOf(session);
|
|
4827
|
+
let movedTwice = false;
|
|
4621
4828
|
try {
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4829
|
+
await lowerBeforeCall();
|
|
4830
|
+
const decide = async () => {
|
|
4831
|
+
decidedTrust = sessionTrustOf(session);
|
|
4832
|
+
const answer = await this.approvals.approve({
|
|
4833
|
+
session,
|
|
4834
|
+
call,
|
|
4835
|
+
tool,
|
|
4836
|
+
risk,
|
|
4837
|
+
...(signal ? { signal } : {}),
|
|
4838
|
+
...(options.parkedAt ? { parkedAt: options.parkedAt } : {}),
|
|
4839
|
+
});
|
|
4840
|
+
// With nothing awaited since the policy returned (see above).
|
|
4841
|
+
originWhenJudged = originForSession(tool, session, call.input);
|
|
4842
|
+
return answer;
|
|
4843
|
+
};
|
|
4844
|
+
approved = await decide();
|
|
4845
|
+
if (approved && tool.trustBeforeCall) {
|
|
4846
|
+
const dropped = async () => {
|
|
4847
|
+
const label = await lowerBeforeCall();
|
|
4848
|
+
return label !== undefined && leastTrusted(decidedTrust, label) !== decidedTrust;
|
|
4849
|
+
};
|
|
4850
|
+
if (await dropped()) {
|
|
4851
|
+
approved = await decide();
|
|
4852
|
+
movedTwice = approved && await dropped();
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4631
4855
|
}
|
|
4632
4856
|
finally {
|
|
4633
4857
|
// Cleared before anything executes and on every exit — an abort that
|
|
@@ -4637,6 +4861,18 @@ export class AgentRunner {
|
|
|
4637
4861
|
await this.checkpointPendingApproval(session, undefined);
|
|
4638
4862
|
}
|
|
4639
4863
|
}
|
|
4864
|
+
if (movedTwice) {
|
|
4865
|
+
await this.bus.emit({ type: 'tool.denied', sessionId: session.id, call });
|
|
4866
|
+
return {
|
|
4867
|
+
callId: call.id,
|
|
4868
|
+
toolName: call.toolName,
|
|
4869
|
+
ok: false,
|
|
4870
|
+
output: null,
|
|
4871
|
+
error: `${call.toolName} did not run: what it may read was marked less trusted twice while it was being approved. `
|
|
4872
|
+
+ 'Call again to have it judged as things stand now.',
|
|
4873
|
+
trust: 'agent',
|
|
4874
|
+
};
|
|
4875
|
+
}
|
|
4640
4876
|
if (!approved) {
|
|
4641
4877
|
await this.bus.emit({ type: 'tool.denied', sessionId: session.id, call });
|
|
4642
4878
|
return {
|
|
@@ -4675,6 +4911,7 @@ export class AgentRunner {
|
|
|
4675
4911
|
const collector = createImageCollector();
|
|
4676
4912
|
const executed = await this.executor.execute(call, tool, session, {
|
|
4677
4913
|
...(signal ? { signal } : {}),
|
|
4914
|
+
decidedTrust,
|
|
4678
4915
|
attachImage: collector.attach,
|
|
4679
4916
|
});
|
|
4680
4917
|
// Only a call that succeeded shows what it attached: a failure's error
|