/**
 * agent-sdk-to-chunks.ts — Claude Agent SDK event-stream → chunks substrate.
 *
 * The load-bearing seam for Phase C: the agent runtime is
 * `@anthropic-ai/claude-agent-sdk` consuming the operator's LOCAL Claude
 * Code subscription (D15). Each event the SDK emits from `query({...})`
 * becomes a chunk row written via host-cp's `POST /v1/chunks` bearer
 * endpoint. The sidecar server-injects `actor_id` + `actor_type` from
 * the bearer principal (T3 mitigation; verified in Phase B B1/PB2).
 *
 * Design notes:
 *
 *   - The adapter accepts an `AsyncIterable<SdkLikeMessage>` (NOT the
 *     full SDKMessage union) so it's testable with mock iterables
 *     without booting the real SDK. The Claude Agent SDK's `query()`
 *     returns an iterable matching this shape; the test harness
 *     supplies synthetic iterables.
 *
 *   - `postChunk` is injected so the adapter is HTTP-transport-agnostic.
 *     Production wires it to `fetch(host-cp/v1/chunks, { bearer, body })`.
 *     Tests pass a recording mock that captures the chunk rows.
 *
 *   - SDK message handling (the load-bearing mapping):
 *       - 'assistant' (SDKAssistantMessage) → extract `message.content`
 *         array; emit one chunk per content block.
 *           - text → kind='text', chunk=<text>
 *           - tool_use → kind='tool-call', chunk=<tool_name + input JSON>
 *           - other content blocks → kind='text' fallback (preserve text repr)
 *       - 'result' (SDKResultMessage) → end of turn; flush + return.
 *       - 'system' → skip (auth, session-state, hooks — not user-visible).
 *       - 'stream_event' (SDKPartialAssistantMessage) → SKIPPED for v1.
 *         Phase C C1 ships at the assistant-message granularity (not
 *         per-token streaming) because: (a) assistant messages already
 *         arrive incrementally per content block; (b) per-token writes
 *         would 10-100× the chunk count and may violate the K3 budget
 *         under sustained load; (c) operator UX renders at the message
 *         level via assistant-ui's `Thread` + `MultiAuthorMessage`
 *         (Phase C C0). Per-token streaming is a deliberate C+1
 *         follow-up if the demo arc needs sub-second token visibility.
 *
 *   - Sequence numbering: caller provides `messageId` (unique per turn)
 *     and `seqStart` (typically 0). The adapter increments `seq` per
 *     emitted chunk; (messageId, seq) is the substrate's primary key
 *     per Phase A A1's @olam/chunks schema. Atomic at the adapter
 *     scope (single async loop; no concurrent writes per (messageId)).
 *
 *   - `audit:auth-callers` posture: the SDK uses the operator's local
 *     Claude Code subscription (D15); it does NOT hit
 *     `fetch('api.anthropic.com')` from this adapter's call path. The
 *     adapter ITSELF only calls `postChunk` (a local HTTP POST to
 *     host-cp on host.docker.internal:3112). CLAUDE.md gets a one-
 *     paragraph clarification per D14 same-commit discipline.
 *
 * Source: docs/plans/olam-plan-chat-chunks-substrate/phase-c-tasks.md (C1)
 */
/**
 * Drain an SDK event stream and emit chunks for each message. Returns a
 * summary; never throws on individual-message issues (logs + continues).
 * Caller's `postChunk` failures DO propagate — substrate writes are the
 * load-bearing surface; silent loss is not acceptable.
 */
