Skip to content

Commit fcb7002

Browse files
authored
Merge pull request #3195 from useautumn/charlie/leaf-flatten-cleanup
Cleanup for the flatten: dead park code, contradiction fixes, one owner per rule
2 parents a268b82 + fa32c0d commit fcb7002

192 files changed

Lines changed: 1649 additions & 3564 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/leaf/agent/agent.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ const workflowWorld =
2222

2323
export default defineAgent({
2424
build: { externalDependencies: ["@vercel/otel"] },
25-
model: leafModel("orchestrator"),
26-
modelContextWindowTokens: leafModelContextWindowTokens("orchestrator"),
27-
reasoning: leafReasoning("orchestrator"),
25+
model: leafModel("leaf"),
26+
modelContextWindowTokens: leafModelContextWindowTokens("leaf"),
27+
reasoning: leafReasoning("leaf"),
2828
...(workflowWorld
2929
? {
3030
experimental: {

apps/leaf/agent/channels/eve.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,12 @@ const leafInternalAuth = (): AuthFn<Request> => async (request) => {
3232
if (chatInstallationId) attributes.chatInstallationId = chatInstallationId;
3333
const autumnUserId = request.headers.get("x-leaf-autumn-user-id");
3434
if (autumnUserId) attributes.autumnUserId = autumnUserId;
35-
for (const [key, header] of [
36-
["orgCatalog", "x-leaf-org-catalog"],
37-
["orgInstructions", "x-leaf-org-instructions"],
38-
] as const) {
39-
const encoded = request.headers.get(header);
40-
if (encoded) {
41-
attributes[key] = Buffer.from(encoded, "base64url").toString("utf8");
42-
}
35+
const orgInstructions = request.headers.get("x-leaf-org-instructions");
36+
if (orgInstructions) {
37+
attributes.orgInstructions = Buffer.from(
38+
orgInstructions,
39+
"base64url",
40+
).toString("utf8");
4341
}
4442

4543
return {

apps/leaf/agent/instructions.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,5 @@ import { leafAgentPrompt } from "@autumn/agent-docs/agent";
22
import { defineInstructions } from "eve/instructions";
33

44
export default defineInstructions({
5-
markdown: [
6-
leafAgentPrompt("orchestrator"),
7-
"Autumn knowledge is available through Eve skills. Load the autumn-concepts skill only when the user asks a conceptual pricing-model question you cannot answer from the preloaded context.",
8-
].join("\n\n"),
5+
markdown: leafAgentPrompt("leaf"),
96
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { inlinedSkill } from "../lib/inlinedSkill.js";
2+
3+
export default inlinedSkill({ name: "billing" });

apps/leaf/agent/instrumentation.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,10 @@ import { BraintrustExporter } from "@braintrust/otel";
22
import { registerOTel } from "@vercel/otel";
33
import { defineInstrumentation } from "eve/instrumentation";
44

5-
/** Braintrust traces the model loop — prompts, tool calls and results, token
6-
* usage, subagent spans. Axiom keeps session lifecycle (parks, reconnects,
7-
* cursors); the two join on the session id, which eve already puts on every
8-
* span as `eve.session.id` — the same value Axiom logs as `session_id`.
9-
*
10-
* Without BRAINTRUST_API_KEY no exporter is registered, so a missing key
11-
* costs traces, never turns. */
5+
// Braintrust traces the model loop; Axiom keeps session lifecycle. The two
6+
// join on `eve.session.id` = Axiom's `session_id`. No key → no traces, never no turns.
127

13-
/** A laptop's turns must not land in the project used to investigate
14-
* production incidents. Matches how the agent identifies production
15-
* elsewhere, so the two can never disagree. */
8+
// A laptop's turns must not land in the production-incident Braintrust project.
169
const projectName = () => {
1710
if (process.env.BRAINTRUST_PROJECT) return process.env.BRAINTRUST_PROJECT;
1811
const isProduction =

apps/leaf/agent/lib/agentSkills.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {
2+
type LeafAgentId,
3+
leafSkillsFor,
4+
type Skill,
5+
skillToText,
6+
} from "@autumn/agent-docs/agent";
7+
import { defineDynamic, defineSkill } from "eve/skills";
8+
9+
/** Loadable skill bundle from agent-docs; a skill named in `inlined` is
10+
* already in the prompt, so it is not offered as loadable. */
11+
export const agentSkills = ({
12+
agent,
13+
inlined = [],
14+
}: {
15+
agent: LeafAgentId;
16+
inlined?: readonly string[];
17+
}) => {
18+
const bundle: Skill[] = leafSkillsFor(agent).filter(
19+
(skill) => !inlined.includes(skill.name),
20+
);
21+
return defineDynamic({
22+
events: {
23+
"session.started": () =>
24+
Object.fromEntries(
25+
bundle.map((skill) => [
26+
skill.name,
27+
defineSkill({
28+
description: skill.description,
29+
markdown: skillToText(skill),
30+
}),
31+
]),
32+
),
33+
},
34+
});
35+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { APPROVAL_DESCRIPTION_KEY } from "../../src/internal/approvals/utils/approvalDescription.js";
2+
import type { JsonSchemaObject } from "../../src/internal/autumnMcp/rpcClient.js";
3+
4+
const APPROVAL_DESCRIPTION_TEXT =
5+
"Required. Write the walkthrough posted immediately after the approval card. " +
6+
"Open with one short line naming the change, then a bullet per change describing what happens to the customer. " +
7+
"Cover the material interpretation or default you chose, any customer-state edge case that affects the decision, " +
8+
"and non-obvious request settings. A change with a preview must state its exact next-payment amount and date. " +
9+
"Use only facts from the request, billing docs, and tool results, and never ask for confirmation. " +
10+
"Use plain English: say 'a draft invoice will be created', never 'invoice mode', 'draft invoice mode', or 'enabled'. " +
11+
"Never describe a requested write unless you issue it in the same batch. " +
12+
"For grouped writes, put the same complete description on every write.";
13+
14+
export const withApprovalDescriptionSchema = (
15+
schema: JsonSchemaObject,
16+
): JsonSchemaObject => ({
17+
...schema,
18+
properties: {
19+
...((schema.properties as JsonSchemaObject | undefined) ?? {}),
20+
[APPROVAL_DESCRIPTION_KEY]: {
21+
description: APPROVAL_DESCRIPTION_TEXT,
22+
minLength: 1,
23+
type: "string",
24+
},
25+
},
26+
required: [
27+
...new Set([
28+
...((schema.required as string[] | undefined) ?? []),
29+
APPROVAL_DESCRIPTION_KEY,
30+
]),
31+
],
32+
});

apps/leaf/agent/lib/approvalSets.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ const setForAgent = (agent: LeafAgentConnection): ReadonlySet<string> =>
99
);
1010

1111
export const approvalSets: Record<LeafAgentConnection, ReadonlySet<string>> = {
12-
billing: setForAgent("billing"),
1312
catalog: setForAgent("catalog"),
14-
investigator: setForAgent("investigator"),
15-
orchestrator: setForAgent("orchestrator"),
13+
leaf: setForAgent("leaf"),
1614
};
1715

1816
for (const write of GATED_WRITES) {

apps/leaf/agent/lib/approvalSummarySchema.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

apps/leaf/agent/lib/autumnDirectTools.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { defineDynamic, defineTool } from "eve/tools";
2-
import { withoutApprovalSummary } from "../../src/internal/approvals/utils/approvalSummary.js";
3-
import { settledWriteResult } from "../../src/internal/approvals/utils/settledWriteResult.js";
42
import { callAutumnMcpTool } from "../../src/internal/autumnMcp/rpcClient.js";
3+
import { withApprovalDescriptionSchema } from "./approvalDescriptionSchema.js";
54
import { approvalSets } from "./approvalSets.js";
6-
import { withApprovalSummarySchema } from "./approvalSummarySchema.js";
75
import {
86
type LeafPrincipalAttributes,
97
mintCachedAutumnToken,
@@ -12,6 +10,27 @@ import { leafMcpBaseUrl, serverToolMetadata } from "./autumnToolMetadata.js";
1210
import { type LeafAgentConnection, toolAllowlists } from "./toolAllowlists.js";
1311
import { slimToolSchema } from "./toolSchemaSlim.js";
1412

13+
// Billing decisions need payment method + plans up front; strict validation
14+
// means the expand must live inside `request`, not beside it.
15+
const DEFAULT_CUSTOMER_EXPAND = ["payment_method", "subscriptions.plan"];
16+
17+
export const withCustomerExpand = (input: Record<string, unknown>) => {
18+
const request = input.request;
19+
if (!request || typeof request !== "object" || Array.isArray(request)) {
20+
return input;
21+
}
22+
const fields = request as Record<string, unknown>;
23+
if (fields.expand !== undefined) return input;
24+
return { ...input, request: { ...fields, expand: DEFAULT_CUSTOMER_EXPAND } };
25+
};
26+
27+
/** What a gated write returns to the model. The card is the user's decision
28+
* point, so the turn ends here rather than waiting on it. */
29+
const RECORDED_FOR_APPROVAL =
30+
"Recorded for approval. The user sees an approval card with the exact " +
31+
"change and applies it from there. Do not call this write again, and do " +
32+
"not tell the user it has been applied.";
33+
1534
/** Pre-registers the agent's allowlisted Autumn tools on every step with the
1635
* exact server schemas, so no discovery round trip is needed. */
1736
export const autumnDirectTools = ({
@@ -40,36 +59,28 @@ export const autumnDirectTools = ({
4059
const qualified = `autumn__${tool.name}`;
4160
const toolName = tool.name;
4261
const requiresApproval = approvalToolNames.has(toolName);
43-
const gatedBillingWrite = agent === "billing" && requiresApproval;
4462
const inputSchema = slimToolSchema(tool.inputSchema);
4563
entries[qualified] = defineTool({
46-
approval: () =>
47-
requiresApproval ? "user-approval" : "not-applicable",
64+
// Gated writes record and end the turn; leaf applies on approval.
65+
approval: () => "not-applicable",
4866
description: tool.description,
4967
execute: async (input, toolCtx) => {
50-
if (requiresApproval) {
51-
const settled = await settledWriteResult({
52-
input: input as Record<string, unknown>,
53-
sessionId: toolCtx.session.id,
54-
toolName,
55-
});
56-
if (settled !== undefined) return settled;
57-
}
68+
if (requiresApproval) return RECORDED_FOR_APPROVAL;
5869
const minted = await mintCachedAutumnToken(
5970
toolCtx.session.auth.current?.attributes,
6071
);
72+
const args = input as Record<string, unknown>;
6173
return callAutumnMcpTool({
62-
args: gatedBillingWrite
63-
? withoutApprovalSummary(input as Record<string, unknown>)
64-
: (input as Record<string, unknown>),
74+
args:
75+
toolName === "getCustomer" ? withCustomerExpand(args) : args,
6576
baseUrl: leafMcpBaseUrl(),
6677
env: minted.appEnv,
6778
token: minted.accessToken,
6879
toolName,
6980
});
7081
},
71-
inputSchema: gatedBillingWrite
72-
? withApprovalSummarySchema(inputSchema)
82+
inputSchema: requiresApproval
83+
? withApprovalDescriptionSchema(inputSchema)
7384
: inputSchema,
7485
});
7586
}

0 commit comments

Comments
 (0)