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
72 changes: 58 additions & 14 deletions packages/agent-core/src/harness/codex-harness-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { existsSync, readFileSync, statSync } from 'node:fs'
import { basename, extname } from 'node:path'
import { createLogger } from '@anton/logger'
import type { ChatImageAttachmentInput } from '@anton/protocol'
import { ANTON_MCP_NAMESPACE } from '../prompt-layers.js'
import type { SessionEvent } from '../session.js'
import { CodexRpcClient, CodexRpcError } from './codex-rpc.js'
import { PINNED_CLI_VERSION, detectCodexCli } from './codex-version.js'
Expand All @@ -45,6 +46,24 @@ export interface CodexHarnessSessionOpts {
systemPrompt?: string
/** Per-turn system-prompt builder — same contract as HarnessSession. */
buildSystemPrompt?: (userMessage: string, turnIndex: number) => Promise<string>
/**
* Capability block appended ONCE to `developerInstructions` at
* thread-start — ground truth for which connectors are live in THIS
* session. Built by the server from live connector state (see
* `buildHarnessCapabilityBlock`). `developerInstructions` is immutable
* for the life of a codex thread, so baking it in here is both
* sufficient and cheap; no per-turn re-injection.
*
* Empty string = no connectors live (still safe to pass — appended
* as-is and produces no extra text).
*/
capabilityBlock?: string
/**
* Stable ids of the connectors reflected in `capabilityBlock`, purely
* for telemetry: logged after `thread/start` succeeds so we know which
* services the model was told it has. Not otherwise used.
*/
capabilityConnectorIds?: string[]
/** Hook invoked after each turn with {userMessage, events}. */
onTurnEnd?: (turn: { userMessage: string; events: SessionEvent[] }) => void | Promise<void>
maxBudgetUsd?: number
Expand Down Expand Up @@ -187,6 +206,18 @@ export class CodexHarnessSession {
} else {
systemPromptForTurn = this.opts.systemPrompt
}

// First-turn title: truncated user question. Mirrors ChatGPT/Claude.ai —
// the UI gets a title immediately; `thread/name/updated` may later
// upgrade it to a server-generated smart title.
if (this.turnIndex === 0 && !this.title) {
const seed = userMessage.trim().slice(0, 60).split('\n')[0]
if (seed.length > 0) {
this.title = seed
yield { type: 'title_update', title: this.title }
}
}

this.turnIndex += 1

try {
Expand Down Expand Up @@ -252,18 +283,6 @@ export class CodexHarnessSession {
while (!queue.done || queue.events.length > 0) {
if (queue.events.length > 0) {
const ev = queue.events.shift()!

// Side effect: capture first text for auto-title.
if (!this.title && ev.type === 'text' && ev.content.length > 0) {
this.title = ev.content.slice(0, 60).split('\n')[0]
turnEvents.push(ev)
yield ev
const titleEv: SessionEvent = { type: 'title_update', title: this.title }
turnEvents.push(titleEv)
yield titleEv
continue
}

turnEvents.push(ev)
yield ev
} else if (!queue.done) {
Expand Down Expand Up @@ -483,6 +502,18 @@ export class CodexHarnessSession {
capabilities: null,
})

const baseInstructions = (systemPrompt ?? this.opts.systemPrompt ?? '').trim()
const capabilityBlock = (this.opts.capabilityBlock ?? '').trim()
// Explicit `\n\n` joiner — don't rely on the leading blank lines
// `systemReminder()` emits to separate the capability block from the
// prompt above it. Either piece may be empty; `filter(Boolean)` drops
// empties so we never leave a stray `\n\n` at the start or end.
const developerInstructionsJoined = [baseInstructions, capabilityBlock]
.filter((s) => s.length > 0)
.join('\n\n')
const developerInstructions =
developerInstructionsJoined.length > 0 ? developerInstructionsJoined : null

const threadStartParams: Record<string, unknown> = {
model: this.model,
modelProvider: null,
Expand All @@ -491,7 +522,7 @@ export class CodexHarnessSession {
sandbox: 'workspace-write',
config: this.buildConfig(),
baseInstructions: null,
developerInstructions: systemPrompt ?? this.opts.systemPrompt ?? null,
developerInstructions,
// Required fields in v2 — we do not opt into either.
experimentalRawEvents: false,
persistExtendedHistory: false,
Expand All @@ -510,6 +541,17 @@ export class CodexHarnessSession {
{ sessionId: this.id, threadId: this.threadId, model: this.model },
'codex app-server ready',
)
if (capabilityBlock.length > 0) {
log.info(
{
sessionId: this.id,
threadId: this.threadId,
capabilityBlockChars: capabilityBlock.length,
liveConnectorIds: this.opts.capabilityConnectorIds ?? [],
},
'capability block installed via thread/start',
)
}
} catch (err) {
const message = err instanceof CodexRpcError ? err.message : (err as Error).message
this.rpc?.close('init-failed')
Expand All @@ -530,10 +572,12 @@ export class CodexHarnessSession {
ANTON_SESSION: this.id,
ANTON_AUTH: this.opts.authToken,
}
// The namespace Codex uses to prefix our tools (e.g. `anton:gmail_*`)
// MUST match the value the identity + capability blocks reference.
return {
model_reasoning_summary: 'detailed',
mcp_servers: {
anton: {
[ANTON_MCP_NAMESPACE]: {
command: 'node',
args: [this.opts.shimPath],
env: mcpEnv,
Expand Down
3 changes: 3 additions & 0 deletions packages/agent-core/src/harness/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ export {
// use buildHarnessContextPrompt when they need the full harness context
// string; it lives next to Session.getSystemPrompt in prompt-layers.ts.
export {
ANTON_MCP_NAMESPACE,
buildHarnessCapabilityBlock,
buildHarnessContextPrompt,
buildHarnessIdentityBlock,
type HarnessContextPromptOpts,
type LiveConnectorSummary,
type WorkflowEntry,
} from '../prompt-layers.js'
export {
Expand Down
8 changes: 8 additions & 0 deletions packages/agent-core/src/harness/mcp-ipc-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ export function createMcpIpcServer(socketPath: string, provider: IpcToolProvider
switch (request.method) {
case 'tools/list': {
const tools = provider.getTools(sessionId)
log.info(
{
sessionId,
toolCount: tools.length,
toolNames: tools.map((t) => t.name),
},
'tools/list served to harness',
)
result = {
tools: tools.map((t) => ({
name: t.name,
Expand Down
53 changes: 53 additions & 0 deletions packages/agent-core/src/harness/tool-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ export interface AntonToolRegistryOpts {
* same ConnectorManager the Pi SDK uses — no per-backend duplication.
*/
connectorManager?: { getAllTools(surface?: string): AgentTool[] }
/**
* External MCP server tools (user-added MCP connectors). Mirrors what
* the Pi SDK path does in `buildTools()` — without this, harness
* sessions would silently never see any external MCP tool, even when
* the server is connected. Tool names are already namespaced with
* `mcp_<id>_` by McpManager, so they do not collide with Anton's core
* tools or connector tools.
*
* Accepts an optional `surface` hint so MCP configs with a
* `surfaces` allowlist stay out of surfaces they were not authorized
* for. Undefined `surfaces` on the config = all surfaces (default).
*/
mcpManager?: { getAllTools(surface?: string): AgentTool[] }
/**
* Called on every tools/list and tools/call to resolve the session's
* context (projectId, surface, handlers). Returning undefined means
Expand All @@ -169,10 +182,12 @@ export interface AntonToolRegistryOpts {
*/
export class AntonToolRegistry implements IpcToolProvider {
private connectorManager?: AntonToolRegistryOpts['connectorManager']
private mcpManager?: AntonToolRegistryOpts['mcpManager']
private getSessionContext?: AntonToolRegistryOpts['getSessionContext']

constructor(opts: AntonToolRegistryOpts = {}) {
this.connectorManager = opts.connectorManager
this.mcpManager = opts.mcpManager
this.getSessionContext = opts.getSessionContext
}

Expand Down Expand Up @@ -203,6 +218,8 @@ export class AntonToolRegistry implements IpcToolProvider {
map.set(tool.name, agentToolToMcpDefinition(tool))
}

const coreCount = map.size
let connectorToolCount = 0
if (this.connectorManager) {
const connectorTools = this.connectorManager.getAllTools(ctx?.surface)
for (const tool of connectorTools) {
Expand All @@ -215,12 +232,48 @@ export class AntonToolRegistry implements IpcToolProvider {
)
}
map.set(def.schema.name, def)
connectorToolCount += 1
} catch (err) {
log.warn({ err, name: tool.name, sessionId }, 'failed to adapt connector tool — skipping')
}
}
}

let mcpToolCount = 0
if (this.mcpManager) {
const mcpTools = this.mcpManager.getAllTools(ctx?.surface)
for (const tool of mcpTools) {
try {
const def = agentToolToMcpDefinition(tool)
if (map.has(def.schema.name)) {
log.warn(
{ name: def.schema.name, sessionId },
'mcp tool name collides with an existing tool — overriding',
)
}
map.set(def.schema.name, def)
mcpToolCount += 1
} catch (err) {
log.warn({ err, name: tool.name, sessionId }, 'failed to adapt mcp tool — skipping')
}
}
}

log.info(
{
sessionId,
projectId: ctx?.projectId,
surface: ctx?.surface,
coreToolCount: coreCount,
connectorToolCount,
mcpToolCount,
totalToolCount: map.size,
hasConnectorManager: Boolean(this.connectorManager),
hasMcpManager: Boolean(this.mcpManager),
},
'built harness tool map',
)

return map
}

Expand Down
4 changes: 4 additions & 0 deletions packages/agent-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type { DeliverResultHandler } from './tools/deliver-result.js'
export {
McpClient,
McpManager,
matchesSurface,
type McpServerConfig,
type ConnectorStatus,
type McpToolPermission,
Expand Down Expand Up @@ -82,8 +83,11 @@ export {
AntonToolRegistry,
type AntonToolRegistryOpts,
type HarnessSessionContext,
ANTON_MCP_NAMESPACE,
buildHarnessCapabilityBlock,
buildHarnessContextPrompt,
type HarnessContextPromptOpts,
type LiveConnectorSummary,
type WorkflowEntry,
synthesizeHarnessTurn,
ensureHarnessSessionInit,
Expand Down
7 changes: 6 additions & 1 deletion packages/agent-core/src/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export { McpClient, type McpServerConfig, type McpTool, type McpToolResult } from './mcp-client.js'
export { McpManager, type ConnectorStatus, type McpToolPermission } from './mcp-manager.js'
export {
McpManager,
matchesSurface,
type ConnectorStatus,
type McpToolPermission,
} from './mcp-manager.js'
export { mcpToolToAgentTool, mcpClientToAgentTools } from './mcp-tool-adapter.js'
export { jsonSchemaToTypebox } from './json-schema-to-typebox.js'
Loading