export async function streamSdkToChunks(input) {
    const { messages, worldId, sessionId, messageId, postChunk } = input;
    const seqStart = input.seqStart ?? 0;
    const now = input.now ?? (() => new Date());
    let seq = seqStart;
    let chunksEmitted = 0;
    let resultObserved = false;
    let hadError = false;
    for await (const msg of messages) {
        if (msg.type === 'assistant') {
            const assistant = msg;
            const content = assistant.message?.content ?? [];
            // B3: capture usage from the assistant message; attach to the first
            // non-null chunk emitted for this assistant turn so host-cp can
            // write a message_usage row. Subsequent chunks within the same turn
            // do NOT carry usage (one usage block per assistant API response).
            const turnUsage = assistant.message?.usage;
            let usageAttached = false;
            // PERF: stamp `created_at` ONCE per assistant message rather than per
            // content block. The blocks of a single assistant message are drained
            // in this synchronous inner loop, so a wall-clock `now()` already
            // returns the same millisecond for every block — the per-block call was
            // re-allocating a `Date` + re-formatting an ISO string (~0.33µs each)
            // that resolved to the identical value. It's also dead work for the
            // persisted substrate: host-cp's `INSERT INTO chunks` omits `created_at`
            // and lets the column default to `clock_timestamp()`, so the adapter's
            // stamp is never stored. Hoisting collapses N Date allocations to 1 per
            // message while keeping per-message timestamp distinction and the
            // injected-clock contract (fixed `now` → byte-identical output).
            const createdAt = now().toISOString();
            for (const block of content) {
                const draft = blockToChunkDraft(block, worldId, sessionId, messageId, seq, createdAt);
                if (draft === null)
                    continue;
                const draftWithUsage = !usageAttached && turnUsage !== undefined
                    ? { ...draft, usage: turnUsage }
                    : draft;
                await postChunk(draftWithUsage);
                usageAttached = true;
                seq += 1;
                chunksEmitted += 1;
            }
            continue;
        }
        if (msg.type === 'result') {
            const result = msg;
            resultObserved = true;
            hadError = result.is_error === true;
            // Emit a terminal chunk so downstream consumers can render
            // turn-completion (or error) state without inferring from
            // absence-of-events.
            await postChunk({
                world_id: worldId,
                session_id: sessionId,
                message_id: messageId,
                seq,
                role: 'system',
                kind: 'result',
                chunk: JSON.stringify({
                    subtype: result.subtype ?? null,
                    is_error: hadError,
                }),
                created_at: now().toISOString(),
            });
            seq += 1;
            chunksEmitted += 1;
            break;
        }
        // 'system' messages (auth status, session state, hooks) and
        // 'stream_event' (per-token granularity) are skipped in v1.
        // 'stream_event' may be promoted to per-token chunk writes in a
        // C+1 follow-up if the demo arc needs sub-second token visibility;
        // current scope ships at assistant-message granularity to keep
        // chunk write rate within K3 budget bounds.
    }
    return {
        chunksEmitted,
        endSeq: seq,
        resultObserved,
        hadError,
    };
}
/**
 * Drain a LONG-LIVED SDK event stream (multi-turn interactive mode) and
 * emit chunks for each message, allocating a fresh messageId per turn.
 *
 * Differences from `streamSdkToChunks`:
 *   - Does NOT break on the first `'result'`; continues consuming until the
 *     iterable closes OR the caller aborts the underlying SDK stream.
 *   - Allocates a new messageId via the caller's callback on each
 *     `'result'` boundary; resets seq to 0 per new messageId.
 *   - No `seqStart` parameter — multi-turn streams always start fresh on
 *     each turn.
 *
 * Use this helper when wiring `query({prompt: AsyncIterable<SDKUserMessage>})`
 * — the SDK emits multiple assistant + result turns over the same stream
 * as operator messages feed in via the iterable. The substrate's per-turn
 * messageId semantics survive because each `'result'` triggers a new
 * messageId via `allocateMessageId()`.
 */
