@caupulican/pi-agent-core 0.81.41 → 0.81.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/agent-loop.d.ts +23 -2
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +189 -42
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/agent.d.ts +15 -1
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +9 -38
  8. package/dist/agent.js.map +1 -1
  9. package/dist/compaction/branch-summarization.d.ts +1 -1
  10. package/dist/compaction/branch-summarization.d.ts.map +1 -1
  11. package/dist/compaction/branch-summarization.js +3 -13
  12. package/dist/compaction/branch-summarization.js.map +1 -1
  13. package/dist/compaction/compaction.d.ts +6 -3
  14. package/dist/compaction/compaction.d.ts.map +1 -1
  15. package/dist/compaction/compaction.js +40 -68
  16. package/dist/compaction/compaction.js.map +1 -1
  17. package/dist/compaction/extraction.d.ts.map +1 -1
  18. package/dist/compaction/extraction.js +1 -6
  19. package/dist/compaction/extraction.js.map +1 -1
  20. package/dist/compaction/index.d.ts.map +1 -1
  21. package/dist/compaction/loop.d.ts.map +1 -1
  22. package/dist/compaction/loop.js.map +1 -1
  23. package/dist/compaction/token-budget.d.ts.map +1 -1
  24. package/dist/compaction/token-budget.js +7 -5
  25. package/dist/compaction/token-budget.js.map +1 -1
  26. package/dist/compaction/utils.d.ts +5 -0
  27. package/dist/compaction/utils.d.ts.map +1 -1
  28. package/dist/compaction/utils.js +20 -0
  29. package/dist/compaction/utils.js.map +1 -1
  30. package/dist/compaction/verification.d.ts.map +1 -1
  31. package/dist/compaction/verification.js +6 -5
  32. package/dist/compaction/verification.js.map +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/messages.d.ts.map +1 -1
  35. package/dist/messages.js.map +1 -1
  36. package/dist/node.d.ts.map +1 -1
  37. package/dist/proxy.d.ts +3 -2
  38. package/dist/proxy.d.ts.map +1 -1
  39. package/dist/proxy.js +88 -56
  40. package/dist/proxy.js.map +1 -1
  41. package/dist/reliability/classifier.d.ts.map +1 -1
  42. package/dist/reliability/classifier.js.map +1 -1
  43. package/dist/reliability/index.d.ts.map +1 -1
  44. package/dist/reliability/node.d.ts.map +1 -1
  45. package/dist/reliability/process-tree.d.ts.map +1 -1
  46. package/dist/reliability/process-tree.js.map +1 -1
  47. package/dist/reliability/provider-signatures.d.ts.map +1 -1
  48. package/dist/reliability/retry-controller.d.ts +1 -1
  49. package/dist/reliability/retry-controller.d.ts.map +1 -1
  50. package/dist/reliability/retry-controller.js +2 -7
  51. package/dist/reliability/retry-controller.js.map +1 -1
  52. package/dist/reliability/retry.d.ts.map +1 -1
  53. package/dist/reliability/retry.js.map +1 -1
  54. package/dist/reliability/watchdogs.d.ts.map +1 -1
  55. package/dist/reliability/watchdogs.js +24 -41
  56. package/dist/reliability/watchdogs.js.map +1 -1
  57. package/dist/session/message-retention.d.ts.map +1 -1
  58. package/dist/session/message-retention.js.map +1 -1
  59. package/dist/session/session-manager.d.ts +1 -1
  60. package/dist/session/session-manager.d.ts.map +1 -1
  61. package/dist/session/session-manager.js +12 -22
  62. package/dist/session/session-manager.js.map +1 -1
  63. package/dist/tool-failure-memory.d.ts +6 -0
  64. package/dist/tool-failure-memory.d.ts.map +1 -1
  65. package/dist/tool-failure-memory.js +78 -20
  66. package/dist/tool-failure-memory.js.map +1 -1
  67. package/dist/types.d.ts +73 -1
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js.map +1 -1
  70. package/dist/usage.d.ts +3 -2
  71. package/dist/usage.d.ts.map +1 -1
  72. package/dist/usage.js +2 -10
  73. package/dist/usage.js.map +1 -1
  74. package/dist/utils/paths.d.ts.map +1 -1
  75. package/dist/utils/paths.js.map +1 -1
  76. package/dist/utils/shell-output.d.ts.map +1 -1
  77. package/dist/utils/shell-output.js.map +1 -1
  78. package/dist/utils/streaming-content.d.ts +23 -0
  79. package/dist/utils/streaming-content.d.ts.map +1 -0
  80. package/dist/utils/streaming-content.js +52 -0
  81. package/dist/utils/streaming-content.js.map +1 -0
  82. package/dist/utils/truncate.d.ts.map +1 -1
  83. package/dist/utils/truncate.js +55 -80
  84. package/dist/utils/truncate.js.map +1 -1
  85. package/dist/uuid.d.ts +1 -1
  86. package/dist/uuid.d.ts.map +1 -1
  87. package/dist/uuid.js +1 -1
  88. package/dist/uuid.js.map +1 -1
  89. package/package.json +6 -6
  90. package/dist/utils/streaming-lines.d.ts +0 -20
  91. package/dist/utils/streaming-lines.d.ts.map +0 -1
  92. package/dist/utils/streaming-lines.js +0 -79
  93. package/dist/utils/streaming-lines.js.map +0 -1
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAoJA;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC","sourcesContent":["import type {\n\tApi,\n\tAssistantMessage,\n\tAssistantMessageEvent,\n\tContext,\n\tImageContent,\n\tMessage,\n\tModel,\n\tSimpleStreamOptions,\n\tstreamSimple,\n\tTextContent,\n\tTool,\n\tToolArgumentValidationTelemetryEvent,\n\tToolResultMessage,\n\tUsage,\n} from \"@caupulican/pi-ai\";\nimport type { Static, TSchema } from \"typebox\";\n\n/**\n * Stream function used by the agent loop.\n *\n * Contract:\n * - Must not throw or return a rejected promise for request/model/runtime failures.\n * - Must return an AssistantMessageEventStream.\n * - Failures must be encoded in the returned stream via protocol events and a\n * final AssistantMessage with stopReason \"error\" or \"aborted\" and errorMessage.\n */\nexport type StreamFn = (\n\t...args: Parameters<typeof streamSimple>\n) => ReturnType<typeof streamSimple> | Promise<ReturnType<typeof streamSimple>>;\n\n/**\n * Configuration for how tool calls from a single assistant message are executed.\n *\n * - \"sequential\": each tool call is prepared, executed, and finalized before the next one starts.\n * - \"parallel\": tool calls are prepared sequentially, then allowed tools execute concurrently.\n * `tool_execution_end` is emitted in tool completion order after each tool is finalized,\n * while tool-result message artifacts are emitted later in assistant source order.\n */\nexport type ToolExecutionMode = \"sequential\" | \"parallel\";\n\n/**\n * Controls how many queued user messages are injected when the agent loop reaches a queue drain point.\n *\n * - \"all\": drain and inject every queued message at that point.\n * - \"one-at-a-time\": drain and inject only the oldest queued message, leaving the rest queued for later drain points.\n */\nexport type QueueMode = \"all\" | \"one-at-a-time\";\n\n/** A single tool call content block emitted by an assistant message. */\nexport type AgentToolCall = Extract<AssistantMessage[\"content\"][number], { type: \"toolCall\" }>;\n\n/**\n * Result returned from `beforeToolCall`.\n *\n * Returning `{ block: true }` prevents the tool from executing. The loop emits an error tool result instead.\n * `reason` becomes the text shown in that error result. If omitted, a default blocked message is used.\n */\nexport interface BeforeToolCallResult {\n\tblock?: boolean;\n\treason?: string;\n}\n\n/**\n * Partial override returned from `afterToolCall`.\n *\n * Merge semantics are field-by-field:\n * - `content`: if provided, replaces the tool result content array in full\n * - `details`: if provided, replaces the tool result details value in full\n * - `usage`: if provided, replaces provider usage reported by the tool\n * - `isError`: if provided, replaces the tool result error flag\n * - `terminate`: if provided, replaces the early-termination hint\n *\n * Omitted fields keep the original executed tool result values.\n * There is no deep merge for `content` or `details`.\n */\nexport interface AfterToolCallResult {\n\tcontent?: (TextContent | ImageContent)[];\n\tdetails?: unknown;\n\tusage?: Usage;\n\tisError?: boolean;\n\t/**\n\t * Hint that the agent should stop after the current tool batch.\n\t * Early termination only happens when every finalized tool result in the batch sets this to true.\n\t */\n\tterminate?: boolean;\n}\n\n/** Context passed to `beforeToolCall`. */\nexport interface BeforeToolCallContext {\n\t/** The assistant message that requested the tool call. */\n\tassistantMessage: AssistantMessage;\n\t/** The raw tool call block from `assistantMessage.content`. */\n\ttoolCall: AgentToolCall;\n\t/** Validated tool arguments for the target tool schema. */\n\targs: unknown;\n\t/** Current agent context at the time the tool call is prepared. */\n\tcontext: AgentContext;\n}\n\n/** Context passed to `afterToolCall`. */\nexport interface AfterToolCallContext {\n\t/** The assistant message that requested the tool call. */\n\tassistantMessage: AssistantMessage;\n\t/** The raw tool call block from `assistantMessage.content`. */\n\ttoolCall: AgentToolCall;\n\t/** Validated tool arguments for the target tool schema. */\n\targs: unknown;\n\t/** The executed tool result before any `afterToolCall` overrides are applied. */\n\tresult: AgentToolResult<any>;\n\t/** Whether the executed tool result is currently treated as an error. */\n\tisError: boolean;\n\t/** Current agent context at the time the tool call is finalized. */\n\tcontext: AgentContext;\n}\n\n/** Context passed to `shouldStopAfterTurn`. */\nexport interface ShouldStopAfterTurnContext {\n\t/** The assistant message that completed the turn. */\n\tmessage: AssistantMessage;\n\t/** Tool result messages passed to the preceding `turn_end` event. */\n\ttoolResults: ToolResultMessage[];\n\t/** Current agent context after the turn's assistant message and tool results have been appended. */\n\tcontext: AgentContext;\n\t/** Messages that this loop invocation will return if it exits at this point. Prompt runs include the initial prompt messages; continuation runs do not include pre-existing context messages. */\n\tnewMessages: AgentMessage[];\n}\n\n/** Replacement runtime state used by the agent loop before starting another provider request. */\nexport interface AgentLoopTurnUpdate {\n\t/** Context for the next provider request. */\n\tcontext?: AgentContext;\n\t/** Model for the next provider request. */\n\tmodel?: Model<any>;\n\t/** Thinking level for the next provider request. */\n\tthinkingLevel?: ThinkingLevel;\n}\n\nexport interface ToolValidationEscalationEvent {\n\ttool: string;\n\tsignature: string;\n\trepeats: number;\n\tmodel: string;\n\tprovider: string;\n}\n\nexport interface PrepareNextTurnContext extends ShouldStopAfterTurnContext {}\n\n/**\n * Default runaway-loop backstop: a single identical tool-call signature recurring this many times\n * within a sliding window (2×) stops the loop. Generous enough that legitimate long/varied work never\n * trips it, but bounds the cost of a model wedged repeating one failing call forever.\n */\nexport const DEFAULT_MAX_STALL_TURNS = 12;\n\nexport interface AgentLoopConfig extends SimpleStreamOptions {\n\tmodel: Model<any>;\n\n\t/**\n\t * Converts AgentMessage[] to LLM-compatible Message[] before each LLM call.\n\t *\n\t * Each AgentMessage must be converted to a UserMessage, AssistantMessage, or ToolResultMessage\n\t * that the LLM can understand. AgentMessages that cannot be converted (e.g., UI-only notifications,\n\t * status messages) should be filtered out.\n\t *\n\t * Contract: must not throw or reject. Return a safe fallback value instead.\n\t * Throwing interrupts the low-level agent loop without producing a normal event sequence.\n\t *\n\t * @example\n\t * ```typescript\n\t * convertToLlm: (messages) => messages.flatMap(m => {\n\t * if (m.role === \"custom\") {\n\t * // Convert custom message to user message\n\t * return [{ role: \"user\", content: m.content, timestamp: m.timestamp }];\n\t * }\n\t * if (m.role === \"notification\") {\n\t * // Filter out UI-only messages\n\t * return [];\n\t * }\n\t * // Pass through standard LLM messages\n\t * return [m];\n\t * })\n\t * ```\n\t */\n\tconvertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;\n\n\t/**\n\t * Optional transform applied to the context before `convertToLlm`.\n\t *\n\t * Use this for operations that work at the AgentMessage level:\n\t * - Context window management (pruning old messages)\n\t * - Injecting context from external sources\n\t *\n\t * Contract: must not throw or reject. Return the original messages or another\n\t * safe fallback value instead.\n\t *\n\t * @example\n\t * ```typescript\n\t * transformContext: async (messages) => {\n\t * if (estimateTokens(messages) > MAX_TOKENS) {\n\t * return pruneOldMessages(messages);\n\t * }\n\t * return messages;\n\t * }\n\t * ```\n\t */\n\ttransformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;\n\n\t/**\n\t * Resolve the reasoning effort after context transformation and immediately before the provider\n\t * request. This supports request-local policy decisions that must not mutate persisted agent state.\n\t */\n\tresolveRequestReasoning?: (\n\t\treasoning: SimpleStreamOptions[\"reasoning\"],\n\t\trequest: { model: Model<Api>; context: Context; maxTokens?: number },\n\t) => SimpleStreamOptions[\"reasoning\"];\n\n\t/**\n\t * Resolves an API key dynamically for each LLM call.\n\t *\n\t * Useful for short-lived OAuth tokens (e.g., GitHub Copilot) that may expire\n\t * during long-running tool execution phases.\n\t *\n\t * Contract: must not throw or reject. Return undefined when no key is available.\n\t */\n\tgetApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;\n\n\t/**\n\t * Called after each turn fully completes and `turn_end` has been emitted.\n\t *\n\t * If it returns true, the loop emits `agent_end` and exits before polling steering or follow-up queues,\n\t * without starting another LLM call. The current assistant response and any tool executions finish normally.\n\t *\n\t * Use this to request a graceful stop after the current turn, e.g. before context gets too full.\n\t *\n\t * Contract: must not throw or reject. Throwing interrupts the low-level agent loop without producing a normal event sequence.\n\t */\n\tshouldStopAfterTurn?: (context: ShouldStopAfterTurnContext) => boolean | Promise<boolean>;\n\n\t/**\n\t * Runaway-loop backstop. A model stuck repeating the SAME tool call (identical name + arguments) —\n\t * because a tool keeps erroring, or it is confused/oscillating — makes no progress yet keeps\n\t * consuming tokens indefinitely (history grows every turn). This bounds that cost: if one tool-call\n\t * signature recurs at least this many times within a sliding window (2×), the loop stops gracefully\n\t * (emits `agent_end`). It counts ONLY turns that issued tool calls and keys on exact name+arguments,\n\t * so legitimate long or varied agentic work never trips it. `0` disables the backstop.\n\t * Default: {@link DEFAULT_MAX_STALL_TURNS}.\n\t */\n\tmaxStallTurns?: number;\n\n\t/**\n\t * Observability hook fired once if the {@link maxStallTurns} runaway backstop trips, just before the\n\t * loop stops. Lets the host surface/log why the run ended. Must not throw.\n\t */\n\tonRunawayStop?: (info: { signature: string; repeats: number }) => void;\n\n\t/**\n\t * Called after `turn_end` and before the loop decides whether another provider request should start.\n\t * Return replacement context/model/thinking state to affect the next turn in this run.\n\t * Return undefined to keep using the current context/config.\n\t */\n\tprepareNextTurn?: (\n\t\tcontext: PrepareNextTurnContext,\n\t) => AgentLoopTurnUpdate | undefined | Promise<AgentLoopTurnUpdate | undefined>;\n\n\t/**\n\t * Returns steering messages to inject into the conversation mid-run.\n\t *\n\t * Called after the current assistant turn finishes executing its tool calls, unless `shouldStopAfterTurn` exits first.\n\t * If messages are returned, they are added to the context before the next LLM call.\n\t * Tool calls from the current assistant message are not skipped.\n\t *\n\t * Use this for \"steering\" the agent while it's working.\n\t *\n\t * Contract: must not throw or reject. Return [] when no steering messages are available.\n\t */\n\tgetSteeringMessages?: () => Promise<AgentMessage[]>;\n\n\t/**\n\t * Returns follow-up messages to process after the agent would otherwise stop.\n\t *\n\t * Called when the agent has no more tool calls and no steering messages.\n\t * If messages are returned, they're added to the context and the agent\n\t * continues with another turn.\n\t *\n\t * Use this for follow-up messages that should wait until the agent finishes.\n\t *\n\t * Contract: must not throw or reject. Return [] when no follow-up messages are available.\n\t */\n\tgetFollowUpMessages?: () => Promise<AgentMessage[]>;\n\n\t/**\n\t * Tool execution mode.\n\t * - \"sequential\": execute tool calls one by one\n\t * - \"parallel\": preflight tool calls sequentially, then execute allowed tools concurrently;\n\t * emit `tool_execution_end` in tool completion order after each tool is finalized,\n\t * then emit tool-result message artifacts later in assistant source order\n\t *\n\t * Default: \"parallel\"\n\t */\n\ttoolExecution?: ToolExecutionMode;\n\n\t/** Disable in-band tool repair teaching notes. Default: enabled. */\n\ttoolArgumentTeachEnabled?: boolean;\n\n\t/**\n\t * Observe tool argument validation outcomes. Events contain only shape metadata\n\t * (outcome, model/provider/tool, failure modes, repairs) and never argument values.\n\t */\n\tonToolArgumentValidation?: (event: ToolArgumentValidationTelemetryEvent) => void;\n\n\t/**\n\t * Number of consecutive identical validation bounces before adding full schema/example feedback\n\t * and notifying the host. Set to 0 to disable. Default: 3.\n\t */\n\ttoolValidationEscalationThreshold?: number;\n\n\t/**\n\t * Fired when a repeated identical tool validation failure reaches the escalation threshold.\n\t * Hosts with model routers can use this signal to move the next turn off a cheap route.\n\t */\n\tonToolValidationEscalation?: (event: ToolValidationEscalationEvent) => void;\n\n\t/**\n\t * Called before a tool is executed, after arguments have been validated.\n\t *\n\t * Return `{ block: true }` to prevent execution. The loop emits an error tool result instead.\n\t * The hook receives the agent abort signal and is responsible for honoring it.\n\t */\n\tbeforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;\n\n\t/**\n\t * Called after a tool finishes executing, before `tool_execution_end` and tool-result message events are emitted.\n\t *\n\t * Return an `AfterToolCallResult` to override parts of the executed tool result:\n\t * - `content` replaces the full content array\n\t * - `details` replaces the full details payload\n\t * - `isError` replaces the error flag\n\t * - `terminate` replaces the early-termination hint\n\t *\n\t * Any omitted fields keep their original values. No deep merge is performed.\n\t * The hook receives the agent abort signal and is responsible for honoring it.\n\t */\n\tafterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;\n}\n\n/**\n * Thinking/reasoning level for models that support it.\n * Note: \"xhigh\", \"max\", and \"ultra\" are only supported by selected model families. \"ultra\" maps\n * to the model's maximum provider effort and reinforces proactive orchestration in capable hosts;\n * delegation can remain available at lower levels. Use model thinking-level metadata from\n * @caupulican/pi-ai to detect support for a concrete model.\n */\nexport type ThinkingLevel = \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" | \"ultra\";\n\n/**\n * Extensible interface for custom app messages.\n * Apps can extend via declaration merging:\n *\n * @example\n * ```typescript\n * declare module \"@mariozechner/agent\" {\n * interface CustomAgentMessages {\n * artifact: ArtifactMessage;\n * notification: NotificationMessage;\n * }\n * }\n * ```\n */\nexport interface CustomAgentMessages {\n\t// Empty by default - apps extend via declaration merging\n}\n\n/**\n * AgentMessage: Union of LLM messages + custom messages.\n * This abstraction allows apps to add custom message types while maintaining\n * type safety and compatibility with the base LLM messages.\n */\nexport type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages];\n\n/**\n * Public agent state.\n *\n * `tools` and `messages` use accessor properties so implementations can copy\n * assigned arrays before storing them.\n */\nexport interface AgentState {\n\t/** System prompt sent with each model request. */\n\tsystemPrompt: string;\n\t/** Active model used for future turns. */\n\tmodel: Model<any>;\n\t/** Requested reasoning level for future turns. */\n\tthinkingLevel: ThinkingLevel;\n\t/** Available tools. Assigning a new array copies the top-level array. */\n\tset tools(tools: AgentTool<any>[]);\n\tget tools(): AgentTool<any>[];\n\t/** Conversation transcript. Assigning a new array copies the top-level array. */\n\tset messages(messages: AgentMessage[]);\n\tget messages(): AgentMessage[];\n\t/**\n\t * True while the agent is processing a prompt or continuation.\n\t *\n\t * This remains true until awaited `agent_end` listeners settle.\n\t */\n\treadonly isStreaming: boolean;\n\t/** Partial assistant message for the current streamed response, if any. */\n\treadonly streamingMessage?: AgentMessage;\n\t/** Tool call ids currently executing. */\n\treadonly pendingToolCalls: ReadonlySet<string>;\n\t/** Error message from the most recent failed or aborted assistant turn, if any. */\n\treadonly errorMessage?: string;\n}\n\n/** Final or partial result produced by a tool. */\nexport interface AgentToolResult<T> {\n\t/** Text or image content returned to the model. */\n\tcontent: (TextContent | ImageContent)[];\n\t/** Arbitrary structured details for logs or UI rendering. */\n\tdetails: T;\n\t/** Provider usage spent inside this tool, for durable budget and cost accounting. */\n\tusage?: Usage;\n\t/**\n\t * Hint that the agent should stop after the current tool batch.\n\t * Early termination only happens when every finalized tool result in the batch sets this to true.\n\t */\n\tterminate?: boolean;\n}\n\n/** Callback used by tools to stream partial execution updates. */\nexport type AgentToolUpdateCallback<T = any> = (partialResult: AgentToolResult<T>) => void;\n\n/** Tool definition used by the agent runtime. */\nexport interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any> extends Tool<TParameters> {\n\t/** Human-readable label for UI display. */\n\tlabel: string;\n\t/**\n\t * Optional compatibility shim for raw tool-call arguments before schema validation.\n\t * Must return an object that matches `TParameters`.\n\t */\n\tprepareArguments?: (args: unknown) => Static<TParameters>;\n\t/** Execute the tool call. Throw on failure instead of encoding errors in `content`. */\n\texecute: (\n\t\ttoolCallId: string,\n\t\tparams: Static<TParameters>,\n\t\tsignal?: AbortSignal,\n\t\tonUpdate?: AgentToolUpdateCallback<TDetails>,\n\t) => Promise<AgentToolResult<TDetails>>;\n\t/**\n\t * Per-tool execution mode override.\n\t * - \"sequential\": this tool must execute one at a time with other tool calls.\n\t * - \"parallel\": this tool can execute concurrently with other tool calls.\n\t *\n\t * If omitted, the default execution mode applies.\n\t */\n\texecutionMode?: ToolExecutionMode;\n}\n\n/** Context snapshot passed into the low-level agent loop. */\nexport interface AgentContext {\n\t/** System prompt included with the request. */\n\tsystemPrompt: string;\n\t/** Transcript visible to the model. */\n\tmessages: AgentMessage[];\n\t/** Tools available for this run. */\n\ttools?: AgentTool<any>[];\n}\n\n/**\n * Events emitted by the Agent for UI updates.\n *\n * `agent_end` is the last event emitted for a run, but awaited `Agent.subscribe()`\n * listeners for that event are still part of run settlement. The agent becomes\n * idle only after those listeners finish.\n */\nexport interface ToolCallRepairInfo {\n\trepaired: true;\n\trawArguments?: Record<string, unknown>;\n\tnotes?: string[];\n}\n\nexport type AgentEvent =\n\t// Agent lifecycle\n\t| { type: \"agent_start\" }\n\t| { type: \"agent_end\"; messages: AgentMessage[] }\n\t// Turn lifecycle - a turn is one assistant response + any tool calls/results\n\t| { type: \"turn_start\" }\n\t| { type: \"turn_end\"; message: AgentMessage; toolResults: ToolResultMessage[] }\n\t// Message lifecycle - emitted for user, assistant, and toolResult messages\n\t| { type: \"message_start\"; message: AgentMessage }\n\t// Only emitted for assistant messages during streaming\n\t| { type: \"message_update\"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }\n\t| { type: \"message_end\"; message: AgentMessage }\n\t// Tool execution lifecycle\n\t| { type: \"tool_execution_start\"; toolCallId: string; toolName: string; args: any; repair?: ToolCallRepairInfo }\n\t| {\n\t\t\ttype: \"tool_execution_update\";\n\t\t\ttoolCallId: string;\n\t\t\ttoolName: string;\n\t\t\targs: any;\n\t\t\tpartialResult: any;\n\t\t\trepair?: ToolCallRepairInfo;\n\t }\n\t| {\n\t\t\ttype: \"tool_execution_end\";\n\t\t\ttoolCallId: string;\n\t\t\ttoolName: string;\n\t\t\tresult: any;\n\t\t\tisError: boolean;\n\t\t\trepair?: ToolCallRepairInfo;\n\t };\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA6LA;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC","sourcesContent":["import type {\n\tApi,\n\tAssistantMessage,\n\tAssistantMessageEvent,\n\tContext,\n\tImageContent,\n\tMessage,\n\tModel,\n\tSimpleStreamOptions,\n\tstreamSimple,\n\tTextContent,\n\tTool,\n\tToolArgumentValidationTelemetryEvent,\n\tToolResultMessage,\n\tUsage,\n} from \"@caupulican/pi-ai\";\nimport type { Static, TSchema } from \"typebox\";\n\n/**\n * Stream function used by the agent loop.\n *\n * Contract:\n * - Must not throw or return a rejected promise for request/model/runtime failures.\n * - Must return an AssistantMessageEventStream.\n * - Failures must be encoded in the returned stream via protocol events and a\n * final AssistantMessage with stopReason \"error\" or \"aborted\" and errorMessage.\n */\nexport type StreamFn = (\n\t...args: Parameters<typeof streamSimple>\n) => ReturnType<typeof streamSimple> | Promise<ReturnType<typeof streamSimple>>;\n\n/**\n * Configuration for how tool calls from a single assistant message are executed.\n *\n * - \"sequential\": each tool call is prepared, executed, and finalized before the next one starts.\n * - \"parallel\": tool calls are prepared sequentially, then allowed tools execute concurrently.\n * `tool_execution_end` is emitted in tool completion order after each tool is finalized,\n * while tool-result message artifacts are emitted later in assistant source order.\n */\nexport type ToolExecutionMode = \"sequential\" | \"parallel\";\n\n/**\n * Controls how many queued user messages are injected when the agent loop reaches a queue drain point.\n *\n * - \"all\": drain and inject every queued message at that point.\n * - \"one-at-a-time\": drain and inject only the oldest queued message, leaving the rest queued for later drain points.\n */\nexport type QueueMode = \"all\" | \"one-at-a-time\";\n\n/** A single tool call content block emitted by an assistant message. */\nexport type AgentToolCall = Extract<AssistantMessage[\"content\"][number], { type: \"toolCall\" }>;\n\n/**\n * Result returned from `beforeToolCall`.\n *\n * Returning `{ block: true }` prevents the tool from executing. The loop emits an error tool result instead.\n * `reason` becomes the text shown in that error result. If omitted, a default blocked message is used.\n */\nexport interface BeforeToolCallResult {\n\tblock?: boolean;\n\treason?: string;\n}\n\n/**\n * Partial override returned from `afterToolCall`.\n *\n * Merge semantics are field-by-field:\n * - `content`: if provided, replaces the tool result content array in full\n * - `details`: if provided, replaces the tool result details value in full\n * - `usage`: if provided, replaces provider usage reported by the tool\n * - `isError`: if provided, replaces the tool result error flag\n * - `terminate`: if provided, replaces the early-termination hint\n *\n * Omitted fields keep the original executed tool result values.\n * There is no deep merge for `content` or `details`.\n */\nexport interface AfterToolCallResult {\n\tcontent?: (TextContent | ImageContent)[];\n\tdetails?: unknown;\n\tusage?: Usage;\n\tisError?: boolean;\n\t/**\n\t * Hint that the agent should stop after the current tool batch.\n\t * Early termination only happens when every finalized tool result in the batch sets this to true.\n\t */\n\tterminate?: boolean;\n}\n\n/** Context passed to `beforeToolCall`. */\nexport interface BeforeToolCallContext {\n\t/** The assistant message that requested the tool call. */\n\tassistantMessage: AssistantMessage;\n\t/** The raw tool call block from `assistantMessage.content`. */\n\ttoolCall: AgentToolCall;\n\t/** Validated tool arguments for the target tool schema. */\n\targs: unknown;\n\t/** Current agent context at the time the tool call is prepared. */\n\tcontext: AgentContext;\n}\n\n/** Context passed to `afterToolCall`. */\nexport interface AfterToolCallContext {\n\t/** The assistant message that requested the tool call. */\n\tassistantMessage: AssistantMessage;\n\t/** The raw tool call block from `assistantMessage.content`. */\n\ttoolCall: AgentToolCall;\n\t/** Validated tool arguments for the target tool schema. */\n\targs: unknown;\n\t/** The executed tool result before any `afterToolCall` overrides are applied. */\n\tresult: AgentToolResult<any>;\n\t/** Whether the executed tool result is currently treated as an error. */\n\tisError: boolean;\n\t/** Current agent context at the time the tool call is finalized. */\n\tcontext: AgentContext;\n}\n\n/** Policy-finalized result of a tool call that outlived its foreground turn. */\nexport interface BackgroundToolCallCompletion {\n\t/** Original tool call identity. */\n\ttoolCall: AgentToolCall;\n\t/** Result after the normal `afterToolCall` policy boundary has run. */\n\tresult: AgentToolResult<any>;\n\t/** Final error classification after policy overrides. */\n\tisError: boolean;\n}\n\n/** Context offered to a host when a prepared tool call crosses its foreground latency budget. */\nexport interface BackgroundToolCallContext extends BeforeToolCallContext {\n\t/** Configured foreground latency budget that elapsed. */\n\telapsedMs: number;\n\t/** Event-driven terminal signal for the real, policy-finalized execution. */\n\tcompletion: Promise<BackgroundToolCallCompletion>;\n\t/** Abort only this detached execution. */\n\tcancel(): void;\n}\n\n/** Immediate foreground result returned when the host accepts ownership of a slow tool call. */\nexport interface BackgroundToolCallHandoff {\n\t/** Bounded result telling the model how to address the session-owned task. */\n\tresult: AgentToolResult<any>;\n\t/** Optional foreground error classification. Defaults to `result.isError === true`. */\n\tisError?: boolean;\n}\n\n/** Context passed to `shouldStopAfterTurn`. */\nexport interface ShouldStopAfterTurnContext {\n\t/** The assistant message that completed the turn. */\n\tmessage: AssistantMessage;\n\t/** Tool result messages passed to the preceding `turn_end` event. */\n\ttoolResults: ToolResultMessage[];\n\t/** Current agent context after the turn's assistant message and tool results have been appended. */\n\tcontext: AgentContext;\n\t/** Messages that this loop invocation will return if it exits at this point. Prompt runs include the initial prompt messages; continuation runs do not include pre-existing context messages. */\n\tnewMessages: AgentMessage[];\n}\n\n/** Replacement runtime state used by the agent loop before starting another provider request. */\nexport interface AgentLoopTurnUpdate {\n\t/** Context for the next provider request. */\n\tcontext?: AgentContext;\n\t/** Model for the next provider request. */\n\tmodel?: Model<any>;\n\t/** Thinking level for the next provider request. */\n\tthinkingLevel?: ThinkingLevel;\n}\n\nexport interface ToolValidationEscalationEvent {\n\ttool: string;\n\tsignature: string;\n\trepeats: number;\n\tmodel: string;\n\tprovider: string;\n}\n\nexport interface PrepareNextTurnContext extends ShouldStopAfterTurnContext {}\n\n/** Provider-ready request inspected after context transformation and immediately before transport. */\nexport interface RequestPreflightContext {\n\tmodel: Model<Api>;\n\tcontext: Context;\n\t/** Current owner-selected output cap before request-local narrowing. */\n\tmaxTokens?: number;\n}\n\n/** Request-local limits. A returned output cap can only narrow the current owner/model limit. */\nexport interface RequestPreflightResult {\n\tmaxTokens?: number;\n}\n\n/**\n * Default runaway-loop backstop: a single identical tool-call signature recurring this many times\n * within a sliding window (2×) stops the loop. Generous enough that legitimate long/varied work never\n * trips it, but bounds the cost of a model wedged repeating one failing call forever.\n */\nexport const DEFAULT_MAX_STALL_TURNS = 12;\n\nexport interface AgentLoopConfig extends SimpleStreamOptions {\n\tmodel: Model<any>;\n\n\t/**\n\t * Converts AgentMessage[] to LLM-compatible Message[] before each LLM call.\n\t *\n\t * Each AgentMessage must be converted to a UserMessage, AssistantMessage, or ToolResultMessage\n\t * that the LLM can understand. AgentMessages that cannot be converted (e.g., UI-only notifications,\n\t * status messages) should be filtered out.\n\t *\n\t * Contract: must not throw or reject. Return a safe fallback value instead.\n\t * Throwing interrupts the low-level agent loop without producing a normal event sequence.\n\t *\n\t * @example\n\t * ```typescript\n\t * convertToLlm: (messages) => messages.flatMap(m => {\n\t * if (m.role === \"custom\") {\n\t * // Convert custom message to user message\n\t * return [{ role: \"user\", content: m.content, timestamp: m.timestamp }];\n\t * }\n\t * if (m.role === \"notification\") {\n\t * // Filter out UI-only messages\n\t * return [];\n\t * }\n\t * // Pass through standard LLM messages\n\t * return [m];\n\t * })\n\t * ```\n\t */\n\tconvertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;\n\n\t/**\n\t * Optional transform applied to the context before `convertToLlm`.\n\t *\n\t * Use this for operations that work at the AgentMessage level:\n\t * - Context window management (pruning old messages)\n\t * - Injecting context from external sources\n\t *\n\t * Contract: must not throw or reject. Return the original messages or another\n\t * safe fallback value instead.\n\t *\n\t * @example\n\t * ```typescript\n\t * transformContext: async (messages) => {\n\t * if (estimateTokens(messages) > MAX_TOKENS) {\n\t * return pruneOldMessages(messages);\n\t * }\n\t * return messages;\n\t * }\n\t * ```\n\t */\n\ttransformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;\n\n\t/**\n\t * Runs after context transformation and conversion, immediately before every provider request.\n\t *\n\t * Use this for request-local budget/authority checks whose state can change between tool turns.\n\t * Throwing prevents transport. A returned `maxTokens` must be a positive safe integer and can\n\t * only narrow the current owner/model output limit; it never mutates the persistent loop config.\n\t */\n\trequestPreflight?: (\n\t\tcontext: RequestPreflightContext,\n\t\tsignal?: AbortSignal,\n\t) => RequestPreflightResult | undefined | Promise<RequestPreflightResult | undefined>;\n\n\t/**\n\t * Resolve the reasoning effort after context transformation and immediately before the provider\n\t * request. This supports request-local policy decisions that must not mutate persisted agent state.\n\t */\n\tresolveRequestReasoning?: (\n\t\treasoning: SimpleStreamOptions[\"reasoning\"],\n\t\trequest: { model: Model<Api>; context: Context; maxTokens?: number },\n\t) => SimpleStreamOptions[\"reasoning\"];\n\n\t/**\n\t * Resolves an API key dynamically for each LLM call.\n\t *\n\t * Useful for short-lived OAuth tokens (e.g., GitHub Copilot) that may expire\n\t * during long-running tool execution phases.\n\t *\n\t * Contract: must not throw or reject. Return undefined when no key is available.\n\t */\n\tgetApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;\n\n\t/**\n\t * Called after each turn fully completes and `turn_end` has been emitted.\n\t *\n\t * If it returns true, the loop emits `agent_end` and exits before polling steering or follow-up queues,\n\t * without starting another LLM call. The current assistant response and any tool executions finish normally.\n\t *\n\t * Use this to request a graceful stop after the current turn, e.g. before context gets too full.\n\t *\n\t * Contract: must not throw or reject. Throwing interrupts the low-level agent loop without producing a normal event sequence.\n\t */\n\tshouldStopAfterTurn?: (context: ShouldStopAfterTurnContext) => boolean | Promise<boolean>;\n\n\t/**\n\t * Runaway-loop backstop. A model stuck repeating the SAME tool call (identical name + arguments) —\n\t * because a tool keeps erroring, or it is confused/oscillating — makes no progress yet keeps\n\t * consuming tokens indefinitely (history grows every turn). This bounds that cost: if one tool-call\n\t * signature recurs at least this many times within a sliding window (2×), the loop stops gracefully\n\t * (emits `agent_end`). It counts ONLY turns that issued tool calls and keys on exact name+arguments,\n\t * so legitimate long or varied agentic work never trips it. `0` disables the backstop.\n\t * Default: {@link DEFAULT_MAX_STALL_TURNS}.\n\t */\n\tmaxStallTurns?: number;\n\n\t/**\n\t * Observability hook fired once if the {@link maxStallTurns} runaway backstop trips, just before the\n\t * loop stops. Lets the host surface/log why the run ended. Must not throw.\n\t */\n\tonRunawayStop?: (info: { signature: string; repeats: number }) => void;\n\n\t/**\n\t * Called after `turn_end` and before the loop decides whether another provider request should start.\n\t * Return replacement context/model/thinking state to affect the next turn in this run.\n\t * Return undefined to keep using the current context/config.\n\t */\n\tprepareNextTurn?: (\n\t\tcontext: PrepareNextTurnContext,\n\t) => AgentLoopTurnUpdate | undefined | Promise<AgentLoopTurnUpdate | undefined>;\n\n\t/**\n\t * Returns steering messages to inject into the conversation mid-run.\n\t *\n\t * Called after the current assistant turn finishes executing its tool calls, unless `shouldStopAfterTurn` exits first.\n\t * If messages are returned, they are added to the context before the next LLM call.\n\t * Tool calls from the current assistant message are not skipped.\n\t *\n\t * Use this for \"steering\" the agent while it's working.\n\t *\n\t * Contract: must not throw or reject. Return [] when no steering messages are available.\n\t */\n\tgetSteeringMessages?: () => Promise<AgentMessage[]>;\n\n\t/**\n\t * Returns follow-up messages to process after the agent would otherwise stop.\n\t *\n\t * Called when the agent has no more tool calls and no steering messages.\n\t * If messages are returned, they're added to the context and the agent\n\t * continues with another turn.\n\t *\n\t * Use this for follow-up messages that should wait until the agent finishes.\n\t *\n\t * Contract: must not throw or reject. Return [] when no follow-up messages are available.\n\t */\n\tgetFollowUpMessages?: () => Promise<AgentMessage[]>;\n\n\t/**\n\t * Tool execution mode.\n\t * - \"sequential\": execute tool calls one by one\n\t * - \"parallel\": preflight tool calls sequentially, then execute allowed tools concurrently;\n\t * emit `tool_execution_end` in tool completion order after each tool is finalized,\n\t * then emit tool-result message artifacts later in assistant source order\n\t *\n\t * Default: \"parallel\"\n\t */\n\ttoolExecution?: ToolExecutionMode;\n\n\t/** Disable in-band tool repair teaching notes. Default: enabled. */\n\ttoolArgumentTeachEnabled?: boolean;\n\n\t/**\n\t * Observe tool argument validation outcomes. Events contain only shape metadata\n\t * (outcome, model/provider/tool, failure modes, repairs) and never argument values.\n\t */\n\tonToolArgumentValidation?: (event: ToolArgumentValidationTelemetryEvent) => void;\n\n\t/**\n\t * Number of consecutive identical validation bounces before adding full schema/example feedback\n\t * and notifying the host. Set to 0 to disable. Default: 3.\n\t */\n\ttoolValidationEscalationThreshold?: number;\n\n\t/**\n\t * Fired when a repeated identical tool validation failure reaches the escalation threshold.\n\t * Hosts with model routers can use this signal to move the next turn off a cheap route.\n\t */\n\tonToolValidationEscalation?: (event: ToolValidationEscalationEvent) => void;\n\n\t/**\n\t * Called before a tool is executed, after arguments have been validated.\n\t *\n\t * Return `{ block: true }` to prevent execution. The loop emits an error tool result instead.\n\t * The hook receives the agent abort signal and is responsible for honoring it.\n\t */\n\tbeforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;\n\n\t/**\n\t * Called after a tool finishes executing, before `tool_execution_end` and tool-result message events are emitted.\n\t *\n\t * Return an `AfterToolCallResult` to override parts of the executed tool result:\n\t * - `content` replaces the full content array\n\t * - `details` replaces the full details payload\n\t * - `isError` replaces the error flag\n\t * - `terminate` replaces the early-termination hint\n\t *\n\t * Any omitted fields keep their original values. No deep merge is performed.\n\t * The hook receives the agent abort signal and is responsible for honoring it.\n\t */\n\tafterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;\n\n\t/**\n\t * Foreground latency budget for prepared tool calls. When it elapses, `handoffToolCall` may\n\t * transfer the still-running execution to a host-owned task. Disabled unless both fields exist.\n\t */\n\tbackgroundToolCallAfterMs?: number;\n\n\t/**\n\t * Synchronously accept ownership of a slow call. Returning a handoff lets the provider loop\n\t * continue with its bounded placeholder while `completion` still crosses `afterToolCall` once.\n\t * Returning `undefined` keeps waiting in the foreground.\n\t */\n\thandoffToolCall?: (context: BackgroundToolCallContext) => BackgroundToolCallHandoff | undefined;\n\n\t/**\n\t * Register a one-shot host request that asks an in-flight foreground call to cross the same\n\t * `handoffToolCall` boundary before its automatic latency budget elapses.\n\t */\n\tsubscribeToolCallHandoffRequest?: (toolCallId: string, request: () => void) => () => void;\n}\n\n/**\n * Thinking/reasoning level for models that support it.\n * Note: \"xhigh\", \"max\", and \"ultra\" are only supported by selected model families. \"ultra\" maps\n * to the model's maximum provider effort and reinforces proactive orchestration in capable hosts;\n * delegation can remain available at lower levels. Use model thinking-level metadata from\n * @caupulican/pi-ai to detect support for a concrete model.\n */\nexport type ThinkingLevel = \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" | \"ultra\";\n\n/**\n * Extensible interface for custom app messages.\n * Apps can extend via declaration merging:\n *\n * @example\n * ```typescript\n * declare module \"@mariozechner/agent\" {\n * interface CustomAgentMessages {\n * artifact: ArtifactMessage;\n * notification: NotificationMessage;\n * }\n * }\n * ```\n */\nexport interface CustomAgentMessages {\n\t// Empty by default - apps extend via declaration merging\n}\n\n/**\n * AgentMessage: Union of LLM messages + custom messages.\n * This abstraction allows apps to add custom message types while maintaining\n * type safety and compatibility with the base LLM messages.\n */\nexport type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages];\n\n/**\n * Public agent state.\n *\n * `tools` and `messages` use accessor properties so implementations can copy\n * assigned arrays before storing them.\n */\nexport interface AgentState {\n\t/** System prompt sent with each model request. */\n\tsystemPrompt: string;\n\t/** Active model used for future turns. */\n\tmodel: Model<any>;\n\t/** Requested reasoning level for future turns. */\n\tthinkingLevel: ThinkingLevel;\n\t/** Available tools. Assigning a new array copies the top-level array. */\n\tset tools(tools: AgentTool<any>[]);\n\tget tools(): AgentTool<any>[];\n\t/** Conversation transcript. Assigning a new array copies the top-level array. */\n\tset messages(messages: AgentMessage[]);\n\tget messages(): AgentMessage[];\n\t/**\n\t * True while the agent is processing a prompt or continuation.\n\t *\n\t * This remains true until awaited `agent_end` listeners settle.\n\t */\n\treadonly isStreaming: boolean;\n\t/** Partial assistant message for the current streamed response, if any. */\n\treadonly streamingMessage?: AgentMessage;\n\t/** Tool call ids currently executing. */\n\treadonly pendingToolCalls: ReadonlySet<string>;\n\t/** Error message from the most recent failed or aborted assistant turn, if any. */\n\treadonly errorMessage?: string;\n}\n\n/** Final or partial result produced by a tool. */\nexport interface AgentToolResult<T> {\n\t/** Text or image content returned to the model. */\n\tcontent: (TextContent | ImageContent)[];\n\t/** Arbitrary structured details for logs or UI rendering. */\n\tdetails: T;\n\t/**\n\t * Marks a completed execution as a failure without throwing.\n\t *\n\t * The agent loop preserves the result long enough for `afterToolCall` to\n\t * inspect it, then converts the bounded diagnostic into its durable failure\n\t * record. Throwing remains valid for exceptional execution failures.\n\t */\n\tisError?: boolean;\n\t/** Provider usage spent inside this tool, for durable budget and cost accounting. */\n\tusage?: Usage;\n\t/**\n\t * Hint that the agent should stop after the current tool batch.\n\t * Early termination only happens when every finalized tool result in the batch sets this to true.\n\t */\n\tterminate?: boolean;\n}\n\n/** Callback used by tools to stream partial execution updates. */\nexport type AgentToolUpdateCallback<T = any> = (partialResult: AgentToolResult<T>) => void;\n\n/** Tool definition used by the agent runtime. */\nexport interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any> extends Tool<TParameters> {\n\t/** Human-readable label for UI display. */\n\tlabel: string;\n\t/**\n\t * Optional compatibility shim for raw tool-call arguments before schema validation.\n\t * Must return an object that matches `TParameters`.\n\t */\n\tprepareArguments?: (args: unknown) => Static<TParameters>;\n\t/**\n\t * Execute the tool call. Throw for exceptional execution failures, or return\n\t * `{ isError: true }` with bounded diagnostic content for an expected\n\t * operation failure such as a non-zero subprocess exit.\n\t */\n\texecute: (\n\t\ttoolCallId: string,\n\t\tparams: Static<TParameters>,\n\t\tsignal?: AbortSignal,\n\t\tonUpdate?: AgentToolUpdateCallback<TDetails>,\n\t) => Promise<AgentToolResult<TDetails>>;\n\t/**\n\t * Per-tool execution mode override.\n\t * - \"sequential\": this tool must execute one at a time with other tool calls.\n\t * - \"parallel\": this tool can execute concurrently with other tool calls.\n\t *\n\t * If omitted, the default execution mode applies.\n\t */\n\texecutionMode?: ToolExecutionMode;\n}\n\n/** Context snapshot passed into the low-level agent loop. */\nexport interface AgentContext {\n\t/** System prompt included with the request. */\n\tsystemPrompt: string;\n\t/** Transcript visible to the model. */\n\tmessages: AgentMessage[];\n\t/** Tools available for this run. */\n\ttools?: AgentTool<any>[];\n}\n\n/**\n * Events emitted by the Agent for UI updates.\n *\n * `agent_end` is the last event emitted for a run, but awaited `Agent.subscribe()`\n * listeners for that event are still part of run settlement. The agent becomes\n * idle only after those listeners finish.\n */\nexport interface ToolCallRepairInfo {\n\trepaired: true;\n\trawArguments?: Record<string, unknown>;\n\tnotes?: string[];\n}\n\nexport type AgentEvent =\n\t// Agent lifecycle\n\t| { type: \"agent_start\" }\n\t| { type: \"agent_end\"; messages: AgentMessage[] }\n\t// Turn lifecycle - a turn is one assistant response + any tool calls/results\n\t| { type: \"turn_start\" }\n\t| { type: \"turn_end\"; message: AgentMessage; toolResults: ToolResultMessage[] }\n\t// Message lifecycle - emitted for user, assistant, and toolResult messages\n\t| { type: \"message_start\"; message: AgentMessage }\n\t// Only emitted for assistant messages during streaming\n\t| { type: \"message_update\"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }\n\t| { type: \"message_end\"; message: AgentMessage }\n\t// Tool execution lifecycle\n\t| { type: \"tool_execution_start\"; toolCallId: string; toolName: string; args: any; repair?: ToolCallRepairInfo }\n\t| {\n\t\t\ttype: \"tool_execution_update\";\n\t\t\ttoolCallId: string;\n\t\t\ttoolName: string;\n\t\t\targs: any;\n\t\t\tpartialResult: any;\n\t\t\trepair?: ToolCallRepairInfo;\n\t }\n\t| {\n\t\t\ttype: \"tool_execution_end\";\n\t\t\ttoolCallId: string;\n\t\t\ttoolName: string;\n\t\t\tresult: any;\n\t\t\tisError: boolean;\n\t\t\trepair?: ToolCallRepairInfo;\n\t };\n"]}
package/dist/usage.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { Usage } from "@caupulican/pi-ai";
1
+ import type { Usage } from "@caupulican/pi-ai/types";
2
+ import { createEmptyUsage } from "@caupulican/pi-ai/usage";
2
3
  import type { SessionEntry } from "./session/session-manager.ts";
