Skip to content

feat: agent-level surfaces — instructions.<source>.md + surfaces mechanics - #150

Merged
linyiru merged 8 commits into
mainfrom
feat/channel-overlay-field
Jul 31, 2026
Merged

feat: agent-level surfaces — instructions.<source>.md + surfaces mechanics#150
linyiru merged 8 commits into
mainfrom
feat/channel-overlay-field

Conversation

@linyiru

@linyiru linyiru commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes #149. Stacked on #147 (shares the assembleDurable/DoAgentDef surfaces); after #147 merges this retargets to main automatically.

The shape (v2 of #149, after the design pivot recorded there)

agent/
  agent.ts                  ← surfaces: { slack: { mode: "replace", denyTools: [...] } }
  instructions.md           ← base
  instructions.slack.md     ← the slack surface's instruction variant
  channels/
    crisp.ts  slack.ts      ← pure transport, no .md

Per-surface behavior is agent-level: instructions.<source>.md is discovered by convention (inlined by june gen for the edge) and applies to turns arriving through that source — the file alone activates it (append default). agent.ts surfaces adds the mechanics the prose used to beg for:

  • mode: "replace" — the variant IS the turn's system prompt; withSystem consumes the mode and drops the base mechanically (no more bold "disregard the above").
  • denyTools — removed at the same per-turn choke point as requiresPrincipal: unlisted AND undispatchable; a hallucinated call fails with the same unknown tool contract. Enforcement, not suggestion. Persisted through the suspend checkpoint like the overlay, so resumed turns keep their policy.

Fail-loud edges: mode without its variant file → assembly/build error; a source authored in both the new pieces and the legacy channelInstructions map → error (one source of truth); a policy matching no mounted channel → warning at assembly (native) or in the DO after factory resolution (edge) — the old silent-no-fire orphan, surfaced.

channels/<name>.md keeps working for one dev-series with loud deprecation warnings at discovery and at june gen. Channels carry no behavior prose — they are transport.

Evidence (full trail on #149)

  • Scenario analysis: channel-level loses 6 of 7 scenarios (two-agents-one-channel, behavior matrices, denyTools coupling, locale variants…); the one it wins (platform-intrinsic format rules) belongs to built-in channel authors, tracked separately.
  • Measurement (10 identical agents, first-instinct, graded): 80/80, including 10/10 on the blind-guess test channels/slack.md failed 2/10, 20/20 agreement with the spec's two design-judgment calls (mode-without-file = build error; file-without-config = applies, append default), and 10/10 zero-divergence locale-grammar generalization.

Implementation notes

One derivation function (surfacePolicies) serves defineAgent, assembleAgent, and assembleDurable — native discovery and the compiled module cannot drift (the existing compiled≡discovered parity test covers the new path with no new machinery). The engine seam is ChannelPolicy on AgentSession's per-source map (bare strings still work — the classic overlay form). bun run ci green: typecheck + full suite (engine tests pin append/replace/bare-string composition, per-source deny + failure contract, and the surfacePolicies edges).

linyiru added 4 commits July 31, 2026 14:31
…enyTools (#149)

ChannelPolicy joins the runtime seam: AgentSession's per-source map now
accepts { overlay, overlayMode, denyTools } beside the classic string.
overlayMode "replace" makes the overlay the turn's ENTIRE system prompt
(withSystem consumes systemMode; adapters only ever see a final system) —
the mechanical form of prose that begged 'disregard the above'. denyTools
filters at the same per-turn choke point as requiresPrincipal: unlisted to
the model AND undispatchable (a hallucinated call fails as unknown tool —
enforcement, not suggestion). Both persist through the suspend checkpoint,
so a resumed turn keeps its policy. Engine tests pin append/replace/bare-
string composition, per-source deny, and the failure contract.
…rivation (#149)

Behavior prose is agent-level, not transport-level: channels stay pure
transport (no overlay fields), and the per-source policies derive from
instructions.<source>.md variants (AgentModule.surfaceInstructions) plus
agent.ts surfaces mechanics ({ mode, denyTools }). Fail-loud edges: a mode
without its variant file throws at assembly; a source authored in both the
new pieces and the legacy channelInstructions map throws (one source of
truth); an orphan policy key warns when channel names are known. defineAgent
/assembleAgent/assembleDurable all derive through the one function, so
native discovery and the compiled module cannot drift.
…channels/<name>.md (#149)

Discovery and june gen pick up the agent-root surface variants
(instructions.<source>.md → AgentModule.surfaceInstructions); the DO's
policy map type widens to ChannelPolicy and the DO warns once when a
policy's source matches no resolved channel (the assembly can't check
factory names — the DO can). channels/<name>.md still works for one
dev-series but warns loudly at discovery AND at gen, pointing to the new
location. Fixture agent-ops moves to the new convention (slack surface
variant + surfaces.slack denyTools) — the compiled≡discovered parity test
covers the whole new path for free.
@linyiru
linyiru force-pushed the feat/channel-overlay-field branch from 79cbca0 to 453996e Compare July 31, 2026 14:32
@linyiru
linyiru changed the base branch from feat/agent-edge-channels to main July 31, 2026 14:32
…-rebase

The test landed on main from the #147 round with the pre-surfaces module
literal; AgentModule.surfaceInstructions is required now.
@linyiru
linyiru force-pushed the feat/channel-overlay-field branch from 453996e to 4501a71 Compare July 31, 2026 14:36
@linyiru
linyiru requested a review from Copilot July 31, 2026 14:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds agent-level, source-specific instructions and enforceable surface policies across native and Durable Object runtimes.

Changes:

  • Discovers and compiles instructions.<source>.md.
  • Adds append/replace prompt modes and per-turn tool denial.
  • Deprecates channels/<source>.md with warnings and compatibility support.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
.changeset/agent-surfaces.md Documents the new surface API.
packages/core/src/agent-config.ts Defines and assembles surface policies.
packages/core/src/agent-runtime.ts Enforces prompt modes and tool denial.
packages/core/test/agent-config.test.ts Tests policy derivation and validation.
packages/core/test/agent-runtime.test.ts Tests runtime policy enforcement.
packages/june/src/agent-compile.ts Compiles surface instruction variants.
packages/june/src/agent-discover.ts Discovers variants and warns on legacy overlays.
packages/june/src/agent-durable.ts Passes policies into Durable Object sessions.
packages/june/test/agent-compile.test.ts Tests surface variant scanning.
packages/june/test/agent-discover.test.ts Tests discovered surface policies.
packages/june/test/fixtures/agent-ops/_agent.gen.ts Updates generated fixture output.
packages/june/test/fixtures/agent-ops/agent.ts Configures the Slack surface policy.
packages/june/test/fixtures/agent-ops/instructions.slack.md Adds Slack-specific instructions.
Suppressed comments (1)

packages/core/src/agent-runtime.ts:563

  • An empty instructions.<source>.md is a valid discovered variant, but this truthiness check omits both the empty overlay and its replace mode. The turn therefore keeps the base prompt (or sends no per-turn system prompt) instead of using the variant as the entire prompt. Distinguish undefined from "" here.
  for await (const d of model(msgs, specs, systemOverlay ? { system: systemOverlay, ...(systemMode ? { systemMode } : {}) } : undefined)) {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/agent-runtime.ts Outdated
return (msgs, tools, opts) => {
const { systemMode, ...rest } = opts ?? {};
const finalSystem =
systemMode === "replace" && opts?.system ? opts.system : [system, opts?.system].filter(Boolean).join("\n\n");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in a041d5d. Both sites (withSystem's join and modelStep's opts gate) now key on !== undefined, so { system: "", systemMode: "replace" } yields an empty system prompt as documented. Pin test drives an empty replace overlay through a real turn — truthiness would have silently fallen back to the base.

Comment thread packages/core/src/agent-config.ts Outdated
// Agent-level surface instruction variants (#149): instructions.<source>.md
// prose, keyed by source. Composition/deny mechanics live in
// config.surfaces; assembly derives the per-source policies from both.
surfaceInstructions: Record<string, string>;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in 7049443. surfaceInstructions is optional — assembly already treated it that way (opts.surfaceInstructions?.[source]), so pre-existing generated and hand-authored modules stay source-compatible; new generations still emit {}.

Comment thread packages/core/src/agent-config.ts Outdated
Comment on lines +327 to +328
// each channel's own overlay/overlayMode/denyTools declarations (#149); the map form
// remains as the programmatic escape hatch. Applied by the runtime (AgentSession).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in 7049443. The comment now describes the actual v2 derivation (agent-level instructions..md + surfaces mechanics; channels declare nothing) instead of the withdrawn channel-declared design.

Comment thread .changeset/agent-surfaces.md Outdated

How an agent behaves when reached over a given surface is agent behavior, not transport configuration — two agents sharing one channel carry different behavior; the transport carries none. The `channels/<source>.md` overlay convention conflated the two (and failed its blind-guess test 8/10: it reads as deletable docs). It is replaced by two agent-level pieces:

**`instructions.<source>.md`** at the agent root is the surface's instruction variant: discovered by convention (and inlined by `june gen` for the edge), applied to turns whose inbound event arrives through that source — the file alone activates it, composing after the base instructions by default. The name is its own documentation: in the measurement round the shape scored 10/10 on the blind-guess test the old convention failed, 80/80 across the full prediction quiz, and zero divergence on how the locale grammar generalizes (`instructions.slack.zh-TW.md`).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in 296f397, both halves. The changeset now labels the locale filename as the measured grammar-generalization result and states plainly that only single-segment sources parse today (locale variants = planned follow-up). And because a variant-shaped filename sitting silently inert is the exact failure class this convention exists to kill, discovery AND june gen now warn loudly on unparsed instructions.*.md names — covered by a test (file ignored, warning names it).

linyiru added 3 commits July 31, 2026 14:52
…not truthiness

{ system: "", systemMode: "replace" } fell back to appending the base at
both the withSystem join and the modelStep opts gate, contradicting the
documented 'the variant IS the system prompt' contract. Both sites now key
on !== undefined; a pin test drives an empty replace overlay through a turn.

Refs #149
…n comment

Required-ness broke source compatibility for every module generated before
the field existed (and hand-authored ones) — assembly already treats it as
optional, so the type now does too; new generations still emit {}. The
AgentDefinition.channelInstructions comment still described the withdrawn
v1 design (channel-declared overlay/overlayMode/denyTools); it now points
at the agent-level derivation.

Refs #149
…locale wording

instructions.slack.zh-TW.md matched nothing and sat silently inert — the
exact failure class this convention exists to kill. Discovery and june gen
now warn loudly on variant-shaped filenames outside the single-segment
grammar, pointing at the planned locale follow-up; the changeset stops
presenting the locale filename as current behavior (it was the measured
grammar-generalization result, now labeled as such).

Refs #149

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (3)

packages/june/src/agent-discover.ts:64

  • This filename grammar is narrower than the source contract: both Channel.name and InboundEvent.source are unrestricted strings. A valid channel named, for example, teams.v2 can be matched by surfaces["teams.v2"], but its documented instructions.teams.v2.md variant is always classified as unrecognized and never applied. Either establish and enforce the same source-name restriction at the channel/event API, or define an unambiguous filename encoding that supports every valid source.
      if (!e.isFile() || !/^instructions\..+\.md$/.test(e.name)) continue;
      const m = e.name.match(/^instructions\.([a-z0-9_-]+)\.md$/);
      if (m) {
        surfaceInstructions[m[1]!] = await readFile(join(dir, e.name), "utf8");

packages/june/src/agent-compile.ts:263

  • The warning is unreachable when an unrecognized variant is the directory's only agent-shaped file: scanAgentDir excludes it, returns null, and this function exits on line 249 before inspecting the directory. Thus june gen silently ignores exactly the malformed-file case this block promises to report. Run this validation before the null return (and add a regression case without instructions.md).
  // A variant-shaped file the grammar doesn't parse yet must never be silently
  // inert (the same failure class this whole convention exists to kill).
  for (const e of readdirSync(dir, { withFileTypes: true })) {

packages/june/src/agent-compile.ts:69

  • The generated-module scanner also cannot represent surface instructions for valid channel sources outside [a-z0-9_-]+ (for example teams.v2), even though Channel.name and InboundEvent.source accept arbitrary strings. This makes native and edge consistently unable to use the new prose convention for those channels; align the source API and filename grammar or introduce an unambiguous encoding.
    surfaceVariants: readdirSync(dir, { withFileTypes: true })
      .filter((e) => e.isFile())
      .map((e) => e.name.match(/^instructions\.([a-z0-9_-]+)\.md$/))
      .filter((m): m is RegExpMatchArray => m !== null)
      .sort((a, b) => a[1]!.localeCompare(b[1]!))
      .map((m) => ({ source: m[1]!, raw: readFileSync(join(dir, m[0]!), "utf8") })),

@linyiru
linyiru merged commit 34e555a into main Jul 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Surface behavior v2: agent-level instructions.<source>.md + agent.ts surfaces replace the channels/<name>.md convention

2 participants