Skip to content
Draft
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
15 changes: 15 additions & 0 deletions packages/core/src/v1/config/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ const KNOWN_KEYS = new Set([
"tools",
])

// altimate_change start — callers can name the top-level keys that `normalize` folds into `options`
// The fold is a published contract: `AgentConfig` allows additional properties, an agent's
// `audience` is read from `options`, and legacy markdown agents carry provider request options
// as top-level keys that `migrateAgent` moves into `request.body`. It stays; the loaders log
// the keys it folded so a typo (`budget` for `steps`) is no longer silent.
/** Top-level keys the agent schema interprets; every other top-level key is folded into `options`. */
export const knownKeys: readonly string[] = [...KNOWN_KEYS]

/** The top-level keys of a raw agent config that `normalize` folds into `options`. */
export function unknownKeys(agent: unknown): string[] {
if (typeof agent !== "object" || agent === null || Array.isArray(agent)) return []
return Object.keys(agent).filter((key) => !KNOWN_KEYS.has(key))
}
// altimate_change end

const normalize = (agent: Schema.Schema.Type<typeof AgentSchema>): Schema.Schema.Type<typeof AgentSchema> => {
const options: Record<string, unknown> = { ...agent.options }
for (const [key, value] of Object.entries(agent)) {
Expand Down
37 changes: 37 additions & 0 deletions packages/core/test/config/agent-v1-keys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { ConfigAgentV1 } from "../../src/v1/config/agent"

const decode = Schema.decodeUnknownSync(ConfigAgentV1.Info)

describe("ConfigAgentV1 top-level keys", () => {
test("a known key is accepted as itself", () => {
const agent = decode({ model: "test/model", steps: 3, description: "Known keys only" })
expect(agent).toMatchObject({ model: "test/model", steps: 3, description: "Known keys only" })
expect(agent.options).toEqual({})
expect(ConfigAgentV1.unknownKeys({ model: "test/model", steps: 3 })).toEqual([])
})

test("an unknown top-level key is folded into options and can be named", () => {
const raw = { model: "test/model", budget: 5, audience: "executive" }
const agent = decode(raw)
expect(agent.options).toEqual({ budget: 5, audience: "executive" })
expect(agent).not.toHaveProperty("steps")
expect(ConfigAgentV1.unknownKeys(raw)).toEqual(["budget", "audience"])
})

test("keys nested under options keep working and are not reported", () => {
const raw = { model: "test/model", options: { audience: "executive", reasoningEffort: "high" } }
const agent = decode(raw)
expect(agent.options).toEqual({ audience: "executive", reasoningEffort: "high" })
expect(ConfigAgentV1.unknownKeys(raw)).toEqual([])
})

test("the known-key list is the schema's own", () => {
for (const key of ["name", "model", "variant", "prompt", "steps", "maxSteps", "options", "permission", "tools"]) {
expect(ConfigAgentV1.knownKeys).toContain(key)
}
expect(ConfigAgentV1.unknownKeys(undefined)).toEqual([])
expect(ConfigAgentV1.unknownKeys(["budget"])).toEqual([])
})
})
79 changes: 57 additions & 22 deletions packages/opencode/bin/altimate-code
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ function findAllNodeModules(startDir) {
return paths
}

function hasControllingTerminal() {
// A controlling terminal delivers Ctrl-C (SIGINT) and hangup (SIGHUP) to the
// whole foreground process group, so the child already has them. Opening
// /dev/tty is the portable test for one; it fails in CI, under harnesses,
// and after setsid, where the wrapper is the only process that was signalled.
if (process.platform === "win32") return true
try {
fs.closeSync(fs.openSync("/dev/tty", "r"))
return true
} catch {
return false
}
}

function run(target) {
// Resolve NODE_PATH so the compiled Bun binary can find external packages
// installed alongside the wrapper (e.g. @altimateai/altimate-core NAPI module).
Expand Down Expand Up @@ -54,37 +68,58 @@ function run(target) {
}
} catch {
// realpathSync failed (e.g. target doesn't exist) — continue without
// NODE_PATH; spawnSync will report the missing binary via result.error.
// NODE_PATH; spawn will report the missing binary via the "error" event.
}

const result = childProcess.spawnSync(target, process.argv.slice(2), {
// Asynchronous spawn so this process can forward termination signals: with
// spawnSync a `kill <wrapper pid>` ended the wrapper alone and left the
// binary running as an orphan (observed: a run kept going past its budget).
const child = childProcess.spawn(target, process.argv.slice(2), {
stdio: "inherit",
env,
})
if (result.error) {
console.error(result.error.message)
process.exit(1)
}
// A child killed by a signal (e.g. the kernel OOM killer) has no exit status; report it the way
// a shell would (128 + signal number) instead of exiting 0 as if the run had succeeded.
if (result.signal) {
console.error(`altimate-code terminated by ${result.signal}`)
process.exit(128 + (os.constants.signals[result.signal] ?? 1))
const forward = (signal) => {
try {
child.kill(signal)
} catch {
// The child already exited; its "exit" handler below decides the status.
}
}
const code = typeof result.status === "number" ? result.status : 1
process.exit(code)
// SIGTERM is never terminal-generated, so it is always ours to deliver.
process.on("SIGTERM", () => forward("SIGTERM"))
// SIGINT and SIGHUP reach the child directly from a controlling terminal;
// forwarding them again would deliver a second signal, which `run` treats as
// "exit now" and skips its shutdown flush. Keep waiting for the child either
// way so its exit status is the wrapper's.
const terminal = hasControllingTerminal()
process.on("SIGINT", () => {
if (!terminal) forward("SIGINT")
})
process.on("SIGHUP", () => {
if (!terminal) forward("SIGHUP")
})
child.on("error", (error) => {
console.error(error.message)
process.exit(1)
})
child.on("exit", (status, signal) => {
// A child killed by a signal (e.g. the kernel OOM killer) has no exit status; report it the way
// a shell would (128 + signal number) instead of exiting 0 as if the run had succeeded.
if (signal) {
console.error(`altimate-code terminated by ${signal}`)
process.exit(128 + (os.constants.signals[signal] ?? 1))
}
const code = typeof status === "number" ? status : 1
process.exit(code)
})
}

// run() returns while the child is still running, so every branch below must
// be the only one taken: the first match wins, and the fallback search runs
// only when neither override is present.
const envPath = process.env.ALTIMATE_CODE_BIN_PATH
if (envPath) {
run(envPath)
}

//
const cached = path.join(scriptDir, ".altimate-code")
if (fs.existsSync(cached)) {
run(cached)
}
const override = envPath || (fs.existsSync(cached) ? cached : undefined)

const platformMap = {
darwin: "darwin",
Expand Down Expand Up @@ -222,7 +257,7 @@ function findBinary(startDir) {
}
}

const resolved = findBinary(scriptDir)
const resolved = override ?? findBinary(scriptDir)
if (!resolved) {
console.error(
"It seems that your package manager failed to install the right version of the altimate-code CLI for your platform. You can try manually installing " +
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/altimate/observability/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ export interface RunBudgetMetadata {
/** Unix milliseconds, matching generation and span timestamps. */
effort_downgraded_at?: number
generation_interrupted_at?: number
/** `--max-tokens` accounting; present only when a token budget is set. */
max_tokens?: number
tokens_used?: number
token_budget_exhausted?: boolean
}

interface TracePartIdentity {
Expand Down
103 changes: 103 additions & 0 deletions packages/opencode/src/cli/cmd/run-token-budget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// altimate_change start — fork-only token budget for `run` (--max-tokens)
// Sums the billed tokens of a run across its root session and every session
// descended from it (task children, and their children), and says when the
// running total reaches the limit. Pure: it observes the SDK event stream and
// never talks to the server, so the decision is testable without one.

/** Token counts as a `step-finish` part reports them. */
export type StepTokens = {
input: number
output: number
cache: { read: number; write: number }
}

/** Billed weight of one step: a cache read costs a tenth of an input token. */
export function billedTokens(tokens: StepTokens): number {
return tokens.input + tokens.output + tokens.cache.write + 0.1 * tokens.cache.read
}

/** A `--max-tokens` value the budget can enforce: a positive integer. */
export function isValidMaxTokens(value: unknown): boolean {
return typeof value === "number" && Number.isInteger(value) && value >= 1
}

type Event = { type: string; properties: Record<string, any> }

export function create(rootSessionID: string, limit: number | undefined) {
// Sessions known to descend from the root. A child announces itself through
// `session.created`/`session.updated` (parentID) or through the parent's
// task tool part (state.metadata.sessionId); either may arrive first, so
// edges are kept until their parent is admitted.
const sessions = new Set<string>([rootSessionID])
const parents = new Map<string, string>()
const counted = new Set<string>()
let total = 0
let exhausted = false

function admit() {
let changed = true
while (changed) {
changed = false
for (const [id, parentID] of parents) {
if (sessions.has(id) || !sessions.has(parentID)) continue
sessions.add(id)
changed = true
}
}
}

function link(id: unknown, parentID: unknown) {
if (typeof id !== "string" || typeof parentID !== "string" || !id || !parentID) return
if (id === rootSessionID) return
parents.set(id, parentID)
admit()
}

return {
get total() {
return total
},
get exhausted() {
return exhausted
},
get limit() {
return limit
},
isDescendant(sessionID: string) {
return sessions.has(sessionID)
},
/**
* Feed one SDK event. Returns true exactly once: on the step whose usage
* takes the total to (or past) the limit. Without a limit it only counts.
*/
observe(event: Event): boolean {
const props = event.properties
if (!props) return false
if (event.type === "session.created" || event.type === "session.updated") {
link(props.info?.id, props.info?.parentID)
}
if (event.type !== "message.part.updated") return false
const part = props.part
if (!part || typeof part.sessionID !== "string") return false
if (part.type === "tool" && part.tool === "task") {
link(part.state?.metadata?.sessionId ?? part.state?.input?.task_id, part.sessionID)
}
if (part.type !== "step-finish" || !sessions.has(part.sessionID)) return false
const tokens = part.tokens as Partial<StepTokens> | undefined
if (!tokens || typeof part.id !== "string" || counted.has(part.id)) return false
counted.add(part.id)
total += billedTokens({
input: tokens.input ?? 0,
output: tokens.output ?? 0,
cache: { read: tokens.cache?.read ?? 0, write: tokens.cache?.write ?? 0 },
})
if (limit === undefined || exhausted || total < limit) return false
exhausted = true
return true
},
}
}
export type Info = ReturnType<typeof create>

export * as RunTokenBudget from "./run-token-budget"
// altimate_change end
Loading
Loading