3
- export declare function createEmptyUsage(): Usage;
4
+ export { createEmptyUsage };
4
5
  export declare function addUsage(target: Usage, usage: Usage): void;
5
6
  export declare function combineUsage(...usages: Array<Usage | undefined>): Usage;
6
7
  /** Return usage billed directly by a durable session entry, excluding nested-session rollups. */
@@ -1 +1 @@
1
- {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEjE,wBAAgB,gBAAgB,IAAI,KAAK,CASxC;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAW1D;AAED,wBAAgB,YAAY,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,KAAK,CAMvE;AAED,iGAAiG;AACjG,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,KAAK,GAAG,SAAS,CAS3E","sourcesContent":["import type { Usage } from \"@caupulican/pi-ai\";\nimport type { SessionEntry } from \"./session/session-manager.ts\";\n\nexport function createEmptyUsage(): Usage {\n\treturn {\n\t\tinput: 0,\n\t\toutput: 0,\n\t\tcacheRead: 0,\n\t\tcacheWrite: 0,\n\t\ttotalTokens: 0,\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t};\n}\n\nexport function addUsage(target: Usage, usage: Usage): void {\n\ttarget.input += usage.input;\n\ttarget.output += usage.output;\n\ttarget.cacheRead += usage.cacheRead;\n\ttarget.cacheWrite += usage.cacheWrite;\n\ttarget.totalTokens += usage.totalTokens;\n\ttarget.cost.input += usage.cost.input;\n\ttarget.cost.output += usage.cost.output;\n\ttarget.cost.cacheRead += usage.cost.cacheRead;\n\ttarget.cost.cacheWrite += usage.cost.cacheWrite;\n\ttarget.cost.total += usage.cost.total;\n}\n\nexport function combineUsage(...usages: Array<Usage | undefined>): Usage {\n\tconst total = createEmptyUsage();\n\tfor (const usage of usages) {\n\t\tif (usage) addUsage(total, usage);\n\t}\n\treturn total;\n}\n\n/** Return usage billed directly by a durable session entry, excluding nested-session rollups. */\nexport function getSessionEntryUsage(entry: SessionEntry): Usage | undefined {\n\tif (entry.type === \"message\") {\n\t\tif (entry.message.role === \"assistant\" || entry.message.role === \"toolResult\") {\n\t\t\treturn entry.message.usage;\n\t\t}\n\t\treturn undefined;\n\t}\n\tif (entry.type === \"compaction\" || entry.type === \"branch_summary\") return entry.usage;\n\treturn undefined;\n}\n"]}
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEjE,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAE5B,wBAAgB,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAW1D;AAED,wBAAgB,YAAY,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,KAAK,CAMvE;AAED,iGAAiG;AACjG,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,KAAK,GAAG,SAAS,CAS3E"}
package/dist/usage.js CHANGED
@@ -1,13 +1,5 @@
1
- export function createEmptyUsage() {
2
- return {
3
- input: 0,
4
- output: 0,
5
- cacheRead: 0,
6
- cacheWrite: 0,
7
- totalTokens: 0,
8
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
9
- };
10
- }
1
+ import { createEmptyUsage } from "@caupulican/pi-ai/usage";
2
+ export { createEmptyUsage };
11
3
  export function addUsage(target, usage) {
12
4
  target.input += usage.input;
13
5
  target.output += usage.output;
package/dist/usage.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,gBAAgB,GAAU;IACzC,OAAO;QACN,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,CAAC;QACT,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,CAAC;QACd,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;KACpE,CAAC;AAAA,CACF;AAED,MAAM,UAAU,QAAQ,CAAC,MAAa,EAAE,KAAY,EAAQ;IAC3D,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC;IAC5B,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC;IAC9B,MAAM,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC;IACpC,MAAM,CAAC,UAAU,IAAI,KAAK,CAAC,UAAU,CAAC;IACtC,MAAM,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,CAAC;IACxC,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;IACtC,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;IACxC,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC;IAC9C,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;IAChD,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;AAAA,CACtC;AAED,MAAM,UAAU,YAAY,CAAC,GAAG,MAAgC,EAAS;IACxE,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC5B,IAAI,KAAK;YAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED,iGAAiG;AACjG,MAAM,UAAU,oBAAoB,CAAC,KAAmB,EAAqB;IAC5E,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC/E,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAC5B,CAAC;QACD,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC;IACvF,OAAO,SAAS,CAAC;AAAA,CACjB","sourcesContent":["import type { Usage } from \"@caupulican/pi-ai\";\nimport type { SessionEntry } from \"./session/session-manager.ts\";\n\nexport function createEmptyUsage(): Usage {\n\treturn {\n\t\tinput: 0,\n\t\toutput: 0,\n\t\tcacheRead: 0,\n\t\tcacheWrite: 0,\n\t\ttotalTokens: 0,\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t};\n}\n\nexport function addUsage(target: Usage, usage: Usage): void {\n\ttarget.input += usage.input;\n\ttarget.output += usage.output;\n\ttarget.cacheRead += usage.cacheRead;\n\ttarget.cacheWrite += usage.cacheWrite;\n\ttarget.totalTokens += usage.totalTokens;\n\ttarget.cost.input += usage.cost.input;\n\ttarget.cost.output += usage.cost.output;\n\ttarget.cost.cacheRead += usage.cost.cacheRead;\n\ttarget.cost.cacheWrite += usage.cost.cacheWrite;\n\ttarget.cost.total += usage.cost.total;\n}\n\nexport function combineUsage(...usages: Array<Usage | undefined>): Usage {\n\tconst total = createEmptyUsage();\n\tfor (const usage of usages) {\n\t\tif (usage) addUsage(total, usage);\n\t}\n\treturn total;\n}\n\n/** Return usage billed directly by a durable session entry, excluding nested-session rollups. */\nexport function getSessionEntryUsage(entry: SessionEntry): Usage | undefined {\n\tif (entry.type === \"message\") {\n\t\tif (entry.message.role === \"assistant\" || entry.message.role === \"toolResult\") {\n\t\t\treturn entry.message.usage;\n\t\t}\n\t\treturn undefined;\n\t}\n\tif (entry.type === \"compaction\" || entry.type === \"branch_summary\") return entry.usage;\n\treturn undefined;\n}\n"]}
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAE5B,MAAM,UAAU,QAAQ,CAAC,MAAa,EAAE,KAAY;IACnD,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC;IAC5B,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC;IAC9B,MAAM,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC;IACpC,MAAM,CAAC,UAAU,IAAI,KAAK,CAAC,UAAU,CAAC;IACtC,MAAM,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,CAAC;IACxC,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;IACtC,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;IACxC,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC;IAC9C,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;IAChD,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAG,MAAgC;IAC/D,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC5B,IAAI,KAAK;YAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,oBAAoB,CAAC,KAAmB;IACvD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC/E,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAC5B,CAAC;QACD,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC;IACvF,OAAO,SAAS,CAAC;AAClB,CAAC","sourcesContent":["import type { Usage } from \"@caupulican/pi-ai/types\";\nimport { createEmptyUsage } from \"@caupulican/pi-ai/usage\";\nimport type { SessionEntry } from \"./session/session-manager.ts\";\n\nexport { createEmptyUsage };\n\nexport function addUsage(target: Usage, usage: Usage): void {\n\ttarget.input += usage.input;\n\ttarget.output += usage.output;\n\ttarget.cacheRead += usage.cacheRead;\n\ttarget.cacheWrite += usage.cacheWrite;\n\ttarget.totalTokens += usage.totalTokens;\n\ttarget.cost.input += usage.cost.input;\n\ttarget.cost.output += usage.cost.output;\n\ttarget.cost.cacheRead += usage.cost.cacheRead;\n\ttarget.cost.cacheWrite += usage.cost.cacheWrite;\n\ttarget.cost.total += usage.cost.total;\n}\n\nexport function combineUsage(...usages: Array<Usage | undefined>): Usage {\n\tconst total = createEmptyUsage();\n\tfor (const usage of usages) {\n\t\tif (usage) addUsage(total, usage);\n\t}\n\treturn total;\n}\n\n/** Return usage billed directly by a durable session entry, excluding nested-session rollups. */\nexport function getSessionEntryUsage(entry: SessionEntry): Usage | undefined {\n\tif (entry.type === \"message\") {\n\t\tif (entry.message.role === \"assistant\" || entry.message.role === \"toolResult\") {\n\t\t\treturn entry.message.usage;\n\t\t}\n\t\treturn undefined;\n\t}\n\tif (entry.type === \"compaction\" || entry.type === \"branch_summary\") return entry.usage;\n\treturn undefined;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAChC,6DAA6D;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0DAA0D;IAC1D,sBAAsB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAsBnF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,MAAsB,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAIlH","sourcesContent":["import { homedir } from \"node:os\";\nimport { isAbsolute, join, resolve as nodeResolvePath } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nexport interface PathInputOptions {\n\t/** Trim leading/trailing whitespace before normalization. */\n\ttrim?: boolean;\n\t/** Expand leading `~` to a home directory. Defaults to true. */\n\texpandTilde?: boolean;\n\t/** Home directory used for `~` expansion. Defaults to `os.homedir()`. */\n\thomeDir?: string;\n\t/** Strip a leading `@`, used for CLI @file paths. */\n\tstripAtPrefix?: boolean;\n\t/** Normalize unicode space variants to regular spaces. */\n\tnormalizeUnicodeSpaces?: boolean;\n}\n\nexport function normalizePath(input: string, options: PathInputOptions = {}): string {\n\tlet normalized = options.trim ? input.trim() : input;\n\tif (options.normalizeUnicodeSpaces) {\n\t\tnormalized = normalized.replace(UNICODE_SPACES, \" \");\n\t}\n\tif (options.stripAtPrefix && normalized.startsWith(\"@\")) {\n\t\tnormalized = normalized.slice(1);\n\t}\n\n\tif (options.expandTilde ?? true) {\n\t\tconst home = options.homeDir ?? homedir();\n\t\tif (normalized === \"~\") return home;\n\t\tif (normalized.startsWith(\"~/\") || (process.platform === \"win32\" && normalized.startsWith(\"~\\\\\"))) {\n\t\t\treturn join(home, normalized.slice(2));\n\t\t}\n\t}\n\n\tif (/^file:\\/\\//.test(normalized)) {\n\t\treturn fileURLToPath(normalized);\n\t}\n\n\treturn normalized;\n}\n\nexport function resolvePath(input: string, baseDir: string = process.cwd(), options: PathInputOptions = {}): string {\n\tconst normalized = normalizePath(input, options);\n\tconst normalizedBaseDir = normalizePath(baseDir);\n\treturn isAbsolute(normalized) ? nodeResolvePath(normalized) : nodeResolvePath(normalizedBaseDir, normalized);\n}\n"]}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAChC,6DAA6D;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0DAA0D;IAC1D,sBAAsB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAsBnF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,MAAsB,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAIlH"}
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,WAAW,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,cAAc,GAAG,0CAA0C,CAAC;AAelE,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,OAAO,GAAqB,EAAE,EAAU;IACpF,IAAI,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACrD,IAAI,OAAO,CAAC,sBAAsB,EAAE,CAAC;QACpC,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,OAAO,CAAC,aAAa,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzD,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,IAAI,IAAI,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC;QAC1C,IAAI,UAAU,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QACpC,IAAI,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACnG,OAAO,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,CAAC;IACF,CAAC;IAED,IAAI,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,OAAO,aAAa,CAAC,UAAU,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,UAAU,CAAC;AAAA,CAClB;AAED,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,OAAO,GAAW,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,GAAqB,EAAE,EAAU;IACnH,MAAM,UAAU,GAAG,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,iBAAiB,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAC;AAAA,CAC7G","sourcesContent":["import { homedir } from \"node:os\";\nimport { isAbsolute, join, resolve as nodeResolvePath } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nexport interface PathInputOptions {\n\t/** Trim leading/trailing whitespace before normalization. */\n\ttrim?: boolean;\n\t/** Expand leading `~` to a home directory. Defaults to true. */\n\texpandTilde?: boolean;\n\t/** Home directory used for `~` expansion. Defaults to `os.homedir()`. */\n\thomeDir?: string;\n\t/** Strip a leading `@`, used for CLI @file paths. */\n\tstripAtPrefix?: boolean;\n\t/** Normalize unicode space variants to regular spaces. */\n\tnormalizeUnicodeSpaces?: boolean;\n}\n\nexport function normalizePath(input: string, options: PathInputOptions = {}): string {\n\tlet normalized = options.trim ? input.trim() : input;\n\tif (options.normalizeUnicodeSpaces) {\n\t\tnormalized = normalized.replace(UNICODE_SPACES, \" \");\n\t}\n\tif (options.stripAtPrefix && normalized.startsWith(\"@\")) {\n\t\tnormalized = normalized.slice(1);\n\t}\n\n\tif (options.expandTilde ?? true) {\n\t\tconst home = options.homeDir ?? homedir();\n\t\tif (normalized === \"~\") return home;\n\t\tif (normalized.startsWith(\"~/\") || (process.platform === \"win32\" && normalized.startsWith(\"~\\\\\"))) {\n\t\t\treturn join(home, normalized.slice(2));\n\t\t}\n\t}\n\n\tif (/^file:\\/\\//.test(normalized)) {\n\t\treturn fileURLToPath(normalized);\n\t}\n\n\treturn normalized;\n}\n\nexport function resolvePath(input: string, baseDir: string = process.cwd(), options: PathInputOptions = {}): string {\n\tconst normalized = normalizePath(input, options);\n\tconst normalizedBaseDir = normalizePath(baseDir);\n\treturn isAbsolute(normalized) ? nodeResolvePath(normalized) : nodeResolvePath(normalizedBaseDir, normalized);\n}\n"]}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,WAAW,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,cAAc,GAAG,0CAA0C,CAAC;AAelE,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,OAAO,GAAqB,EAAE;IAC1E,IAAI,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACrD,IAAI,OAAO,CAAC,sBAAsB,EAAE,CAAC;QACpC,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,OAAO,CAAC,aAAa,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzD,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,IAAI,IAAI,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC;QAC1C,IAAI,UAAU,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QACpC,IAAI,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACnG,OAAO,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,CAAC;IACF,CAAC;IAED,IAAI,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,OAAO,aAAa,CAAC,UAAU,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,UAAU,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,OAAO,GAAW,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,GAAqB,EAAE;IACzG,MAAM,UAAU,GAAG,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,iBAAiB,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAC;AAC9G,CAAC","sourcesContent":["import { homedir } from \"node:os\";\nimport { isAbsolute, join, resolve as nodeResolvePath } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nexport interface PathInputOptions {\n\t/** Trim leading/trailing whitespace before normalization. */\n\ttrim?: boolean;\n\t/** Expand leading `~` to a home directory. Defaults to true. */\n\texpandTilde?: boolean;\n\t/** Home directory used for `~` expansion. Defaults to `os.homedir()`. */\n\thomeDir?: string;\n\t/** Strip a leading `@`, used for CLI @file paths. */\n\tstripAtPrefix?: boolean;\n\t/** Normalize unicode space variants to regular spaces. */\n\tnormalizeUnicodeSpaces?: boolean;\n}\n\nexport function normalizePath(input: string, options: PathInputOptions = {}): string {\n\tlet normalized = options.trim ? input.trim() : input;\n\tif (options.normalizeUnicodeSpaces) {\n\t\tnormalized = normalized.replace(UNICODE_SPACES, \" \");\n\t}\n\tif (options.stripAtPrefix && normalized.startsWith(\"@\")) {\n\t\tnormalized = normalized.slice(1);\n\t}\n\n\tif (options.expandTilde ?? true) {\n\t\tconst home = options.homeDir ?? homedir();\n\t\tif (normalized === \"~\") return home;\n\t\tif (normalized.startsWith(\"~/\") || (process.platform === \"win32\" && normalized.startsWith(\"~\\\\\"))) {\n\t\t\treturn join(home, normalized.slice(2));\n\t\t}\n\t}\n\n\tif (/^file:\\/\\//.test(normalized)) {\n\t\treturn fileURLToPath(normalized);\n\t}\n\n\treturn normalized;\n}\n\nexport function resolvePath(input: string, baseDir: string = process.cwd(), options: PathInputOptions = {}): string {\n\tconst normalized = normalizePath(input, options);\n\tconst normalizedBaseDir = normalizePath(baseDir);\n\treturn isAbsolute(normalized) ? nodeResolvePath(normalized) : nodeResolvePath(normalizedBaseDir, normalized);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"shell-output.d.ts","sourceRoot":"","sources":["../../src/utils/shell-output.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA+BxD","sourcesContent":["/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, 0x7F, 0x80-0x9F; except 0x09, 0x0a, 0x0d)\n\t\t\tif (code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f)) return false;\n\n\t\t\t// Filter out lone surrogate code units. Valid surrogate pairs are emitted by Array.from as a\n\t\t\t// two-code-unit string whose code point is outside the surrogate range, so they survive.\n\t\t\tif (code >= 0xd800 && code <= 0xdfff) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n"]}
1
+ {"version":3,"file":"shell-output.d.ts","sourceRoot":"","sources":["../../src/utils/shell-output.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA+BxD"}
@@ -1 +1 @@
1
- {"version":3,"file":"shell-output.js","sourceRoot":"","sources":["../../src/utils/shell-output.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAU;IACzD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;SACpB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QACjB,yDAAyD;QACzD,iBAAiB;QACjB,8BAA8B;QAC9B,oBAAoB;QACpB,kCAAkC;QAClC,0CAA0C;QAE1C,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QAEjC,mEAAmE;QACnE,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAErC,sCAAsC;QACtC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAEjE,sFAAsF;QACtF,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QAElF,6FAA6F;QAC7F,yFAAyF;QACzF,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM;YAAE,OAAO,KAAK,CAAC;QAEnD,uCAAuC;QACvC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM;YAAE,OAAO,KAAK,CAAC;QAEnD,OAAO,IAAI,CAAC;IAAA,CACZ,CAAC;SACD,IAAI,CAAC,EAAE,CAAC,CAAC;AAAA,CACX","sourcesContent":["/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, 0x7F, 0x80-0x9F; except 0x09, 0x0a, 0x0d)\n\t\t\tif (code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f)) return false;\n\n\t\t\t// Filter out lone surrogate code units. Valid surrogate pairs are emitted by Array.from as a\n\t\t\t// two-code-unit string whose code point is outside the surrogate range, so they survive.\n\t\t\tif (code >= 0xd800 && code <= 0xdfff) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n"]}
1
+ {"version":3,"file":"shell-output.js","sourceRoot":"","sources":["../../src/utils/shell-output.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC/C,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;SACpB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAChB,yDAAyD;QACzD,iBAAiB;QACjB,8BAA8B;QAC9B,oBAAoB;QACpB,kCAAkC;QAClC,0CAA0C;QAE1C,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QAEjC,mEAAmE;QACnE,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAErC,sCAAsC;QACtC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAEjE,sFAAsF;QACtF,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QAElF,6FAA6F;QAC7F,yFAAyF;QACzF,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM;YAAE,OAAO,KAAK,CAAC;QAEnD,uCAAuC;QACvC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM;YAAE,OAAO,KAAK,CAAC;QAEnD,OAAO,IAAI,CAAC;IACb,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC,CAAC;AACZ,CAAC","sourcesContent":["/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, 0x7F, 0x80-0x9F; except 0x09, 0x0a, 0x0d)\n\t\t\tif (code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f)) return false;\n\n\t\t\t// Filter out lone surrogate code units. Valid surrogate pairs are emitted by Array.from as a\n\t\t\t// two-code-unit string whose code point is outside the surrogate range, so they survive.\n\t\t\tif (code >= 0xd800 && code <= 0xdfff) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n"]}
@@ -0,0 +1,23 @@
1
+ /** Append-only text storage that materializes immutable chunks only when a consumer requests a string. */
2
+ export declare class StreamingTextBuffer {
3
+ private chunks;
4
+ private cached;
5
+ private dirty;
6
+ private totalLength;
7
+ get length(): number;
8
+ append(chunk: string): void;
9
+ materialize(): string;
10
+ }
11
+ /**
12
+ * Projects growing structured text at geometrically spaced lengths and once at completion.
13
+ * Prefix work therefore remains bounded by a geometric series instead of running for every delta.
14
+ */
15
+ export declare class GeometricStreamingProjector<T> {
16
+ private readonly buffer;
17
+ private readonly project;
18
+ private nextProjectionLength;
19
+ constructor(project: (text: string) => T);
20
+ append(chunk: string): T | undefined;
21
+ finish(): T;
22
+ }
23
+ //# sourceMappingURL=streaming-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming-content.d.ts","sourceRoot":"","sources":["../../src/utils/streaming-content.ts"],"names":[],"mappings":"AAAA,0GAA0G;AAC1G,qBAAa,mBAAmB;IAC/B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,WAAW,CAAK;IAExB,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAK1B;IAED,WAAW,IAAI,MAAM,CAMpB;CACD;AAED;;;GAGG;AACH,qBAAa,2BAA2B,CAAC,CAAC;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,oBAAoB,CAAK;IAEjC,YAAY,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,CAAC,EAEvC;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAQnC;IAED,MAAM,IAAI,CAAC,CAEV;CACD"}
@@ -0,0 +1,52 @@
1
+ /** Append-only text storage that materializes immutable chunks only when a consumer requests a string. */
2
+ export class StreamingTextBuffer {
3
+ constructor() {
4
+ this.chunks = [];
5
+ this.cached = "";
6
+ this.dirty = false;
7
+ this.totalLength = 0;
8
+ }
9
+ get length() {
10
+ return this.totalLength;
11
+ }
12
+ append(chunk) {
13
+ if (chunk.length === 0)
14
+ return;
15
+ this.chunks.push(chunk);
16
+ this.totalLength += chunk.length;
17
+ this.dirty = true;
18
+ }
19
+ materialize() {
20
+ if (!this.dirty)
21
+ return this.cached;
22
+ this.cached = this.chunks.join("");
23
+ this.chunks = [this.cached];
24
+ this.dirty = false;
25
+ return this.cached;
26
+ }
27
+ }
28
+ /**
29
+ * Projects growing structured text at geometrically spaced lengths and once at completion.
30
+ * Prefix work therefore remains bounded by a geometric series instead of running for every delta.
31
+ */
32
+ export class GeometricStreamingProjector {
33
+ constructor(project) {
34
+ this.buffer = new StreamingTextBuffer();
35
+ this.nextProjectionLength = 1;
36
+ this.project = project;
37
+ }
38
+ append(chunk) {
39
+ this.buffer.append(chunk);
40
+ if (this.buffer.length < this.nextProjectionLength)
41
+ return undefined;
42
+ const projected = this.project(this.buffer.materialize());
43
+ do {
44
+ this.nextProjectionLength *= 2;
45
+ } while (this.nextProjectionLength <= this.buffer.length);
46
+ return projected;
47
+ }
48
+ finish() {
49
+ return this.project(this.buffer.materialize());
50
+ }
51
+ }
52
+ //# sourceMappingURL=streaming-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming-content.js","sourceRoot":"","sources":["../../src/utils/streaming-content.ts"],"names":[],"mappings":"AAAA,0GAA0G;AAC1G,MAAM,OAAO,mBAAmB;IAAhC;QACS,WAAM,GAAa,EAAE,CAAC;QACtB,WAAM,GAAG,EAAE,CAAC;QACZ,UAAK,GAAG,KAAK,CAAC;QACd,gBAAW,GAAG,CAAC,CAAC;IAoBzB,CAAC;IAlBA,IAAI,MAAM;QACT,OAAO,IAAI,CAAC,WAAW,CAAC;IACzB,CAAC;IAED,MAAM,CAAC,KAAa;QACnB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC/B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC,MAAM,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,WAAW;QACV,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC;QACpC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC;IACpB,CAAC;CACD;AAED;;;GAGG;AACH,MAAM,OAAO,2BAA2B;IAKvC,YAAY,OAA4B;QAJvB,WAAM,GAAG,IAAI,mBAAmB,EAAE,CAAC;QAE5C,yBAAoB,GAAG,CAAC,CAAC;QAGhC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;IAED,MAAM,CAAC,KAAa;QACnB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,oBAAoB;YAAE,OAAO,SAAS,CAAC;QACrE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;QAC1D,GAAG,CAAC;YACH,IAAI,CAAC,oBAAoB,IAAI,CAAC,CAAC;QAChC,CAAC,QAAQ,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;QAC1D,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,MAAM;QACL,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAChD,CAAC;CACD","sourcesContent":["/** Append-only text storage that materializes immutable chunks only when a consumer requests a string. */\nexport class StreamingTextBuffer {\n\tprivate chunks: string[] = [];\n\tprivate cached = \"\";\n\tprivate dirty = false;\n\tprivate totalLength = 0;\n\n\tget length(): number {\n\t\treturn this.totalLength;\n\t}\n\n\tappend(chunk: string): void {\n\t\tif (chunk.length === 0) return;\n\t\tthis.chunks.push(chunk);\n\t\tthis.totalLength += chunk.length;\n\t\tthis.dirty = true;\n\t}\n\n\tmaterialize(): string {\n\t\tif (!this.dirty) return this.cached;\n\t\tthis.cached = this.chunks.join(\"\");\n\t\tthis.chunks = [this.cached];\n\t\tthis.dirty = false;\n\t\treturn this.cached;\n\t}\n}\n\n/**\n * Projects growing structured text at geometrically spaced lengths and once at completion.\n * Prefix work therefore remains bounded by a geometric series instead of running for every delta.\n */\nexport class GeometricStreamingProjector<T> {\n\tprivate readonly buffer = new StreamingTextBuffer();\n\tprivate readonly project: (text: string) => T;\n\tprivate nextProjectionLength = 1;\n\n\tconstructor(project: (text: string) => T) {\n\t\tthis.project = project;\n\t}\n\n\tappend(chunk: string): T | undefined {\n\t\tthis.buffer.append(chunk);\n\t\tif (this.buffer.length < this.nextProjectionLength) return undefined;\n\t\tconst projected = this.project(this.buffer.materialize());\n\t\tdo {\n\t\t\tthis.nextProjectionLength *= 2;\n\t\t} while (this.nextProjectionLength <= this.buffer.length);\n\t\treturn projected;\n\t}\n\n\tfinish(): T {\n\t\treturn this.project(this.buffer.materialize());\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAC3C,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,MAAM,WAAW,gBAAgB;IAChC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IACjC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAaD;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAkF/F;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAyE/F;AAuBD;;;GAGG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAA6B,GACrC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAKzC","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines (except bash tail truncation edge case).\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\nexport const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line\n\nexport interface TruncationResult {\n\t/** The truncated content */\n\tcontent: string;\n\t/** Whether truncation occurred */\n\ttruncated: boolean;\n\t/** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\t/** Total number of lines in the original content */\n\ttotalLines: number;\n\t/** Total number of bytes in the original content */\n\ttotalBytes: number;\n\t/** Number of complete lines in the truncated output */\n\toutputLines: number;\n\t/** Number of bytes in the truncated output */\n\toutputBytes: number;\n\t/** Whether the last line was partially truncated (only for tail truncation edge case) */\n\tlastLinePartial: boolean;\n\t/** Whether the first line exceeded the byte limit (for head truncation) */\n\tfirstLineExceedsLimit: boolean;\n\t/** The max lines limit that was applied */\n\tmaxLines: number;\n\t/** The max bytes limit that was applied */\n\tmaxBytes: number;\n}\n\nexport interface TruncationOptions {\n\t/** Maximum number of lines (default: 2000) */\n\tmaxLines?: number;\n\t/** Maximum number of bytes (default: 50KB) */\n\tmaxBytes?: number;\n}\n\nfunction splitLinesForCounting(content: string): string[] {\n\tif (content.length === 0) {\n\t\treturn [];\n\t}\n\tconst lines = content.split(\"\\n\");\n\tif (content.endsWith(\"\\n\")) {\n\t\tlines.pop();\n\t}\n\treturn lines;\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n\tif (bytes < 1024) {\n\t\treturn `${bytes}B`;\n\t} else if (bytes < 1024 * 1024) {\n\t\treturn `${(bytes / 1024).toFixed(1)}KB`;\n\t} else {\n\t\treturn `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\t}\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = splitLinesForCounting(content);\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Check if first line alone exceeds byte limit\n\tconst firstLineBytes = Buffer.byteLength(lines[0], \"utf-8\");\n\tif (firstLineBytes > maxBytes) {\n\t\treturn {\n\t\t\tcontent: \"\",\n\t\t\ttruncated: true,\n\t\t\ttruncatedBy: \"bytes\",\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: 0,\n\t\t\toutputBytes: 0,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: true,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Collect complete lines that fit\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n\tfor (let i = 0; i < lines.length && i < maxLines; i++) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (i > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.push(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial: false,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n\n/**\n * Truncate content from the tail (keep last N lines/bytes).\n * Suitable for bash output where you want to see the end (errors, final results).\n *\n * May return partial first line if the last line of original content exceeds byte limit.\n */\nexport function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = splitLinesForCounting(content);\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Work backwards from the end\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\tlet lastLinePartial = false;\n\n\tfor (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\t// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,\n\t\t\t// take the end of the line (partial)\n\t\t\tif (outputLinesArr.length === 0) {\n\t\t\t\tconst truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);\n\t\t\t\toutputLinesArr.unshift(truncatedLine);\n\t\t\t\toutputBytesCount = Buffer.byteLength(truncatedLine, \"utf-8\");\n\t\t\t\tlastLinePartial = true;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.unshift(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n\n/**\n * Truncate a string to fit within a byte limit (from the end).\n * Handles multi-byte UTF-8 characters correctly.\n */\nfunction truncateStringToBytesFromEnd(str: string, maxBytes: number): string {\n\tconst buf = Buffer.from(str, \"utf-8\");\n\tif (buf.length <= maxBytes) {\n\t\treturn str;\n\t}\n\n\t// Start from the end, skip maxBytes back\n\tlet start = buf.length - maxBytes;\n\n\t// Find a valid UTF-8 boundary (start of a character)\n\twhile (start < buf.length && (buf[start] & 0xc0) === 0x80) {\n\t\tstart++;\n\t}\n\n\treturn buf.slice(start).toString(\"utf-8\");\n}\n\n/**\n * Truncate a single line to max characters, adding [truncated] suffix.\n * Used for grep match lines.\n */\nexport function truncateLine(\n\tline: string,\n\tmaxChars: number = GREP_MAX_LINE_LENGTH,\n): { text: string; wasTruncated: boolean } {\n\tif (line.length <= maxChars) {\n\t\treturn { text: line, wasTruncated: false };\n\t}\n\treturn { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };\n}\n"]}
1
+ {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAC3C,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,MAAM,WAAW,gBAAgB;IAChC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IACjC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AA+ED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CA2C/F;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAwC/F;AAuBD;;;GAGG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAA6B,GACrC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAKzC"}
@@ -20,6 +20,47 @@ function splitLinesForCounting(content) {
20
20
  }
21
21
  return lines;
22
22
  }
23
+ function measureTruncation(content, options) {
24
+ const lines = splitLinesForCounting(content);
25
+ return {
26
+ lines,
27
+ maxLines: options.maxLines ?? DEFAULT_MAX_LINES,
28
+ maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,
29
+ totalLines: lines.length,
30
+ totalBytes: Buffer.byteLength(content, "utf-8"),
31
+ };
32
+ }
33
+ function buildTruncationResult(measurement, outcome) {
34
+ return {
35
+ ...outcome,
36
+ totalLines: measurement.totalLines,
37
+ totalBytes: measurement.totalBytes,
38
+ outputBytes: Buffer.byteLength(outcome.content, "utf-8"),
39
+ lastLinePartial: outcome.lastLinePartial ?? false,
40
+ firstLineExceedsLimit: outcome.firstLineExceedsLimit ?? false,
41
+ maxLines: measurement.maxLines,
42
+ maxBytes: measurement.maxBytes,
43
+ };
44
+ }
45
+ function unchangedTruncationResult(content, measurement) {
46
+ if (measurement.totalLines > measurement.maxLines || measurement.totalBytes > measurement.maxBytes)
47
+ return undefined;
48
+ return buildTruncationResult(measurement, {
49
+ content,
50
+ truncated: false,
51
+ truncatedBy: null,
52
+ outputLines: measurement.totalLines,
53
+ });
54
+ }
55
+ function completedTruncationResult(measurement, outputLines, truncatedBy, lastLinePartial = false) {
56
+ return buildTruncationResult(measurement, {
57
+ content: outputLines.join("\n"),
58
+ truncated: true,
59
+ truncatedBy,
60
+ outputLines: outputLines.length,
61
+ lastLinePartial,
62
+ });
63
+ }
23
64
  /**
24
65
  * Format bytes as human-readable size.
25
66
  */
@@ -42,43 +83,21 @@ export function formatSize(bytes) {
42
83
  * returns empty content with firstLineExceedsLimit=true.
43
84
  */
44
85
  export function truncateHead(content, options = {}) {
45
- const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
46
- const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
47
- const totalBytes = Buffer.byteLength(content, "utf-8");
48
- const lines = splitLinesForCounting(content);
49
- const totalLines = lines.length;
50
- // Check if no truncation needed
51
- if (totalLines <= maxLines && totalBytes <= maxBytes) {
52
- return {
53
- content,
54
- truncated: false,
55
- truncatedBy: null,
56
- totalLines,
57
- totalBytes,
58
- outputLines: totalLines,
59
- outputBytes: totalBytes,
60
- lastLinePartial: false,
61
- firstLineExceedsLimit: false,
62
- maxLines,
63
- maxBytes,
64
- };
65
- }
86
+ const measurement = measureTruncation(content, options);
87
+ const { lines, maxBytes, maxLines } = measurement;
88
+ const unchanged = unchangedTruncationResult(content, measurement);
89
+ if (unchanged)
90
+ return unchanged;
66
91
  // Check if first line alone exceeds byte limit
67
92
  const firstLineBytes = Buffer.byteLength(lines[0], "utf-8");
68
93
  if (firstLineBytes > maxBytes) {
69
- return {
94
+ return buildTruncationResult(measurement, {
70
95
  content: "",
71
96
  truncated: true,
72
97
  truncatedBy: "bytes",
73
- totalLines,
74
- totalBytes,
75
98
  outputLines: 0,
76
- outputBytes: 0,
77
- lastLinePartial: false,
78
99
  firstLineExceedsLimit: true,
79
- maxLines,
80
- maxBytes,
81
- };
100
+ });
82
101
  }
83
102
  // Collect complete lines that fit
84
103
  const outputLinesArr = [];
@@ -98,21 +117,7 @@ export function truncateHead(content, options = {}) {
98
117
  if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
99
118
  truncatedBy = "lines";
100
119
  }
101
- const outputContent = outputLinesArr.join("\n");
102
- const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
103
- return {
104
- content: outputContent,
105
- truncated: true,
106
- truncatedBy,
107
- totalLines,
108
- totalBytes,
109
- outputLines: outputLinesArr.length,
110
- outputBytes: finalOutputBytes,
111
- lastLinePartial: false,
112
- firstLineExceedsLimit: false,
113
- maxLines,
114
- maxBytes,
115
- };
120
+ return completedTruncationResult(measurement, outputLinesArr, truncatedBy);
116
121
  }
117
122
  /**
118
123
  * Truncate content from the tail (keep last N lines/bytes).
@@ -121,27 +126,11 @@ export function truncateHead(content, options = {}) {
121
126
  * May return partial first line if the last line of original content exceeds byte limit.
122
127
  */
123
128
  export function truncateTail(content, options = {}) {
124
- const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
125
- const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
126
- const totalBytes = Buffer.byteLength(content, "utf-8");
127
- const lines = splitLinesForCounting(content);
128
- const totalLines = lines.length;
129
- // Check if no truncation needed
130
- if (totalLines <= maxLines && totalBytes <= maxBytes) {
131
- return {
132
- content,
133
- truncated: false,
134
- truncatedBy: null,
135
- totalLines,
136
- totalBytes,
137
- outputLines: totalLines,
138
- outputBytes: totalBytes,
139
- lastLinePartial: false,
140
- firstLineExceedsLimit: false,
141
- maxLines,
142
- maxBytes,
143
- };
144
- }
129
+ const measurement = measureTruncation(content, options);
130
+ const { lines, maxBytes, maxLines } = measurement;
131
+ const unchanged = unchangedTruncationResult(content, measurement);
132
+ if (unchanged)
133
+ return unchanged;
145
134
  // Work backwards from the end
146
135
  const outputLinesArr = [];
147
136
  let outputBytesCount = 0;
@@ -169,21 +158,7 @@ export function truncateTail(content, options = {}) {
169
158
  if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
170
159
  truncatedBy = "lines";
171
160
  }
172
- const outputContent = outputLinesArr.join("\n");
173
- const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
174
- return {
175
- content: outputContent,
176
- truncated: true,
177
- truncatedBy,
178
- totalLines,
179
- totalBytes,
180
- outputLines: outputLinesArr.length,
181
- outputBytes: finalOutputBytes,
182
- lastLinePartial,
183
- firstLineExceedsLimit: false,
184
- maxLines,
185
- maxBytes,
186
- };
161
+ return completedTruncationResult(measurement, outputLinesArr, truncatedBy, lastLinePartial);
187
162
  }
188
163
  /**
189
164
  * Truncate a string to fit within a byte limit (from the end).
@@ -1 +1 @@
1
- {"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines (except bash tail truncation edge case).\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\nexport const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line\n\nexport interface TruncationResult {\n\t/** The truncated content */\n\tcontent: string;\n\t/** Whether truncation occurred */\n\ttruncated: boolean;\n\t/** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\t/** Total number of lines in the original content */\n\ttotalLines: number;\n\t/** Total number of bytes in the original content */\n\ttotalBytes: number;\n\t/** Number of complete lines in the truncated output */\n\toutputLines: number;\n\t/** Number of bytes in the truncated output */\n\toutputBytes: number;\n\t/** Whether the last line was partially truncated (only for tail truncation edge case) */\n\tlastLinePartial: boolean;\n\t/** Whether the first line exceeded the byte limit (for head truncation) */\n\tfirstLineExceedsLimit: boolean;\n\t/** The max lines limit that was applied */\n\tmaxLines: number;\n\t/** The max bytes limit that was applied */\n\tmaxBytes: number;\n}\n\nexport interface TruncationOptions {\n\t/** Maximum number of lines (default: 2000) */\n\tmaxLines?: number;\n\t/** Maximum number of bytes (default: 50KB) */\n\tmaxBytes?: number;\n}\n\nfunction splitLinesForCounting(content: string): string[] {\n\tif (content.length === 0) {\n\t\treturn [];\n\t}\n\tconst lines = content.split(\"\\n\");\n\tif (content.endsWith(\"\\n\")) {\n\t\tlines.pop();\n\t}\n\treturn lines;\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n\tif (bytes < 1024) {\n\t\treturn `${bytes}B`;\n\t} else if (bytes < 1024 * 1024) {\n\t\treturn `${(bytes / 1024).toFixed(1)}KB`;\n\t} else {\n\t\treturn `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\t}\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = splitLinesForCounting(content);\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Check if first line alone exceeds byte limit\n\tconst firstLineBytes = Buffer.byteLength(lines[0], \"utf-8\");\n\tif (firstLineBytes > maxBytes) {\n\t\treturn {\n\t\t\tcontent: \"\",\n\t\t\ttruncated: true,\n\t\t\ttruncatedBy: \"bytes\",\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: 0,\n\t\t\toutputBytes: 0,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: true,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Collect complete lines that fit\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n\tfor (let i = 0; i < lines.length && i < maxLines; i++) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (i > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.push(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial: false,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n\n/**\n * Truncate content from the tail (keep last N lines/bytes).\n * Suitable for bash output where you want to see the end (errors, final results).\n *\n * May return partial first line if the last line of original content exceeds byte limit.\n */\nexport function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = splitLinesForCounting(content);\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Work backwards from the end\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\tlet lastLinePartial = false;\n\n\tfor (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\t// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,\n\t\t\t// take the end of the line (partial)\n\t\t\tif (outputLinesArr.length === 0) {\n\t\t\t\tconst truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);\n\t\t\t\toutputLinesArr.unshift(truncatedLine);\n\t\t\t\toutputBytesCount = Buffer.byteLength(truncatedLine, \"utf-8\");\n\t\t\t\tlastLinePartial = true;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.unshift(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n\n/**\n * Truncate a string to fit within a byte limit (from the end).\n * Handles multi-byte UTF-8 characters correctly.\n */\nfunction truncateStringToBytesFromEnd(str: string, maxBytes: number): string {\n\tconst buf = Buffer.from(str, \"utf-8\");\n\tif (buf.length <= maxBytes) {\n\t\treturn str;\n\t}\n\n\t// Start from the end, skip maxBytes back\n\tlet start = buf.length - maxBytes;\n\n\t// Find a valid UTF-8 boundary (start of a character)\n\twhile (start < buf.length && (buf[start] & 0xc0) === 0x80) {\n\t\tstart++;\n\t}\n\n\treturn buf.slice(start).toString(\"utf-8\");\n}\n\n/**\n * Truncate a single line to max characters, adding [truncated] suffix.\n * Used for grep match lines.\n */\nexport function truncateLine(\n\tline: string,\n\tmaxChars: number = GREP_MAX_LINE_LENGTH,\n): { text: string; wasTruncated: boolean } {\n\tif (line.length <= maxChars) {\n\t\treturn { text: line, wasTruncated: false };\n\t}\n\treturn { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };\n}\n"]}
1
+ {"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines (except bash tail truncation edge case).\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\nexport const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line\n\nexport interface TruncationResult {\n\t/** The truncated content */\n\tcontent: string;\n\t/** Whether truncation occurred */\n\ttruncated: boolean;\n\t/** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\t/** Total number of lines in the original content */\n\ttotalLines: number;\n\t/** Total number of bytes in the original content */\n\ttotalBytes: number;\n\t/** Number of complete lines in the truncated output */\n\toutputLines: number;\n\t/** Number of bytes in the truncated output */\n\toutputBytes: number;\n\t/** Whether the last line was partially truncated (only for tail truncation edge case) */\n\tlastLinePartial: boolean;\n\t/** Whether the first line exceeded the byte limit (for head truncation) */\n\tfirstLineExceedsLimit: boolean;\n\t/** The max lines limit that was applied */\n\tmaxLines: number;\n\t/** The max bytes limit that was applied */\n\tmaxBytes: number;\n}\n\nexport interface TruncationOptions {\n\t/** Maximum number of lines (default: 2000) */\n\tmaxLines?: number;\n\t/** Maximum number of bytes (default: 50KB) */\n\tmaxBytes?: number;\n}\n\nfunction splitLinesForCounting(content: string): string[] {\n\tif (content.length === 0) {\n\t\treturn [];\n\t}\n\tconst lines = content.split(\"\\n\");\n\tif (content.endsWith(\"\\n\")) {\n\t\tlines.pop();\n\t}\n\treturn lines;\n}\n\ninterface TruncationMeasurement {\n\tlines: string[];\n\tmaxLines: number;\n\tmaxBytes: number;\n\ttotalLines: number;\n\ttotalBytes: number;\n}\n\ninterface TruncationOutcome {\n\tcontent: string;\n\ttruncated: boolean;\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\toutputLines: number;\n\tlastLinePartial?: boolean;\n\tfirstLineExceedsLimit?: boolean;\n}\n\nfunction measureTruncation(content: string, options: TruncationOptions): TruncationMeasurement {\n\tconst lines = splitLinesForCounting(content);\n\treturn {\n\t\tlines,\n\t\tmaxLines: options.maxLines ?? DEFAULT_MAX_LINES,\n\t\tmaxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,\n\t\ttotalLines: lines.length,\n\t\ttotalBytes: Buffer.byteLength(content, \"utf-8\"),\n\t};\n}\n\nfunction buildTruncationResult(measurement: TruncationMeasurement, outcome: TruncationOutcome): TruncationResult {\n\treturn {\n\t\t...outcome,\n\t\ttotalLines: measurement.totalLines,\n\t\ttotalBytes: measurement.totalBytes,\n\t\toutputBytes: Buffer.byteLength(outcome.content, \"utf-8\"),\n\t\tlastLinePartial: outcome.lastLinePartial ?? false,\n\t\tfirstLineExceedsLimit: outcome.firstLineExceedsLimit ?? false,\n\t\tmaxLines: measurement.maxLines,\n\t\tmaxBytes: measurement.maxBytes,\n\t};\n}\n\nfunction unchangedTruncationResult(content: string, measurement: TruncationMeasurement): TruncationResult | undefined {\n\tif (measurement.totalLines > measurement.maxLines || measurement.totalBytes > measurement.maxBytes) return undefined;\n\treturn buildTruncationResult(measurement, {\n\t\tcontent,\n\t\ttruncated: false,\n\t\ttruncatedBy: null,\n\t\toutputLines: measurement.totalLines,\n\t});\n}\n\nfunction completedTruncationResult(\n\tmeasurement: TruncationMeasurement,\n\toutputLines: string[],\n\ttruncatedBy: \"lines\" | \"bytes\",\n\tlastLinePartial = false,\n): TruncationResult {\n\treturn buildTruncationResult(measurement, {\n\t\tcontent: outputLines.join(\"\\n\"),\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\toutputLines: outputLines.length,\n\t\tlastLinePartial,\n\t});\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n\tif (bytes < 1024) {\n\t\treturn `${bytes}B`;\n\t} else if (bytes < 1024 * 1024) {\n\t\treturn `${(bytes / 1024).toFixed(1)}KB`;\n\t} else {\n\t\treturn `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\t}\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst measurement = measureTruncation(content, options);\n\tconst { lines, maxBytes, maxLines } = measurement;\n\n\tconst unchanged = unchangedTruncationResult(content, measurement);\n\tif (unchanged) return unchanged;\n\n\t// Check if first line alone exceeds byte limit\n\tconst firstLineBytes = Buffer.byteLength(lines[0], \"utf-8\");\n\tif (firstLineBytes > maxBytes) {\n\t\treturn buildTruncationResult(measurement, {\n\t\t\tcontent: \"\",\n\t\t\ttruncated: true,\n\t\t\ttruncatedBy: \"bytes\",\n\t\t\toutputLines: 0,\n\t\t\tfirstLineExceedsLimit: true,\n\t\t});\n\t}\n\n\t// Collect complete lines that fit\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n\tfor (let i = 0; i < lines.length && i < maxLines; i++) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (i > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.push(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\treturn completedTruncationResult(measurement, outputLinesArr, truncatedBy);\n}\n\n/**\n * Truncate content from the tail (keep last N lines/bytes).\n * Suitable for bash output where you want to see the end (errors, final results).\n *\n * May return partial first line if the last line of original content exceeds byte limit.\n */\nexport function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst measurement = measureTruncation(content, options);\n\tconst { lines, maxBytes, maxLines } = measurement;\n\n\tconst unchanged = unchangedTruncationResult(content, measurement);\n\tif (unchanged) return unchanged;\n\n\t// Work backwards from the end\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\tlet lastLinePartial = false;\n\n\tfor (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\t// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,\n\t\t\t// take the end of the line (partial)\n\t\t\tif (outputLinesArr.length === 0) {\n\t\t\t\tconst truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);\n\t\t\t\toutputLinesArr.unshift(truncatedLine);\n\t\t\t\toutputBytesCount = Buffer.byteLength(truncatedLine, \"utf-8\");\n\t\t\t\tlastLinePartial = true;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.unshift(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\treturn completedTruncationResult(measurement, outputLinesArr, truncatedBy, lastLinePartial);\n}\n\n/**\n * Truncate a string to fit within a byte limit (from the end).\n * Handles multi-byte UTF-8 characters correctly.\n */\nfunction truncateStringToBytesFromEnd(str: string, maxBytes: number): string {\n\tconst buf = Buffer.from(str, \"utf-8\");\n\tif (buf.length <= maxBytes) {\n\t\treturn str;\n\t}\n\n\t// Start from the end, skip maxBytes back\n\tlet start = buf.length - maxBytes;\n\n\t// Find a valid UTF-8 boundary (start of a character)\n\twhile (start < buf.length && (buf[start] & 0xc0) === 0x80) {\n\t\tstart++;\n\t}\n\n\treturn buf.slice(start).toString(\"utf-8\");\n}\n\n/**\n * Truncate a single line to max characters, adding [truncated] suffix.\n * Used for grep match lines.\n */\nexport function truncateLine(\n\tline: string,\n\tmaxChars: number = GREP_MAX_LINE_LENGTH,\n): { text: string; wasTruncated: boolean } {\n\tif (line.length <= maxChars) {\n\t\treturn { text: line, wasTruncated: false };\n\t}\n\treturn { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };\n}\n"]}
package/dist/uuid.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { uuidv7 } from "@caupulican/pi-ai";
1
+ export { uuidv7 } from "@caupulican/pi-ai/uuid";
2
2
  //# sourceMappingURL=uuid.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../src/uuid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["export { uuidv7 } from \"@caupulican/pi-ai\";\n"]}
1
+ {"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../src/uuid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/uuid.js CHANGED
@@ -1,2 +1,2 @@
1
- export { uuidv7 } from "@caupulican/pi-ai";
1
+ export { uuidv7 } from "@caupulican/pi-ai/uuid";
2
2
  //# sourceMappingURL=uuid.js.map