export async function streamMultiTurnSdkToChunks(input) {
    const { messages, worldId, sessionId, allocateMessageId, postChunk } = input;
    const now = input.now ?? (() => new Date());
    let messageId = allocateMessageId();
    let seq = 0;
    let turnsObserved = 0;
    let chunksEmitted = 0;
    let lastError = false;
    for await (const msg of messages) {
        if (msg.type === 'assistant') {
            const assistant = msg;
            const content = assistant.message?.content ?? [];
            // B3: same usage-attach logic as streamSdkToChunks — attach to the
            // first non-null chunk per turn; subsequent chunks carry no usage.
            const turnUsage = assistant.message?.usage;
            let usageAttached = false;
            // PERF: stamp `created_at` once per assistant message (see the matching
            // note in `streamSdkToChunks` — blocks of one message land in the same
            // wall-clock millisecond and the persisted column ignores this value
            // anyway). One Date allocation per message instead of one per block.
            const createdAt = now().toISOString();
            for (const block of content) {
                const draft = blockToChunkDraft(block, worldId, sessionId, messageId, seq, createdAt);
                if (draft === null)
                    continue;
                const draftWithUsage = !usageAttached && turnUsage !== undefined
                    ? { ...draft, usage: turnUsage }
                    : draft;
                await postChunk(draftWithUsage);
                usageAttached = true;
                seq += 1;
                chunksEmitted += 1;
            }
            continue;
        }
        if (msg.type === 'result') {
            const result = msg;
            lastError = result.is_error === true;
            await postChunk({
                world_id: worldId,
                session_id: sessionId,
                message_id: messageId,
                seq,
                role: 'system',
                kind: 'result',
                chunk: JSON.stringify({
                    subtype: result.subtype ?? null,
                    is_error: lastError,
                }),
                created_at: now().toISOString(),
            });
            seq += 1;
            chunksEmitted += 1;
            turnsObserved += 1;
            // Allocate fresh messageId for the NEXT turn; reset seq to 0.
            // (Unlike streamSdkToChunks which BREAKs here, this helper keeps
            // consuming the long-lived stream.)
            messageId = allocateMessageId();
            seq = 0;
            continue;
        }
        // 'system' + 'stream_event' skipped (same scope as streamSdkToChunks).
    }
    return { turnsObserved, chunksEmitted, lastError };
}
/**
 * Map a single SDK content block to a chunk draft, or `null` to skip (empty
 * text). Takes the stream-constant scope fields (`worldId`/`sessionId`/
 * `messageId`) plus the per-block `seq`/`createdAt` as flat parameters rather
 * than a context object — this is a per-chunk hot path (every agent
 * token-burst flows through it), and a freshly-allocated `BlockContext`
 * literal per block was pure parameter-passing garbage. Output is identical;
 * only the intermediate allocation is removed.
 */
export function blockToChunkDraft(block, worldId, sessionId, messageId, seq, createdAt) {
    if (block.type === 'text') {
        const text = block.text;
        if (!text)
            return null;
        return {
            world_id: worldId,
            session_id: sessionId,
            message_id: messageId,
            seq,
            role: 'assistant',
            kind: 'text',
            chunk_type: 'text',
            chunk: text,
            created_at: createdAt,
        };
    }
    if (block.type === 'tool_use') {
        const tool = block;
        return {
            world_id: worldId,
            session_id: sessionId,
            message_id: messageId,
            seq,
            role: 'tool',
            kind: 'tool-call',
            chunk_type: 'tool_use',
            chunk: JSON.stringify({
                tool_use_id: tool.id,
                name: tool.name,
                input: tool.input,
            }),
            created_at: createdAt,
        };
    }
    // Unknown block types: preserve via JSON repr so nothing is silently
    // dropped (operator-visible kind='text' fallback keeps the stream
    // intact for forward-compat with future SDK content-block additions).
    return {
        world_id: worldId,
        session_id: sessionId,
        message_id: messageId,
        seq,
        role: 'assistant',
        kind: 'text',
        chunk_type: 'text',
        chunk: JSON.stringify(block),
        created_at: createdAt,
    };
}
/**
 * Production transport for `postChunk`: HTTPs POST to host-cp's
 * `/v1/chunks` endpoint with bearer auth. Returns a function that
 * matches `StreamSdkToChunksInput['postChunk']`.
 *
 * Devbox container calls this with `host.docker.internal:3112` as the
 * sidecar address; bearer is read from `~/.olam/plan-chat-secret`
 * (matches the Phase B B3 SPA bearer-channel pattern, per OQ8 lean (a)).
 */
export function makeHostCpChunkPoster(opts) {
    return async (row) => {
        const res = await fetch(`${opts.sidecarUrl}/v1/chunks`, {
            method: 'POST',
            headers: {
                'content-type': 'application/json',
                authorization: `Bearer ${opts.bearer}`,
            },
            body: JSON.stringify(row),
        });
        if (!res.ok) {
            const body = await res.text().catch(() => '<no body>');
            throw new Error(`host-cp /v1/chunks POST failed: ${res.status} ${res.statusText} — ${body}`);
        }
    };
}
//# sourceMappingURL=agent-sdk-to-chunks.js.map