Skip to content
This repository was archived by the owner on Aug 19, 2026. It is now read-only.

Commit dbd4255

Browse files
committed
Narrow agent broker capability inference
1 parent 112c6ab commit dbd4255

2 files changed

Lines changed: 104 additions & 20 deletions

File tree

packages/server/src/agent-broker.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ export function policyFromIntent(
254254
options: { persistence?: SurfacePersistence } = {},
255255
): SurfacePolicy {
256256
const persistence = options.persistence ?? 'replayable';
257+
const hasSurfaceAccess = intent.requestedCapabilities.length > 0 || intent.requestedComponents.length > 0;
257258
if (intent.sideEffect === 'approval-required' || intent.interaction === 'approval') {
258259
return {
259260
tier: 'approval',
@@ -273,10 +274,13 @@ export function policyFromIntent(
273274
};
274275
}
275276
if (
276-
intent.interaction !== 'none' ||
277-
intent.dataNeed === 'host-resource' ||
278-
intent.sideEffect === 'local-state' ||
279-
intent.sideEffect === 'external-action'
277+
hasSurfaceAccess &&
278+
(
279+
intent.interaction !== 'none' ||
280+
intent.dataNeed === 'host-resource' ||
281+
intent.sideEffect === 'local-state' ||
282+
intent.sideEffect === 'external-action'
283+
)
280284
) {
281285
return {
282286
tier: 'declarative',
@@ -587,44 +591,40 @@ function inferCapabilityNames(prompt: string, pack: CapabilityPack | null): stri
587591
const intents = pack?.intents ?? [];
588592
if (intents.length === 0) return [];
589593
const text = prompt.toLowerCase();
590-
const matches = intents.filter((intent) => capabilityMatchesIntent(prompt, text, intent));
591-
if (matches.length > 0) return matches.map((intent) => intent.name);
594+
const directMatches = intents.filter((intent) => matchesCapabilityName(text, intent.name));
595+
if (directMatches.length > 0) return directMatches.map((intent) => intent.name);
592596

593597
const approval = APPROVAL_RE.test(prompt)
594598
? intents.filter((intent) => intentAuthority(intent) === 'approval-gated')
595599
: [];
596-
if (approval.length === 1) return [approval[0]!.name];
600+
if (approval.length > 0) return singleCandidateNames(approval);
597601

598602
const worker = BACKGROUND_RE.test(prompt)
599603
? intents.filter((intent) => intentData(intent) === 'worker')
600604
: [];
601-
if (worker.length > 0) return worker.map((intent) => intent.name);
605+
if (worker.length > 0) return singleCandidateNames(worker);
602606

603607
const resource = SEARCH_RE.test(prompt)
604608
? intents.filter((intent) => intentData(intent) === 'host-resource')
605609
: [];
606-
if (resource.length === 1) return [resource[0]!.name];
610+
if (resource.length > 0) return singleCandidateNames(resource);
607611

608-
const actions = intents.filter((intent) => intentAuthority(intent) === 'host-action');
609-
if ((FORM_RE.test(prompt) || SELECT_RE.test(prompt)) && actions.length === 1) {
610-
return [actions[0]!.name];
612+
const actions = intents.filter((intent) => capabilityMatchesActionClass(prompt, intent));
613+
if (actions.length > 0) {
614+
return singleCandidateNames(actions);
611615
}
612616
return [];
613617
}
614618

615-
function capabilityMatchesIntent(prompt: string, text: string, intent: IntentSpec): boolean {
616-
if (matchesCapabilityName(text, intent.name)) return true;
617-
619+
function capabilityMatchesActionClass(prompt: string, intent: IntentSpec): boolean {
618620
const data = intentData(intent);
619621
const authority = intentAuthority(intent);
620-
if (APPROVAL_RE.test(prompt)) return authority === 'approval-gated';
621-
if (BACKGROUND_RE.test(prompt)) return data === 'worker';
622-
if (SEARCH_RE.test(prompt)) return data === 'host-resource';
622+
if (authority !== 'host-action' || data === 'worker') return false;
623623
if (FORM_RE.test(prompt)) {
624-
return authority === 'host-action' && data !== 'worker' && capabilityClassMatches(intent, FORM_CAPABILITY_RE);
624+
return capabilityClassMatches(intent, FORM_CAPABILITY_RE);
625625
}
626626
if (SELECT_RE.test(prompt)) {
627-
return authority === 'host-action' && data !== 'worker' && capabilityClassMatches(intent, SELECT_CAPABILITY_RE);
627+
return capabilityClassMatches(intent, SELECT_CAPABILITY_RE);
628628
}
629629
return false;
630630
}
@@ -639,6 +639,10 @@ function matchesCapabilityName(text: string, name: string): boolean {
639639
return terms.every((term) => text.includes(term));
640640
}
641641

642+
function singleCandidateNames(intents: IntentSpec[]): string[] {
643+
return intents.length === 1 ? [intents[0]!.name] : [];
644+
}
645+
642646
function capabilityClassMatches(intent: IntentSpec, pattern: RegExp): boolean {
643647
return pattern.test(`${intent.name} ${intent.description}`);
644648
}

packages/server/test/agent-broker.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,44 @@ const capabilities: CapabilityPack = {
4949
],
5050
};
5151

52+
const multiToolCapabilities: CapabilityPack = {
53+
intents: [
54+
...capabilities.intents,
55+
{
56+
name: 'delete_record',
57+
description: 'Delete a selected record after host approval.',
58+
argsSchema: '{}',
59+
stateShape: '{}',
60+
kind: 'action',
61+
surface: { authority: 'approval-gated' },
62+
},
63+
{
64+
name: 'github_lookup',
65+
description: 'Look up GitHub profile data.',
66+
argsSchema: '{}',
67+
stateShape: '{}',
68+
kind: 'resource',
69+
surface: { data: 'host-resource', authority: 'read' },
70+
},
71+
{
72+
name: 'compute_score',
73+
description: 'Run background score computation.',
74+
argsSchema: '{}',
75+
stateShape: '{}',
76+
kind: 'action',
77+
surface: { data: 'worker', authority: 'host-action' },
78+
},
79+
{
80+
name: 'counter',
81+
description: 'Increment a local counter.',
82+
argsSchema: '{}',
83+
stateShape: '{}',
84+
kind: 'action',
85+
surface: { authority: 'host-action' },
86+
},
87+
],
88+
};
89+
5290
test('inferSurfaceIntent maps search prompts to host-resource intent', () => {
5391
const intent = inferSurfaceIntent(
5492
'build a dinner finder where i can search recipes and browse results',
@@ -116,6 +154,48 @@ test('planAgentSurface selects worker and approval tiers from catalog-backed int
116154
assert.equal(approval.surfacePolicy.purpose, 'operate');
117155
});
118156

157+
test('planAgentSurface keeps multi-tool class inference narrow', async () => {
158+
const approval = await planAgentSurface({
159+
prompt: 'publish the prepared summary',
160+
capabilities: multiToolCapabilities,
161+
});
162+
assert.equal(approval.surfacePolicy.tier, 'approval');
163+
assert.deepEqual(approval.surfacePolicy.grants, ['publish_summary']);
164+
165+
const search = await planAgentSurface({
166+
prompt: 'search recipes for dinner',
167+
capabilities: multiToolCapabilities,
168+
});
169+
assert.equal(search.surfacePolicy.tier, 'declarative');
170+
assert.deepEqual(search.surfacePolicy.grants, ['search']);
171+
172+
const ambiguousSearch = await planAgentSurface({
173+
prompt: 'search the host data',
174+
capabilities: {
175+
intents: [
176+
{
177+
name: 'recipe_lookup',
178+
description: 'Look up recipe data.',
179+
argsSchema: '{}',
180+
stateShape: '{}',
181+
kind: 'resource',
182+
surface: { data: 'host-resource', authority: 'read' },
183+
},
184+
{
185+
name: 'github_lookup',
186+
description: 'Look up GitHub profile data.',
187+
argsSchema: '{}',
188+
stateShape: '{}',
189+
kind: 'resource',
190+
surface: { data: 'host-resource', authority: 'read' },
191+
},
192+
],
193+
},
194+
});
195+
assert.equal(ambiguousSearch.surfacePolicy.tier, 'static');
196+
assert.equal(ambiguousSearch.surfacePolicy.grants, undefined);
197+
});
198+
119199
test('planAgentSurface selects host actions only from explicit action phrasing', async () => {
120200
const plan = await planAgentSurface({
121201
prompt: 'let me save a choice for the launch announcement',

0 commit comments

Comments
 (0)