Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions apps/server/src/generate-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,49 @@ test('api generate sends narrowed contract and stream meta shape through package
assert.deepEqual(policyContract.tools?.map((tool) => tool.name), ['search']);
assert.deepEqual(policyContract.components, []);

const agentResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
prompt: 'build a dinner finder where i can search recipes',
capabilities: searchCapability,
agent: { enabled: true, intentModel: 'off' },
}),
});
const agentBody = await agentResponse.text();
assert.equal(agentResponse.status, 200, agentBody);

assert.equal(anthropicRequests.length, 3);
const agentRequest = anthropicRequests[2] as { system?: Array<{ text?: string }>; stream?: boolean };
assert.equal(agentRequest.stream, true);
const agentSystemText = agentRequest.system?.map((block) => block.text ?? '').join('\n') ?? '';
assert.match(agentSystemText, /Search host-owned dinner data/);
assert.match(agentSystemText, /Surface contract/);
assert.match(agentSystemText, /runtime=`declarative`/);

const agentLines = agentBody
.trim()
.split(/\n/)
.filter(Boolean)
.map((raw) => JSON.parse(raw) as ProtocolLine);
assert.deepEqual(agentLines.slice(0, 6).map((line) => `${line.op} ${line.path}`), [
'meta /mode-upgraded',
'meta /agent-intent',
'meta /agent-policy-resolution',
'meta /surface-policy',
'meta /surface-plan',
'meta /surface-contract',
]);
const agentIntent = agentLines[1] as Extract<ProtocolLine, { op: 'meta' }>;
assert.equal((agentIntent.value as { interaction?: unknown }).interaction, 'search');
const agentPolicy = agentLines[3] as Extract<ProtocolLine, { op: 'meta' }>;
assert.deepEqual(agentPolicy.value, {
tier: 'declarative',
purpose: 'explore',
grants: ['search'],
components: [],
persistence: 'replayable',
});
const ghostResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
Expand All @@ -257,8 +300,8 @@ test('api generate sends narrowed contract and stream meta shape through package
const ghostBody = await ghostResponse.text();
assert.equal(ghostResponse.status, 200, ghostBody);

assert.equal(anthropicRequests.length, 3);
const ghostRequest = anthropicRequests[2] as { system?: Array<{ text?: string }>; stream?: boolean };
assert.equal(anthropicRequests.length, 4);
const ghostRequest = anthropicRequests[3] as { system?: Array<{ text?: string }>; stream?: boolean };
const ghostSystemText = ghostRequest.system?.map((block) => block.text ?? '').join('\n') ?? '';
assert.match(ghostSystemText, /Checkout product experience/);

Expand Down Expand Up @@ -296,7 +339,7 @@ test('api generate sends narrowed contract and stream meta shape through package
const ghostOverrideBody = await ghostOverrideResponse.text();
assert.equal(ghostOverrideResponse.status, 400);
assert.match(ghostOverrideBody, /tokenOverrides are not supported with Ghost product memory/);
assert.equal(anthropicRequests.length, 3);
assert.equal(anthropicRequests.length, 4);
});

