@animalabs/agent-framework 0.22.0 → 0.22.1
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 +11 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +3 -1
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +41 -4
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/framework.d.ts +319 -20
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1500 -182
- package/dist/src/framework.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +2 -1
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +20 -9
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +29 -6
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +57 -16
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +28 -1
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +32 -4
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/module-registry.d.ts +9 -3
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +3 -2
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +46 -4
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +13 -3
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +24 -4
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +14 -0
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +10 -2
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/operator-log.d.ts +41 -0
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/provider-waits.d.ts +216 -0
- package/dist/src/provider-waits.d.ts.map +1 -0
- package/dist/src/provider-waits.js +482 -0
- package/dist/src/provider-waits.js.map +1 -0
- package/dist/src/refusal-reactions.d.ts +14 -12
- package/dist/src/refusal-reactions.d.ts.map +1 -1
- package/dist/src/refusal-reactions.js +14 -12
- package/dist/src/refusal-reactions.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +30 -13
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +71 -25
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/types/events.d.ts +12 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +12 -4
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +14 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/discord-awareness-outbox.test.js +3 -0
- package/dist/test/discord-awareness-outbox.test.js.map +1 -1
- package/dist/test/explicit-prose-routing.test.js +0 -2
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/framework.test.js +160 -1
- package/dist/test/framework.test.js.map +1 -1
- package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
- package/dist/test/helpers/coalescing-fixture.js +3 -2
- package/dist/test/helpers/coalescing-fixture.js.map +1 -1
- package/dist/test/inference-exhaustion-rewind.test.js +142 -14
- package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
- package/dist/test/inference-failure-bounds.test.d.ts +2 -0
- package/dist/test/inference-failure-bounds.test.d.ts.map +1 -0
- package/dist/test/inference-failure-bounds.test.js +1201 -0
- package/dist/test/inference-failure-bounds.test.js.map +1 -0
- package/dist/test/late-events-after-stop.test.d.ts +2 -0
- package/dist/test/late-events-after-stop.test.d.ts.map +1 -0
- package/dist/test/late-events-after-stop.test.js +122 -0
- package/dist/test/late-events-after-stop.test.js.map +1 -0
- package/dist/test/mcpl-awareness-barrier.test.js +141 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
- package/dist/test/mcpl-coalescing.test.js +78 -16
- package/dist/test/mcpl-coalescing.test.js.map +1 -1
- package/dist/test/mcpl-config-diagnostics.test.js +6 -3
- package/dist/test/mcpl-config-diagnostics.test.js.map +1 -1
- package/dist/test/mcpl-manifest-changed.test.js +2 -1
- package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
- package/dist/test/mcpl-reference-fetcher.test.js +232 -4
- package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
- package/dist/test/mcpl-reference-fields.test.js +69 -2
- package/dist/test/mcpl-reference-fields.test.js.map +1 -1
- package/dist/test/mcpl-ws-transport.test.js +103 -0
- package/dist/test/mcpl-ws-transport.test.js.map +1 -1
- package/dist/test/ops-alert.test.js +2 -1
- package/dist/test/ops-alert.test.js.map +1 -1
- package/dist/test/placed-reaction-baseline.test.d.ts +2 -0
- package/dist/test/placed-reaction-baseline.test.d.ts.map +1 -0
- package/dist/test/placed-reaction-baseline.test.js +312 -0
- package/dist/test/placed-reaction-baseline.test.js.map +1 -0
- package/dist/test/provider-acceleration-cooldown.test.js +5 -1
- package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
- package/dist/test/provider-waits.test.d.ts +2 -0
- package/dist/test/provider-waits.test.d.ts.map +1 -0
- package/dist/test/provider-waits.test.js +2196 -0
- package/dist/test/provider-waits.test.js.map +1 -0
- package/dist/test/reaction-baseline-platform.test.d.ts +2 -0
- package/dist/test/reaction-baseline-platform.test.d.ts.map +1 -0
- package/dist/test/reaction-baseline-platform.test.js +124 -0
- package/dist/test/reaction-baseline-platform.test.js.map +1 -0
- package/dist/test/refusal-reaction-baseline.test.js +10 -3
- package/dist/test/refusal-reaction-baseline.test.js.map +1 -1
- package/dist/test/save-recent-image.test.js +138 -2
- package/dist/test/save-recent-image.test.js.map +1 -1
- package/dist/test/surgery-marks-receipt.test.d.ts +2 -0
- package/dist/test/surgery-marks-receipt.test.d.ts.map +1 -0
- package/dist/test/surgery-marks-receipt.test.js +540 -0
- package/dist/test/surgery-marks-receipt.test.js.map +1 -0
- package/dist/test/tool-result-guard.test.js +221 -7
- package/dist/test/tool-result-guard.test.js.map +1 -1
- package/dist/test/workspace-three-way.test.js +115 -0
- package/dist/test/workspace-three-way.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/agent.ts +4 -1
- package/src/api/server.ts +43 -4
- package/src/framework.ts +1538 -189
- package/src/index.ts +7 -2
- package/src/mcpl/index.ts +2 -0
- package/src/mcpl/reference-fetcher.ts +18 -8
- package/src/mcpl/references.ts +57 -13
- package/src/mcpl/server-connection.ts +55 -4
- package/src/module-registry.ts +17 -9
- package/src/modules/workspace/index.ts +44 -4
- package/src/modules/workspace/observe.ts +31 -5
- package/src/modules/workspace/reconcile.ts +23 -2
- package/src/operator-log.ts +30 -0
- package/src/provider-waits.ts +523 -0
- package/src/refusal-reactions.ts +14 -12
- package/src/tool-result-guard.ts +72 -24
- package/src/types/events.ts +12 -0
- package/src/types/module.ts +12 -4
- package/src/types/trace.ts +14 -3
|
@@ -6,9 +6,10 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { PassthroughStrategy } from '@animalabs/context-manager';
|
|
7
7
|
import { Membrane as RealMembrane, NativeFormatter } from '@animalabs/membrane';
|
|
8
8
|
import { AgentFramework } from '../src/index.js';
|
|
9
|
-
import { TOOL_RESULT_GUARD_AUDIT_STATE, TOOL_RESULT_GUARD_NOTICE, withheldResultNotice } from '../src/tool-result-guard.js';
|
|
9
|
+
import { TOOL_RESULT_GUARD_AUDIT_STATE, TOOL_RESULT_GUARD_NOTICE, keptWhenWithheld, withheldResultNotice } from '../src/tool-result-guard.js';
|
|
10
10
|
import { MockYieldingStream, createMockResponse } from './helpers/mock-membrane.js';
|
|
11
11
|
import { WorkspaceModule } from '../src/modules/workspace/index.js';
|
|
12
|
+
import { DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS } from '../src/tool-result-history.js';
|
|
12
13
|
import { JsStore } from '@animalabs/chronicle';
|
|
13
14
|
// What a withheld result settles to with no writable workspace (#277): the
|
|
14
15
|
// neutral notice, then where the original is kept. Written out, not computed
|
|
@@ -1503,7 +1504,7 @@ async function compressionRig() {
|
|
|
1503
1504
|
const guard = new ToolResultGuard('Claude', cm, true);
|
|
1504
1505
|
const REAL = 'REAL-ACCEPTED-TOOL-OUTPUT';
|
|
1505
1506
|
const content = [{ type: 'tool_result', toolUseId: 'tu-1', content: REAL }];
|
|
1506
|
-
const messageId = guard.storeResults(content, [{ toolUseId: 'tu-1', content: REAL }], []);
|
|
1507
|
+
const messageId = guard.storeResults(content, [{ toolUseId: 'tu-1', content: REAL }], [], undefined, undefined);
|
|
1507
1508
|
// Deferred messages flushed right behind the staged batch push it out of
|
|
1508
1509
|
// the protected tail.
|
|
1509
1510
|
for (let i = 0; i < 6; i++)
|
|
@@ -2561,7 +2562,7 @@ test('#281: a compile waits for an annotation that began while it waited for ano
|
|
|
2561
2562
|
const compiled = r.guard.afterAnnotations(async () => { reads++; return [stub(r.messageId), stub(secondId)]; });
|
|
2562
2563
|
// A second batch settles while the compile waits for the first annotation.
|
|
2563
2564
|
r.cm.addMessage('Claude', [{ type: 'tool_use', id: 'tu-2', name: 'search', input: {} }]);
|
|
2564
|
-
secondId = r.guard.storeResults([{ type: 'tool_result', toolUseId: 'tu-2', content: 'SECOND' }], [{ toolUseId: 'tu-2', content: 'SECOND' }], []);
|
|
2565
|
+
secondId = r.guard.storeResults([{ type: 'tool_result', toolUseId: 'tu-2', content: 'SECOND' }], [{ toolUseId: 'tu-2', content: 'SECOND' }], [], undefined, undefined);
|
|
2565
2566
|
r.guard.submissionResults([{ toolUseId: 'tu-2', content: 'SECOND' }]);
|
|
2566
2567
|
assert.ok(r.guard.withhold('test-category'));
|
|
2567
2568
|
first.open();
|
|
@@ -2947,7 +2948,7 @@ test('#281: an annotation that starts while modules stop writes no file, and is
|
|
|
2947
2948
|
const stopModule = h.module.stop.bind(h.module);
|
|
2948
2949
|
h.module.stop = async () => {
|
|
2949
2950
|
// A settlement during shutdown, as a direct-API call that stop doesn't cancel can make.
|
|
2950
|
-
agent.toolResultGuard.storeResults([{ type: 'tool_result', toolUseId: 'late', content: 'late-original' }], [{ toolUseId: 'late', content: 'late-original' }], []);
|
|
2951
|
+
agent.toolResultGuard.storeResults([{ type: 'tool_result', toolUseId: 'late', content: 'late-original' }], [{ toolUseId: 'late', content: 'late-original' }], [], undefined, undefined);
|
|
2951
2952
|
agent.toolResultGuard.abandon('stopping');
|
|
2952
2953
|
await stopModule();
|
|
2953
2954
|
};
|
|
@@ -2965,6 +2966,219 @@ test('#281: an annotation that starts while modules stop writes no file, and is
|
|
|
2965
2966
|
await restarted.stop();
|
|
2966
2967
|
}
|
|
2967
2968
|
});
|
|
2969
|
+
test('#277: a write for a branch that waits for its mount while the branch changes is refused, and neither branch gets it', async () => {
|
|
2970
|
+
const dir = mkdtempSync(join(tmpdir(), 'af-guard-write-binary-'));
|
|
2971
|
+
dirs.push(dir);
|
|
2972
|
+
mkdirSync(join(dir, 'mount'));
|
|
2973
|
+
const store = JsStore.openOrCreate({ path: join(dir, 'store') });
|
|
2974
|
+
const workspace = new WorkspaceModule({ mounts: [{ name: 'work', path: join(dir, 'mount'), mode: 'read-write', watch: 'never' }] });
|
|
2975
|
+
workspace.initStore(store);
|
|
2976
|
+
try {
|
|
2977
|
+
const tree = 'workspace/work/tree';
|
|
2978
|
+
const source = store.currentBranch().name;
|
|
2979
|
+
const before = store.currentSequence();
|
|
2980
|
+
// Hold the mount's turn, as a pass on it would: the write queues behind it.
|
|
2981
|
+
const internals = workspace;
|
|
2982
|
+
let release;
|
|
2983
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
2984
|
+
const held = internals.withMount(internals.mounts.get('work'), () => gate);
|
|
2985
|
+
const writing = workspace.writeBinary('work/tool-results/late.txt', Buffer.from('late'), 'text/plain', { branch: source });
|
|
2986
|
+
store.createBranchAt('elsewhere', source, before);
|
|
2987
|
+
store.switchBranch('elsewhere');
|
|
2988
|
+
release();
|
|
2989
|
+
await held;
|
|
2990
|
+
assert.deepEqual(await writing, { success: false, error: 'the workspace had left the branch this file is for', isError: true,
|
|
2991
|
+
data: { branchLeft: true } });
|
|
2992
|
+
assert.equal(store.treeGet(tree, 'tool-results/late.txt'), null, 'nothing lands on the branch switched to');
|
|
2993
|
+
store.switchBranch(source);
|
|
2994
|
+
assert.equal(store.treeGet(tree, 'tool-results/late.txt'), null, 'nor on the branch the write named');
|
|
2995
|
+
}
|
|
2996
|
+
finally {
|
|
2997
|
+
store.close();
|
|
2998
|
+
}
|
|
2999
|
+
});
|
|
3000
|
+
// ---------------------------------------------------------------------------
|
|
3001
|
+
// #277's second half: a withheld result keeps the file lists its tool marked
|
|
3002
|
+
// (ToolResult.keepWhenWithheld) inline in its stub. materialize, sync and
|
|
3003
|
+
// delete mark theirs; nothing else does.
|
|
3004
|
+
// ---------------------------------------------------------------------------
|
|
3005
|
+
const LISTS = ' Kept from its result: ';
|
|
3006
|
+
/** The file lists a stub kept, parsed; undefined when it kept none. */
|
|
3007
|
+
const keptLists = (stub) => stub.includes(LISTS) ? JSON.parse(stub.slice(stub.indexOf(LISTS) + LISTS.length)) : undefined;
|
|
3008
|
+
const toolUse = (id, name, input = {}) => createMockResponse([{ type: 'tool_use', id, name, input }], 'tool_use');
|
|
3009
|
+
test('#277: a stub keeps the file lists after where the original is, for each place it can be', () => {
|
|
3010
|
+
const notice = 'Tool result withheld by the guard. The tool has already executed.';
|
|
3011
|
+
const lists = '{"materialized":[{"mount":"work","path":"LOG.md"}]}';
|
|
3012
|
+
assert.equal(withheldResultNotice({ path: 'work/tool-results/x.txt' }, true, lists), `${notice} Its result is in workspace file work/tool-results/x.txt. Kept from its result: ${lists}`);
|
|
3013
|
+
assert.equal(withheldResultNotice({ path: 'work/tool-results/x.txt', error: 'disk full' }, true, lists), `${notice} Writing its result to workspace file work/tool-results/x.txt failed, ` +
|
|
3014
|
+
`so it is kept only in the guard's audit record (framework/tool-result-guard), for an operator. Kept from its result: ${lists}`);
|
|
3015
|
+
assert.equal(withheldResultNotice({ path: 'work/tool-results/y.txt', unfinished: true }, true, lists), `${notice} Its result was still being written to workspace file work/tool-results/y.txt when this note was made; ` +
|
|
3016
|
+
`if that file is missing, it is kept in the guard's audit record (framework/tool-result-guard), for an operator. ` +
|
|
3017
|
+
`Kept from its result: ${lists}`);
|
|
3018
|
+
assert.equal(withheldResultNotice(null, false, lists), `${notice} Its result went to the guard's audit record (framework/tool-result-guard), for an operator, ` +
|
|
3019
|
+
`though saving that record had failed when the result arrived. Kept from its result: ${lists}`);
|
|
3020
|
+
assert.equal(withheldResultNotice(null, true, undefined), AUDIT_STUB, 'nothing kept: the stub is as before');
|
|
3021
|
+
});
|
|
3022
|
+
test('#277: a result keeps the fields its tool marked, in the order marked, cut at the inline cap', () => {
|
|
3023
|
+
const data = { materialized: [{ mount: 'work', path: 'LOG.md' }], count: 1, deleted: [{ mount: 'work', path: 'old.txt' }],
|
|
3024
|
+
skipped: [{ mount: 'work', reason: 'a.txt: divergent' }] };
|
|
3025
|
+
assert.equal(keptWhenWithheld({ success: true, data, keepWhenWithheld: ['deleted', 'materialized'] }, undefined), '{"deleted":[{"mount":"work","path":"old.txt"}],"materialized":[{"mount":"work","path":"LOG.md"}]}');
|
|
3026
|
+
assert.equal(keptWhenWithheld({ success: true, data: { materialized: [], count: 0 }, keepWhenWithheld: ['deleted', 'materialized'] }, undefined), '{"materialized":[]}', 'a marked field the result lacks is left out; an empty list is kept');
|
|
3027
|
+
for (const result of [
|
|
3028
|
+
{ success: true, data },
|
|
3029
|
+
{ success: true, data, keepWhenWithheld: [] },
|
|
3030
|
+
{ success: true, data, keepWhenWithheld: ['absent'] },
|
|
3031
|
+
{ success: true, data: 'text', keepWhenWithheld: ['length'] },
|
|
3032
|
+
{ success: true, data: [{ path: 'a' }], keepWhenWithheld: ['0'] },
|
|
3033
|
+
{ success: false, isError: true, error: 'failed', data: { path: 'work/a.txt' }, keepWhenWithheld: ['path'] },
|
|
3034
|
+
// A failure is success false, whatever isError says.
|
|
3035
|
+
{ success: false, isError: false, error: 'failed', data: { path: 'work/a.txt' }, keepWhenWithheld: ['path'] },
|
|
3036
|
+
// No data at all, with marks.
|
|
3037
|
+
{ success: true, data: null, keepWhenWithheld: ['path'] },
|
|
3038
|
+
{ success: true, keepWhenWithheld: ['path'] },
|
|
3039
|
+
])
|
|
3040
|
+
assert.equal(keptWhenWithheld(result, undefined), undefined, `keeps nothing: ${JSON.stringify(result)}`);
|
|
3041
|
+
const long = { written: Array.from({ length: 200 }, (_, i) => `file-${i}.txt`) };
|
|
3042
|
+
const cut = keptWhenWithheld({ success: true, data: long, keepWhenWithheld: ['written'] }, 1000);
|
|
3043
|
+
const whole = JSON.stringify(long);
|
|
3044
|
+
assert.ok(whole.length > 1000);
|
|
3045
|
+
// The note says what was cut and where the rest is, not "original was N",
|
|
3046
|
+
// which would read as the whole result's size.
|
|
3047
|
+
assert.equal(cut, `${whole.slice(0, 1000)} [cut at the inline cap: 1000 of these fields' ${whole.length} characters; ` +
|
|
3048
|
+
'the whole result is where this note says]');
|
|
3049
|
+
// A cut never leaves half a surrogate pair.
|
|
3050
|
+
// '{"name":"' is 9 characters and each emoji 2, so a cap of 14 falls inside the third.
|
|
3051
|
+
const emoji = keptWhenWithheld({ success: true, data: { name: '\u{1F600}'.repeat(10) }, keepWhenWithheld: ['name'] }, 14);
|
|
3052
|
+
const loneSurrogate = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
|
|
3053
|
+
assert.doesNotMatch(emoji, loneSurrogate, `well-formed: ${JSON.stringify(emoji)}`);
|
|
3054
|
+
assert.match(emoji, /^\{"name":"\u{1F600}\u{1F600} \[cut at the inline cap: 13 of these fields' 31 characters;/u);
|
|
3055
|
+
});
|
|
3056
|
+
test('#277: a withheld batch keeps what each marked result listed; an unmarked one keeps nothing, and its file has everything', async () => {
|
|
3057
|
+
const h = await workspaceHarness([[calls('marked', 'plain'), refused()], [answer()]]);
|
|
3058
|
+
h.module.results.marked = { success: true, data: { written: ['LOG.md'], detail: 'payload-marked' }, keepWhenWithheld: ['written'] };
|
|
3059
|
+
try {
|
|
3060
|
+
await h.run();
|
|
3061
|
+
const [marked, plain] = toolResults(h.framework).map((block) => String(block.content));
|
|
3062
|
+
assert.deepEqual(keptLists(marked), { written: ['LOG.md'] });
|
|
3063
|
+
assert.doesNotMatch(marked, /payload-marked/, 'only the marked field is kept');
|
|
3064
|
+
assert.match(marked, /^Tool result withheld by the guard\. The tool has already executed\. Its result is in workspace file work\/tool-results\/\S+-0-marked\.txt\. Kept from its result: /);
|
|
3065
|
+
assert.equal(keptLists(plain), undefined);
|
|
3066
|
+
assert.match(plain, /Its result is in workspace file work\/tool-results\/\S+-1-plain\.txt\.$/);
|
|
3067
|
+
const file = await h.workspace.readBinary(/workspace file (\S+\.txt)\./.exec(marked)[1]);
|
|
3068
|
+
assert.match(file.data.toString('utf8'), /payload-marked/, 'the file holds the whole result');
|
|
3069
|
+
const retried = h.membrane.requests[1].messages.flatMap((m) => m.content).filter((b) => b.type === 'tool_result');
|
|
3070
|
+
assert.deepEqual(retried.map((b) => b.content), [marked, plain], 'the retry already carries the lists');
|
|
3071
|
+
assert.doesNotMatch(marked, /refus|test-category/, "#159's line: no refusal or category in the context");
|
|
3072
|
+
}
|
|
3073
|
+
finally {
|
|
3074
|
+
await h.framework.stop();
|
|
3075
|
+
}
|
|
3076
|
+
});
|
|
3077
|
+
test('#277: a marked result skipped for a branch change keeps its fields in the stub that names the audit record', async () => {
|
|
3078
|
+
const h = await workspaceHarness([[calls('marked', 'plain'), refused()], [answer()]]);
|
|
3079
|
+
h.module.results.marked = { success: true, data: { written: ['LOG.md'], detail: 'payload-marked' }, keepWhenWithheld: ['written'] };
|
|
3080
|
+
// Declined where it commits, and the store is back on the batch's branch by
|
|
3081
|
+
// the edit, as after an undo and a redo.
|
|
3082
|
+
h.framework.writeToolResultFile = async () => ({ path: 'work/tool-results/declined.txt', branchLeft: true });
|
|
3083
|
+
try {
|
|
3084
|
+
await bounded(h.run(), 'the run');
|
|
3085
|
+
const [marked, plain] = toolResults(h.framework).map((block) => String(block.content));
|
|
3086
|
+
assert.equal(plain, AUDIT_STUB);
|
|
3087
|
+
assert.ok(marked.startsWith(`${AUDIT_STUB} Kept from its result: `), marked);
|
|
3088
|
+
assert.deepEqual(keptLists(marked), { written: ['LOG.md'] });
|
|
3089
|
+
}
|
|
3090
|
+
finally {
|
|
3091
|
+
await h.framework.stop();
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
3094
|
+
test('#277: with no writable workspace the stub still keeps the file lists, cut at the agent\'s inline cap', async () => {
|
|
3095
|
+
const written = Array.from({ length: 200 }, (_, i) => `file-${i}.txt`);
|
|
3096
|
+
const h = await harness([[calls('marked'), refused()], [answer()]], { toolResultGuard: true }, { marked: { success: true, data: { written }, keepWhenWithheld: ['written'] } });
|
|
3097
|
+
try {
|
|
3098
|
+
const spill = [...h.framework.collectAgentSettingsExtensions().values()].find((ext) => ext.keys.includes('tool_result_inline_max_chars'));
|
|
3099
|
+
spill.update('assistant', { tool_result_inline_max_chars: 1000 });
|
|
3100
|
+
await h.run();
|
|
3101
|
+
const stub = String(toolResults(h.framework)[0].content);
|
|
3102
|
+
const whole = JSON.stringify({ written });
|
|
3103
|
+
assert.equal(stub, `${AUDIT_STUB}${LISTS}${whole.slice(0, 1000)} [cut at the inline cap: 1000 of these fields' ` +
|
|
3104
|
+
`${whole.length} characters; the whole result is where this note says]`);
|
|
3105
|
+
}
|
|
3106
|
+
finally {
|
|
3107
|
+
await h.framework.stop();
|
|
3108
|
+
}
|
|
3109
|
+
});
|
|
3110
|
+
test('#277: direct API: a withheld result keeps its file lists, cut at the house default cap it was stored under', async () => {
|
|
3111
|
+
const h = await harness([], { toolResultGuard: true });
|
|
3112
|
+
const responses = [calls('marked'), refused(), answer()];
|
|
3113
|
+
h.membrane.stream = async () => {
|
|
3114
|
+
const response = responses.shift();
|
|
3115
|
+
assert.ok(response);
|
|
3116
|
+
return response;
|
|
3117
|
+
};
|
|
3118
|
+
try {
|
|
3119
|
+
const agent = h.framework.getAgent('assistant');
|
|
3120
|
+
agent.getContextManager().addMessage('user', [{ type: 'text', text: 'read' }]);
|
|
3121
|
+
await agent.runInference(h.framework.getAllTools());
|
|
3122
|
+
const written = Array.from({ length: 3000 }, (_, i) => `file-${i}.txt`);
|
|
3123
|
+
agent.provideToolResult('marked', { success: true, data: { written }, keepWhenWithheld: ['written'] });
|
|
3124
|
+
await agent.runInference(h.framework.getAllTools());
|
|
3125
|
+
const stub = String(toolResults(h.framework)[0].content);
|
|
3126
|
+
const whole = JSON.stringify({ written });
|
|
3127
|
+
assert.ok(whole.length > DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS);
|
|
3128
|
+
assert.equal(stub, `${AUDIT_STUB}${LISTS}${whole.slice(0, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS)}` +
|
|
3129
|
+
` [cut at the inline cap: ${DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS} of these fields' ${whole.length} characters; ` +
|
|
3130
|
+
'the whole result is where this note says]');
|
|
3131
|
+
}
|
|
3132
|
+
finally {
|
|
3133
|
+
await h.framework.stop();
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
test('#283: a withheld materialize keeps the deletions it made; sync keeps a root it accepted ahead of its results', async () => {
|
|
3137
|
+
const h = await workspaceHarness([[toolUse('m', 'workspace--materialize', { applyDeletions: true }), refused()], [answer()]]);
|
|
3138
|
+
const call = (name, input = {}) => h.workspace.handleToolCall({ id: name, name, input });
|
|
3139
|
+
try {
|
|
3140
|
+
// A file on disk that the workspace then deletes: the agent's materialize removes it.
|
|
3141
|
+
assert.equal((await h.workspace.writeBinary('work/old.txt', Buffer.from('old'), 'text/plain')).success, true);
|
|
3142
|
+
assert.equal((await call('materialize')).success, true);
|
|
3143
|
+
assert.equal((await call('delete', { path: 'work/old.txt' })).success, true);
|
|
3144
|
+
await h.run();
|
|
3145
|
+
const [materialize] = toolResults(h.framework).map((block) => String(block.content));
|
|
3146
|
+
assert.deepEqual(keptLists(materialize), { deleted: [{ mount: 'work', path: 'old.txt' }], materialized: [] }, materialize);
|
|
3147
|
+
// sync marks a root it accepted first, so a long results list can't cut it away.
|
|
3148
|
+
const synced = await call('sync');
|
|
3149
|
+
assert.deepEqual(synced.keepWhenWithheld, ['rootsAccepted', 'results']);
|
|
3150
|
+
}
|
|
3151
|
+
finally {
|
|
3152
|
+
await h.framework.stop();
|
|
3153
|
+
}
|
|
3154
|
+
});
|
|
3155
|
+
test('#277: withheld materialize, sync and delete keep the files they acted on in their stubs', async () => {
|
|
3156
|
+
const h = await workspaceHarness([
|
|
3157
|
+
[toolUse('m', 'workspace--materialize'), refused()], [answer()],
|
|
3158
|
+
[toolUse('s', 'workspace--sync'), refused()], [answer()],
|
|
3159
|
+
[toolUse('d', 'workspace--delete', { path: 'work/LOG.md' }), refused()], [answer()],
|
|
3160
|
+
]);
|
|
3161
|
+
try {
|
|
3162
|
+
// A workspace draft disk doesn't have yet: the materialize writes it.
|
|
3163
|
+
assert.equal((await h.workspace.writeBinary('work/LOG.md', Buffer.from('log'), 'text/plain')).success, true);
|
|
3164
|
+
await h.run();
|
|
3165
|
+
// A file disk has and the workspace doesn't: the sync takes it in.
|
|
3166
|
+
const mountDir = /^(.*)\/LOG\.md$/.exec(h.workspace.resolveAbsolutePath('work/LOG.md'))[1];
|
|
3167
|
+
writeFileSync(join(mountDir, 'disk.txt'), 'from disk');
|
|
3168
|
+
await h.run();
|
|
3169
|
+
await h.run();
|
|
3170
|
+
const [materialize, sync, del] = toolResults(h.framework).map((block) => String(block.content));
|
|
3171
|
+
assert.deepEqual(keptLists(materialize), { materialized: [{ mount: 'work', path: 'LOG.md' }] }, materialize);
|
|
3172
|
+
const synced = keptLists(sync);
|
|
3173
|
+
assert.deepEqual(synced.results.map((r) => [r.mount, r.synced]), [['work', ['disk.txt']]], sync);
|
|
3174
|
+
assert.deepEqual(keptLists(del), { path: 'work/LOG.md', deleted: true }, del);
|
|
3175
|
+
for (const stub of [materialize, sync, del])
|
|
3176
|
+
assert.match(stub, /^Tool result withheld by the guard\. The tool has already executed\. Its result is in workspace file /);
|
|
3177
|
+
}
|
|
3178
|
+
finally {
|
|
3179
|
+
await h.framework.stop();
|
|
3180
|
+
}
|
|
3181
|
+
});
|
|
2968
3182
|
test('an unexpected annotation exception releases its compression hold', async () => {
|
|
2969
3183
|
const r = await compressionRig();
|
|
2970
3184
|
const branch = r.cm.currentBranch.bind(r.cm);
|
|
@@ -3099,7 +3313,7 @@ test("a batch's deadline is taken when it is queued, not when the batches ahead
|
|
|
3099
3313
|
await until(() => entered === 1, 'the first annotation is writing');
|
|
3100
3314
|
// A second batch queued behind the first, still writing.
|
|
3101
3315
|
r.cm.addMessage('Claude', [{ type: 'tool_use', id: 'tu-2', name: 'search', input: {} }]);
|
|
3102
|
-
r.guard.storeResults([{ type: 'tool_result', toolUseId: 'tu-2', content: 'SECOND' }], [{ toolUseId: 'tu-2', content: 'SECOND' }], []);
|
|
3316
|
+
r.guard.storeResults([{ type: 'tool_result', toolUseId: 'tu-2', content: 'SECOND' }], [{ toolUseId: 'tu-2', content: 'SECOND' }], [], undefined, undefined);
|
|
3103
3317
|
r.guard.submissionResults([{ toolUseId: 'tu-2', content: 'SECOND' }]);
|
|
3104
3318
|
assert.ok(r.guard.withhold('test-category'));
|
|
3105
3319
|
assert.equal(taken, 2, "the second batch's deadline was taken when it was queued");
|
|
@@ -3196,7 +3410,7 @@ test('stop then start reopens withheld spills', async () => {
|
|
|
3196
3410
|
await framework.stop();
|
|
3197
3411
|
framework.start();
|
|
3198
3412
|
const guard = framework.getAgent('assistant').toolResultGuard;
|
|
3199
|
-
guard.storeResults([{ type: 'tool_result', toolUseId: 'one', content: 'original' }], [{ toolUseId: 'one', content: 'original' }], []);
|
|
3413
|
+
guard.storeResults([{ type: 'tool_result', toolUseId: 'one', content: 'original' }], [{ toolUseId: 'one', content: 'original' }], [], undefined, undefined);
|
|
3200
3414
|
guard.abandon('test');
|
|
3201
3415
|
await guard.whenAnnotated();
|
|
3202
3416
|
assert.equal(writes, 1);
|
|
@@ -3278,7 +3492,7 @@ test('slow successful writes share one batch deadline', async (t) => {
|
|
|
3278
3492
|
const guard = h.framework.getAgent('assistant').toolResultGuard;
|
|
3279
3493
|
try {
|
|
3280
3494
|
t.mock.timers.enable({ apis: ['Date', 'setTimeout'], now: Date.now() });
|
|
3281
|
-
guard.storeResults(['one', 'two', 'three'].map((id) => ({ type: 'tool_result', toolUseId: id, content: id })), ['one', 'two', 'three'].map((id) => ({ toolUseId: id, content: id })), []);
|
|
3495
|
+
guard.storeResults(['one', 'two', 'three'].map((id) => ({ type: 'tool_result', toolUseId: id, content: id })), ['one', 'two', 'three'].map((id) => ({ toolUseId: id, content: id })), [], undefined, undefined);
|
|
3282
3496
|
guard.abandon('test');
|
|
3283
3497
|
let annotated = false;
|
|
3284
3498
|
void guard.whenAnnotated().then(() => { annotated = true; });
|