{"_id":"@cyberxninja-omp/pi-agent-core","name":"@cyberxninja-omp/pi-agent-core","dist-tags":{"latest":"17.5.3"},"versions":{"17.5.3":{"type":"module","name":"@cyberxninja-omp/pi-agent-core","version":"17.5.3","description":"General-purpose agent with transport abstraction, state management, and attachment support","homepage":"https://omp.sh","author":{"name":"Can Boluk"},"contributors":[{"name":"Mario Zechner"}],"license":"MIT","repository":{"type":"git","url":"git+https://github.com/can1357/oh-my-pi.git","directory":"packages/agent"},"bugs":{"url":"https://github.com/can1357/oh-my-pi/issues"},"keywords":["ai","agent","llm","transport","state-management"],"main":"./src/index.ts","types":"./dist/types/index.d.ts","scripts":{"check":"biome check . && bun run check:types","check:types":"tsgo -p tsconfig.json --noEmit","lint":"biome lint .","test":"bun test --parallel","fix":"biome check --write --unsafe .","fmt":"biome format --write ."},"dependencies":{"@cyberxninja-omp/pi-ai":"17.5.3","@cyberxninja-omp/pi-catalog":"17.5.3","@cyberxninja-omp/pi-natives":"17.5.3","@cyberxninja-omp/pi-utils":"17.5.3","@cyberxninja-omp/pi-wire":"17.5.3","@cyberxninja-omp/snapcompact":"17.5.3","@opentelemetry/api":"^1.9.1"},"devDependencies":{"@cyberxninja-omp/omptype":"17.5.3","@opentelemetry/context-async-hooks":"^2.9.0","@opentelemetry/sdk-trace-base":"^2.9.0","@types/bun":"^1.3.14"},"engines":{"bun":">=1.3.14"},"exports":{".":{"types":"./dist/types/index.d.ts","import":"./src/index.ts"},"./compaction":{"types":"./dist/types/compaction.d.ts","import":"./src/compaction.ts"},"./compaction/*":{"types":"./dist/types/compaction/*.d.ts","import":"./src/compaction/*.ts"},"./*":{"types":"./dist/types/*.d.ts","import":"./src/*.ts"}},"_id":"@cyberxninja-omp/pi-agent-core@17.5.3","_integrity":"sha512-JJXb8YgVfVnS9JxZduDk8oNMm4ELIvIC/exJqJplKS/PgyiSbMoPMrhAwCm41u4FdWlZ3l33z60JGlkFHNG9HA==","_resolved":"/tmp/cxn-pack-BQdkhM/cyberxninja-omp-pi-agent-core-17.5.3.tgz","_from":"file:/tmp/cxn-pack-BQdkhM/cyberxninja-omp-pi-agent-core-17.5.3.tgz","_nodeVersion":"24.3.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-JJXb8YgVfVnS9JxZduDk8oNMm4ELIvIC/exJqJplKS/PgyiSbMoPMrhAwCm41u4FdWlZ3l33z60JGlkFHNG9HA==","shasum":"e433ca4fc27df3a117b4c13824cb09d6d9c22285","tarball":"https://registry.npmjs.org/@cyberxninja-omp/pi-agent-core/-/pi-agent-core-17.5.3.tgz","fileCount":71,"unpackedSize":850217,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHYOIwklCrswZTZqpOy2gM5TT5qKOE8vRool4PGj+9OzAiAK4HiiR3zZvMej9ix7/fNyoqcq8y+qaZFdkR7771bQrg=="}]},"_npmUser":{"name":"cyberxninja","email":"jailbreaker20th@gmail.com"},"directories":{},"maintainers":[{"name":"cyberxninja","email":"jailbreaker20th@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pi-agent-core_17.5.3_1787233057300_0.5100402478618837"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-20T13:37:37.152Z","17.5.3":"2026-08-20T13:37:37.471Z","modified":"2026-08-20T13:37:37.702Z"},"maintainers":[{"name":"cyberxninja","email":"jailbreaker20th@gmail.com"}],"description":"General-purpose agent with transport abstraction, state management, and attachment support","homepage":"https://omp.sh","keywords":["ai","agent","llm","transport","state-management"],"repository":{"type":"git","url":"git+https://github.com/can1357/oh-my-pi.git","directory":"packages/agent"},"contributors":[{"name":"Mario Zechner"}],"author":{"name":"Can Boluk"},"bugs":{"url":"https://github.com/can1357/oh-my-pi/issues"},"license":"MIT","readme":"# @cyberxninja-omp/pi-agent\n\nStateful agent with tool execution and event streaming. Built on `@cyberxninja-omp/pi-ai`.\n\n## Installation\n\n```bash\nnpm install @cyberxninja-omp/pi-agent\n```\n\n## Quick Start\n\n```typescript\nimport { Agent } from \"@cyberxninja-omp/pi-agent\";\nimport { getModel } from \"@cyberxninja-omp/pi-ai\";\n\nconst agent = new Agent({\n\tinitialState: {\n\t\tsystemPrompt: [\"You are a helpful assistant.\"],\n\t\tmodel: getModel(\"anthropic\", \"claude-sonnet-4-20250514\"),\n\t},\n});\n\nagent.subscribe((event) => {\n\tif (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n\t\t// Stream just the new text chunk\n\t\tprocess.stdout.write(event.assistantMessageEvent.delta);\n\t}\n});\n\nawait agent.prompt(\"Hello!\");\n```\n\n## Core Concepts\n\n### AgentMessage vs LLM Message\n\nThe agent works with `AgentMessage`, a flexible type that can include:\n\n- Standard LLM messages (`user`, `assistant`, `toolResult`)\n- Custom app-specific message types via declaration merging\n\nLLMs only understand `user`, `assistant`, and `toolResult`. The `convertToLlm` function bridges this gap by filtering and transforming messages before each LLM call.\n\n### Message Flow\n\n```\nAgentMessage[] → transformContext() → AgentMessage[] → convertToLlm() → Message[] → LLM\n                    (optional)                           (required)\n```\n\n1. **transformContext**: Prune old messages, inject external context\n2. **convertToLlm**: Filter out UI-only messages, convert custom types to LLM format\n\n## Event Flow\n\nThe agent emits events for UI updates. Understanding the event sequence helps build responsive interfaces.\n\n### prompt() Event Sequence\n\nWhen you call `prompt(\"Hello\")`:\n\n```\nprompt(\"Hello\")\n├─ agent_start\n├─ turn_start\n├─ message_start   { message: userMessage }      // Your prompt\n├─ message_end     { message: userMessage }\n├─ message_start   { message: assistantMessage } // LLM starts responding\n├─ message_update  { message: partial... }       // Streaming chunks\n├─ message_update  { message: partial... }\n├─ message_end     { message: assistantMessage } // Complete response\n├─ turn_end        { message, toolResults: [] }\n└─ agent_end       { messages: [...] }\n```\n\n### With Tool Calls\n\nIf the assistant calls tools, the loop continues:\n\n```\nprompt(\"Read config.json\")\n├─ agent_start\n├─ turn_start\n├─ message_start/end  { userMessage }\n├─ message_start      { assistantMessage with toolCall }\n├─ message_update...\n├─ message_end        { assistantMessage }\n├─ tool_execution_start  { toolCallId, toolName, args }\n├─ tool_execution_update { partialResult }           // If tool streams\n├─ tool_execution_end    { toolCallId, result }\n├─ message_start/end  { toolResultMessage }\n├─ turn_end           { message, toolResults: [toolResult] }\n│\n├─ turn_start                                        // Next turn\n├─ message_start      { assistantMessage }           // LLM responds to tool result\n├─ message_update...\n├─ message_end\n├─ turn_end\n└─ agent_end\n```\n\n### continue() Event Sequence\n\n`continue()` resumes from existing context without adding a new message. Use it for retries after errors.\n\n```typescript\n// After an error, retry from current state\nawait agent.continue();\n```\n\nThe last message in context must be `user` or `toolResult` (not `assistant`).\n\n### Event Types\n\n| Event                   | Description                                                     |\n| ----------------------- | --------------------------------------------------------------- |\n| `agent_start`           | Agent begins processing                                         |\n| `agent_end`             | Agent completes with all new messages                           |\n| `turn_start`            | New turn begins (one LLM call + tool executions)                |\n| `turn_end`              | Turn completes with assistant message and tool results          |\n| `message_start`         | Any message begins (user, assistant, toolResult)                |\n| `message_update`        | **Assistant only.** Includes `assistantMessageEvent` with delta |\n| `message_end`           | Message completes                                               |\n| `tool_execution_start`  | Tool begins                                                     |\n| `tool_execution_update` | Tool streams progress                                           |\n| `tool_execution_end`    | Tool completes                                                  |\n\n## Agent Options\n\n```typescript\nconst agent = new Agent({\n  // Initial state\n  initialState: {\n    systemPrompt: string[],\n    model: Model,\n    thinkingLevel: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\",\n    tools: AgentTool<any>[],\n    messages: AgentMessage[],\n  },\n\n  // Convert AgentMessage[] to LLM Message[] (required for custom message types)\n  convertToLlm: (messages) => messages.filter(...),\n\n  // Transform context before convertToLlm (for pruning, compaction)\n  transformContext: async (messages, signal) => pruneOldMessages(messages),\n\n  // How to handle queued messages: \"one-at-a-time\" (default) or \"all\"\n  queueMode: \"one-at-a-time\",\n\n  // Custom stream function (for proxy backends)\n  streamFn: streamProxy,\n\n  // Dynamic model-scoped API key resolution (for expiring OAuth tokens)\n  getApiKey: async (model) => tokenForModel(model),\n\n  // Tool execution context (late-bound UI/session access)\n  getToolContext: () => ({ /* app-defined */ }),\n});\n```\n\n## Agent State\n\n```typescript\ninterface AgentState {\n\tsystemPrompt: string[];\n\tmodel: Model;\n\tthinkingLevel: ThinkingLevel;\n\ttools: AgentTool<any>[];\n\tmessages: AgentMessage[];\n\tisStreaming: boolean;\n\tstreamMessage: AgentMessage | null; // Current partial during streaming\n\tpendingToolCalls: Set<string>;\n\terror?: string;\n}\n```\n\nAccess via `agent.state`. During streaming, `streamMessage` contains the partial assistant message.\n\n## Methods\n\n### Prompting\n\n```typescript\n// Text prompt\nawait agent.prompt(\"Hello\");\n\n// With images\nawait agent.prompt(\"What's in this image?\", [{ type: \"image\", data: base64Data, mimeType: \"image/jpeg\" }]);\n\n// AgentMessage directly\nawait agent.prompt({ role: \"user\", content: \"Hello\", timestamp: Date.now() });\n\n// Continue from current context (last message must be user or toolResult)\nawait agent.continue();\n```\n\n### State Management\n\n```typescript\nagent.setSystemPrompt(\"New prompt\");\nagent.setModel(getModel(\"openai\", \"gpt-4o\"));\nagent.setThinkingLevel(\"medium\");\nagent.setTools([myTool]);\nagent.replaceMessages(newMessages);\nagent.appendMessage(message);\nagent.clearMessages();\nagent.reset(); // Clear everything\n```\n\n### Control\n\n```typescript\nagent.abort(); // Cancel current operation\nawait agent.waitForIdle(); // Wait for completion\n```\n\n### Events\n\n```typescript\nconst unsubscribe = agent.subscribe((event) => {\n\tconsole.log(event.type);\n});\nunsubscribe();\n```\n\n## Steering & Follow-up\n\nQueue messages to inject during tool execution (steering) or after the agent would otherwise stop (follow-up):\n\n```typescript\nagent.setSteeringMode(\"one-at-a-time\");\nagent.setInterruptMode(\"immediate\");\n\n// While agent is running tools\nagent.steer({\n\trole: \"user\",\n\tcontent: \"Stop! Do this instead.\",\n\ttimestamp: Date.now(),\n});\n\n// Queue a follow-up to run after the current turn completes\nagent.followUp({\n\trole: \"user\",\n\tcontent: \"After that, summarize the changes.\",\n\ttimestamp: Date.now(),\n});\n```\n\nSteering messages are checked after each tool call by default. Set `interruptMode` to `\"wait\"` to defer\nsteering until the current turn completes.\n\n## Custom Message Types\n\nExtend `AgentMessage` via declaration merging:\n\n```typescript\ndeclare module \"@cyberxninja-omp/pi-agent\" {\n\tinterface CustomAgentMessages {\n\t\tnotification: { role: \"notification\"; text: string; timestamp: number };\n\t}\n}\n\n// Now valid\nconst msg: AgentMessage = { role: \"notification\", text: \"Info\", timestamp: Date.now() };\n```\n\nHandle custom types in `convertToLlm`:\n\n```typescript\nconst agent = new Agent({\n\tconvertToLlm: (messages) =>\n\t\tmessages.flatMap((m) => {\n\t\t\tif (m.role === \"notification\") return []; // Filter out\n\t\t\treturn [m];\n\t\t}),\n});\n```\n\n## Tools\n\nDefine tools using `AgentTool` with an omptype parameter schema.\n\n```typescript\nimport { type } from \"@cyberxninja-omp/omptype\";\n\nconst readFileTool: AgentTool = {\n\tname: \"read_file\",\n\tlabel: \"Read File\", // For UI display\n\tdescription: \"Read a file's contents\",\n\tparameters: type({\n\t\tpath: type(\"string\").describe(\"File path\"),\n\t}),\n\texecute: async (toolCallId, params, signal, onUpdate, context) => {\n\t\tconst content = await fs.readFile(params.path, \"utf-8\");\n\n\t\t// Optional: stream progress\n\t\tonUpdate?.({ content: [{ type: \"text\", text: \"Reading...\" }], details: {} });\n\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: content }],\n\t\t\tdetails: { path: params.path, size: content.length },\n\t\t};\n\t},\n};\n\nagent.setTools([readFileTool]);\n```\n\n### Error Handling\n\n**Throw an error** when a tool fails. Do not return error messages as content.\n\n```typescript\nexecute: async (toolCallId, params, signal, onUpdate) => {\n\tif (!fs.existsSync(params.path)) {\n\t\tthrow new Error(`File not found: ${params.path}`);\n\t}\n\t// Return content only on success\n\treturn { content: [{ type: \"text\", text: \"...\" }] };\n};\n```\n\nThrown errors are caught by the agent and reported to the LLM as tool errors with `isError: true`.\n\n## Proxy Usage\n\nFor browser apps that proxy through a backend:\n\n```typescript\nimport { Agent, streamProxy } from \"@cyberxninja-omp/pi-agent\";\n\nconst agent = new Agent({\n\tstreamFn: (model, context, options) =>\n\t\tstreamProxy(model, context, {\n\t\t\t...options,\n\t\t\tauthToken: \"...\",\n\t\t\tproxyUrl: \"https://your-server.com\",\n\t\t}),\n});\n```\n\n## Low-Level API\n\nFor direct control without the Agent class:\n\n```typescript\nimport { agentLoop, agentLoopContinue } from \"@cyberxninja-omp/pi-agent\";\n\nconst context: AgentContext = {\n\tsystemPrompt: [\"You are helpful.\"],\n\tmessages: [],\n\ttools: [],\n};\n\nconst config: AgentLoopConfig = {\n\tmodel: getModel(\"openai\", \"gpt-4o\"),\n\tconvertToLlm: (msgs) => msgs.filter((m) => [\"user\", \"assistant\", \"toolResult\"].includes(m.role)),\n};\n\nconst userMessage = { role: \"user\", content: \"Hello\", timestamp: Date.now() };\n\nfor await (const event of agentLoop([userMessage], context, config)) {\n\tconsole.log(event.type);\n}\n\n// Continue from existing context\nfor await (const event of agentLoopContinue(context, config)) {\n\tconsole.log(event.type);\n}\n```\n\n## Run-level telemetry\nEvery `invoke_agent` produces two values alongside the OTEL spans:\n\n- **`AgentRunSummary`** — chat / tool / usage / cost / error counters bucketed\n  by status, with per-tool-name breakdowns. Pure aggregation, safe to\n  persist, diff, or assert.\n- **`AgentRunCoverage`** — sorted+deduped `toolsAvailable` / `toolsInvoked` /\n  `toolsUnused` / `modelsUsed` / `providersUsed` arrays. Stable for snapshot\n  tests.\n\nThree delivery channels (use whichever fits):\n\n### `agent_end` event (additive)\n\n```typescript\nfor await (const event of agentLoop([userMessage], context, {\n\t...config,\n\ttelemetry: {},\n})) {\n\tif (event.type === \"agent_end\" && event.telemetry) {\n\t\tconsole.log(\"tokens:\", event.telemetry.usage.totalTokens);\n\t\tconsole.log(\"unused tools:\", event.coverage?.toolsUnused);\n\t}\n}\n```\n\nThe `messages` field is unchanged. Consumers that ignore `telemetry`/\n`coverage` continue to work.\n\n### `onRunEnd` hook (non-fatal)\n\n```typescript\nconst stream = agentLoop([userMessage], context, {\n\t...config,\n\ttelemetry: {\n\t\tonRunEnd: (summary, coverage) => {\n\t\t\tawait persistRunSummary(summary, coverage);\n\t\t},\n\t},\n});\n```\n\nExceptions thrown from `onRunEnd` are caught and logged via `console.warn`;\na misbehaving telemetry consumer can **never** turn a successful agent run\ninto a failed one.\n\n### `agentLoopDetailed` (typed `detailed()` result)\n\nConvenience wrapper that preserves the existing stream API and exposes the\nrollup as a typed value:\n\n```typescript\nconst { stream, detailed } = agentLoopDetailed([userMessage], context, {\n\t...config,\n\ttelemetry: {}, // required to populate telemetry/coverage\n});\n\nfor await (const event of stream) {\n\t// existing event handling\n}\n\nconst { messages, telemetry, coverage } = await detailed();\n```\n\n`stream.result()` still resolves to `AgentMessage[]` — no breaking change.\n\n### Multi-run aggregation\n\nCallers that drive the loop multiple times (verify pass, benchmark harness)\nfold N summaries with `aggregateAgentRunSummaries` / `aggregateAgentRunCoverage`:\n\n```typescript\nimport {\n\taggregateAgentRunSummaries,\n\taggregateAgentRunCoverage,\n} from \"@cyberxninja-omp/pi-agent\";\n\nconst summaries: AgentRunSummary[] = [];\nconst coverages: AgentRunCoverage[] = [];\nfor (const target of targets) {\n\tconst { detailed } = agentLoopDetailed(/* ... */);\n\tconst result = await detailed();\n\tif (result.telemetry) summaries.push(result.telemetry);\n\tif (result.coverage) coverages.push(result.coverage);\n}\nconst runSummary = aggregateAgentRunSummaries(summaries);\nconst runCoverage = aggregateAgentRunCoverage(coverages);\n```\n\n### Tool status reporting\n\n`execute_tool` spans carry `pi.gen_ai.tool.status` ∈\n`\"ok\" | \"error\" | \"skipped\" | \"blocked\" | \"timeout\" | \"aborted\"`.\n`beforeToolCall` blocks throw a distinguishable `ToolCallBlockedError`\ninternally; the catch path reports `status: \"blocked\"` instead of conflating\nwith generic tool errors. Pre-run interrupts and tail-sweep skips are\nrecorded as `\"skipped\"` even though they never start a span.\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-bd7f8284be33c4b59383fa1a1532fdb8"}