test('api generate emits compact Ghost capsule for root contexts', async (t) => {
Expand Down
69 changes: 60 additions & 9 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
type TokenOverride,
} from '@anarchitecture/summon/engine';
import {
planAgentSurface,
resolveSurfaceGenerationPlan,
runSurfaceGeneration,
summarizeContractIssues,
type AgentSurfacePlanResult,
type GenerateEditInput,
type RepairOptions as SurfaceRepairOptions,
type SurfaceGenerationSummary,
Expand Down Expand Up @@ -466,6 +468,11 @@ app.post('/api/generate', async (req, res) => {
}
const edit = parsedEdit.edit;
const repairOptions = parseRepairOptions(req.body?.repair);
const rawAgentOptions = req.body?.agent;
const agentOptions = rawAgentOptions && typeof rawAgentOptions === 'object'
? rawAgentOptions as Record<string, unknown>
: null;
const agentPlanningEnabled = agentOptions?.enabled === true;

const hasSurfacePolicy =
req.body?.surfacePolicy !== undefined && req.body.surfacePolicy !== null;
Expand All @@ -481,6 +488,7 @@ app.post('/api/generate', async (req, res) => {
let modeUpgraded = false;
let inferenceUsed = false;
let surfacePlan: SurfacePlan;
let agentPlan: AgentSurfacePlanResult | null = null;

// Shape classification — picks ONE response shape so the per-direction
// block ships only the matching shape exemplar (atoms always ship). Falls
Expand All @@ -504,6 +512,23 @@ app.post('/api/generate', async (req, res) => {
scriptPolicy = compiledPolicy.scriptPolicy;
pack = compiledPolicy.capabilities;
surfacePlan = compiledPolicy.surfacePlan;
} else if (agentPlanningEnabled) {
agentPlan = await planAgentSurface({
prompt,
capabilities: capabilityCeiling,
components: componentPack,
intentModel: process.env.SUMMON_AGENT_INTENT_MODEL === '0' || agentOptions?.intentModel === 'off'
? null
: {
completeText: (request) => modelProvider.completeText(request, modelSelection),
},
intentTimeoutMs: clampInt(agentOptions?.intentTimeoutMs, 250, 5000, 1800),
});
mode = agentPlan.compiledPolicy.mode;
scriptPolicy = agentPlan.compiledPolicy.scriptPolicy;
pack = agentPlan.compiledPolicy.capabilities;
surfacePlan = agentPlan.compiledPolicy.surfacePlan;
modeUpgraded = requestedMode === 'static' && mode === 'interactive';
} else {
// Layer 3: utility-model capability inference. Decides mode + narrows the
// pack to the minimal subset of intents the prompt actually needs. The
Expand Down Expand Up @@ -562,7 +587,11 @@ app.post('/api/generate', async (req, res) => {
mode,
surfacePlan,
shape,
capabilities: hasSurfacePolicy ? capabilityCeiling : pack,
capabilities: hasSurfacePolicy
? capabilityCeiling
: agentPlan
? agentPlan.compiledPolicy.capabilities
: pack,
components: componentPack,
});
}
Expand Down Expand Up @@ -592,13 +621,31 @@ app.post('/api/generate', async (req, res) => {
});
}
}

// Emit the mode-upgrade signal as the first byte the client sees, before
// any concurrency wait. The client respawns its sandbox into interactive
// mode in response, so we want this to land before any artifact bytes.
// Emit the mode-upgrade signal before agent diagnostics. The client respawns
// its sandbox into interactive mode in response, so this should land before
// any broker or artifact bytes that assume the upgraded mode.
if (modeUpgraded) {
preludeLines.push({ op: 'meta', path: '/mode-upgraded', value: 'static→interactive' });
}
if (agentPlan) {
preludeLines.push({
op: 'meta',
path: '/agent-intent',
value: agentPlan.intent,
});
preludeLines.push({
op: 'meta',
path: '/agent-policy-resolution',
value: {
source: agentPlan.policyResolution.source,
proposedSurfacePolicy: agentPlan.policyResolution.proposedSurfacePolicy,
surfacePolicy: agentPlan.policyResolution.surfacePolicy,
rejectedCapabilities: agentPlan.policyResolution.rejectedCapabilities,
rejectedComponents: agentPlan.policyResolution.rejectedComponents,
fallback: agentPlan.policyResolution.fallback,
},
});
}
if (shape) {
preludeLines.push({ op: 'meta', path: '/shape', value: shape });
}
Expand Down Expand Up @@ -653,11 +700,15 @@ app.post('/api/generate', async (req, res) => {
ghost: ghostContext ?? null,
layout,
edit,
capabilities: hasSurfacePolicy ? capabilityCeiling : pack,
capabilities: hasSurfacePolicy || agentPlan ? capabilityCeiling : pack,
components: componentPack,
surfacePolicy: hasSurfacePolicy ? req.body.surfacePolicy : null,
scriptPolicy: hasSurfacePolicy ? undefined : scriptPolicy,
surfacePlan: hasSurfacePolicy ? null : surfacePlan,
surfacePolicy: hasSurfacePolicy
? req.body.surfacePolicy
: agentPlan
? agentPlan.surfacePolicy
: null,
scriptPolicy: hasSurfacePolicy || agentPlan ? undefined : scriptPolicy,
surfacePlan: hasSurfacePolicy || agentPlan ? null : surfacePlan,
tokenOverrides: overrides.applied,
activeTokensCss: ghostContext?.tokenSource.css ?? direction?.tokensCss ?? null,
preludeLines,
Expand Down
27 changes: 27 additions & 0 deletions docs/adoption/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,33 @@ Common configs:
Hosts choose the config before generation. The model may react to the compiled
safety details, but it cannot widen what the host allowed.

### Agent-Driven Configs

When a user is talking to an agent or another harness, the user should not need
to choose Summon tiers, grants, script policy, or surface plans. Use the server
broker to translate the prompt into a bounded host-owned config:

```ts
import { runAgentSurfaceGeneration } from '@anarchitecture/summon-server';

await runAgentSurfaceGeneration({
prompt,
modelProvider,
capabilities: capabilityContract.pack,
components: componentContract.pack,
hostPolicyResolver: ({ proposedSurfacePolicy }) => {
return productPolicy.narrow(proposedSurfacePolicy);
},
}, emit);
```

The broker emits `/agent-intent` and `/agent-policy-resolution` diagnostics,
then generation continues through the normal `/surface-policy`,
`/surface-plan`, and `/surface-contract` path. `SurfaceIntent` is an
experimental planning shape, not an authority contract. The inferred intent is
advisory: the host resolver and `compileSurfacePolicy()` still decide which
tools, components, runtime, and approval paths are actually available.

### Surface Contract View

When a server receives a `SurfacePolicy`, Summon also derives a read-only
Expand Down
28 changes: 28 additions & 0 deletions docs/adoption/package-consumption.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ await consumeSurfaceStream(response.body!, {

```ts
import {
runAgentSurfaceGeneration,
runSurfaceGeneration,
type SummonModelProvider,
} from '@anarchitecture/summon-server';
Expand All @@ -209,6 +210,33 @@ accepted Summon lines and diagnostics, and returns a replay summary.
consume an async generator, but new servers should prefer
`runSurfaceGeneration(input, emit)`.

For agent-driven hosts, use `runAgentSurfaceGeneration(input, emit)` when the
end user should not choose Summon-specific configs. The harness supplies the
prompt, model provider, host tool catalog, trusted component catalog, and any
host policy resolver. The broker converts the prompt to an advisory
`SurfaceIntent`, proposes a `SurfacePolicy`, narrows it through host-owned
policy, then calls the same `runSurfaceGeneration()` lifecycle.

```ts
await runAgentSurfaceGeneration({
prompt,
modelProvider,
capabilities: capabilityContract.pack,
components: componentContract.pack,
hostPolicyResolver: ({ proposedSurfacePolicy }) => {
return productPolicy.narrow(proposedSurfacePolicy);
},
}, (line) => {
response.write(`${JSON.stringify(line)}\n`);
});
```

The intent converter can use rules, a model-assisted `intentModel`, or a custom
`intentProvider`. `SurfaceIntent` is experimental and advisory; its output is
never authority. The host resolver and `compileSurfacePolicy()` still decide
which host tools, components, runtime, and approval paths are actually
available.

## Package Gate

Run this before publishing:
Expand Down
Loading
Loading