Skip to content
← Public packages

@kody/codex

Create and manage OpenAI Agents API (Codex harness) cloud agent sessions.

src/sessions/input.ts

105 lines · 3.1 KB · TypeScript
import { any, boolean, object, optional, parse, string } from 'remix/data-schema'
import {
	DOCS_SESSIONS,
	agentsFetch,
	pickAuth,
	requireSessionId,
	userTextMessage,
} from '../client.ts'

const inputSchema = object(
	{
		sessionId: string(),
		/** Plain text follow-up — wrapped as agent.session.input.message */
		text: optional(string()),
		/** When true, send agent.session.input.cancel instead of a message */
		cancel: optional(boolean()),
		/**
		 * Full `events` array for the POST body when you need function results
		 * or custom event shapes. Mutually exclusive with text/cancel shortcuts.
		 */
		events: optional(any()),
		apiKeySecret: optional(string()),
		dryRun: optional(boolean()),
	},
	{ unknownKeys: 'error' },
)

/**
 * Send input or cancel on an existing session
 * (`POST /v1/agents/sessions/{session_id}/events`).
 * Prefer opening `./events/stream` before follow-up so early events are not
 * missed. `text` steers an active turn or starts a new one when idle.
 *
 * @param raw.sessionId - Session id
 * @param raw.text - User follow-up text (shortcut for input.message)
 * @param raw.cancel - Send turn cancel instead of a message
 * @param raw.events - Full events array when not using text/cancel
 * @param raw.dryRun - Preview without calling OpenAI
 * @param raw.apiKeySecret - Optional alternate secret name
 * @returns API result or dry-run preview
 *
 * @example
 * import sendInput from 'kody:@kody/codex/sessions/input'
 * await sendInput({
 *   sessionId: 'sess_123',
 *   text: 'Add a max-depth option to tree.py, run it, show output.',
 * })
 */
export default async function sendInput(raw: unknown = {}) {
	const input = parse(inputSchema, raw ?? {})
	const auth = pickAuth(input)
	const sessionId = requireSessionId(input.sessionId)
	const path = `/agents/sessions/${encodeURIComponent(sessionId)}/events`

	let events: unknown[]
	if (input.events !== undefined) {
		if (!Array.isArray(input.events)) {
			throw new Error('events must be an array of Agents API input events.')
		}
		if (input.text !== undefined || input.cancel === true) {
			throw new Error('Pass either events or text/cancel, not both.')
		}
		events = input.events
	} else if (input.cancel === true) {
		events = [{ type: 'agent.session.input.cancel' }]
	} else if (input.text !== undefined) {
		const text = String(input.text).trim()
		if (!text) throw new Error('text must be a non-empty string.')
		events = [
			{
				type: 'agent.session.input.message',
				input: userTextMessage(text),
			},
		]
	} else {
		throw new Error('Provide text, cancel: true, or an events array.')
	}

	const body = { events }

	if (input.dryRun === true) {
		return {
			dryRun: true as const,
			method: 'POST' as const,
			path: `/v1${path}`,
			sessionId,
			body,
			docs: DOCS_SESSIONS,
			note: 'Open events/stream on this session before sending live follow-up when you need early events.',
		}
	}

	const response = await agentsFetch(path, {
		...auth,
		method: 'POST',
		body,
	})
	return {
		ok: true,
		sessionId,
		result: response.body,
		status: response.status,
		hint: 'If you need live progress, call events/stream before the next input.',
	}
}