npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@looops/protocol

v0.0.1

Published

Wire protocol for Loops: event content shapes, tool interface, streaming handler types. Shared between agent runtime and frontend clients.

Readme

@looops/protocol

The wire protocol shared between Loops agent runtimes and frontend clients. Defines:

  • The event envelope and content shapes that every session is built from.
  • The tool interface (LoopsTool) and the loopsTool() factory that produces AI-SDK-compatible tools with built-in approval gates.
  • The stream handler types the agent loop calls into.

This package contains types and one small factory function. It has no I/O, no storage, no networking. Every other Loops package depends on it.

Install

npm install @looops/protocol

Peer requirements (already pulled in transitively): ai and @ai-sdk/provider-utils.

Subpath exports

The package is split into three entry points so frontends don't pay for backend dependencies:

| Import path | Contents | Pulls in ai SDK? | | --- | --- | --- | | @looops/protocol/events | Event content types, EVENT_TYPES, EventEnvelope, EventEnvelopeOf | No — pure types | | @looops/protocol/tools | LoopsTool, LoopsToolSet, ApprovalRequiredError, loopsTool() | Yes | | @looops/protocol/streaming | StreamHandlers, StreamResult, *Info shapes | No — pure types | | @looops/protocol | Everything | Yes |

Frontends that only render an event log should import from @looops/protocol/events.

Events

A session is an ordered list of EventEnvelope records. Each envelope carries a type discriminant and a matching content payload.

import {
  EVENT_TYPES,
  type EventEnvelope,
  type EventEnvelopeOf,
} from '@looops/protocol/events'

EVENT_TYPES
// readonly ['user-message', 'assistant-message', 'tool-call', 'tool-result',
//           'tool-error', 'trigger', 'loop-triggered', 'other-agent-message',
//           'user-abort', 'child-generation', 'generation-failure', 'reasoning']

function render(event: EventEnvelope) {
  switch (event.type) {
    case 'tool-call':
      // event.content is narrowed to ToolCallContent
      return event.content.toolName
    case 'reasoning':
      return event.content.text
    // ...exhaustive
  }
}

// Narrow at the type level when you know the variant up front:
type ToolCallEvent = EventEnvelopeOf<'tool-call'>

Timestamps

EventEnvelope is generic over the timestamp type and defaults to string (ISO 8601, JSON-safe for the wire). Backends holding native Date values can parameterize:

type WireEvent   = EventEnvelope            // createdAt: string
type ServerEvent = EventEnvelope<Date>      // createdAt: Date

Event kinds

| type | When emitted | content shape | | --- | --- | --- | | user-message | User submits a prompt | { text } | | assistant-message | Model produces an assistant turn | { text } | | tool-call | Model invokes a tool | { toolCallId, toolName, args, readOnly?, approvalStatus?, approvalDescription? } | | tool-result | Tool returned a value | { toolCallId, toolName, result, output? } | | tool-error | Tool threw or was rejected | { toolCallId, toolName, error, isUserRejection? } | | reasoning | Thinking block from a reasoning model | { text, durationMs? } | | trigger | External trigger (cron, webhook) started a generation | { triggerName, payload? } | | loop-triggered | A child thread/loop was kicked off | { triggeredBy, parentGenerationId?, instructions? } | | child-generation | Child generation finished, summary recorded on the parent | { item, index, total, durationMs, parentGenerationId, childGenerationId, ... } | | other-agent-message | Another agent in a multi-agent setup spoke | { text } | | user-abort | User stopped a running generation | { generationId } | | generation-failure | Generation failed before completing | { error } |

Tools

loopsTool() is a drop-in replacement for the AI SDK's tool() that adds two things:

  1. Streaming output back to the UI via onToolOutputDelta(delta).
  2. Approval gates that pause the generation until the user confirms a tool call.
import { loopsTool } from '@looops/protocol/tools'
import { z } from 'zod'

export const writeFile = loopsTool({
  name: 'write_file',
  description: 'Write content to a file',
  readOnly: false,
  inputSchema: z.object({
    path: z.string(),
    content: z.string(),
  }),
  // Return a description string to require approval; null to auto-run.
  // Only consulted when the runtime's policy is 'ask'.
  requestApproval: ({ path }, ctx) =>
    ctx.mode === 'unsupervised' ? null : `Write to \`${path}\`?`,
  execute: async ({ path, content }, opts) => {
    opts.onToolOutputDelta(`Writing ${content.length} bytes to ${path}…\n`)
    await fs.writeFile(path, content)
    return { ok: true }
  },
})

Approval policies

writeFile.withApproval(policy, modeName) returns a copy with the policy baked in. The runtime calls this before instantiating the tool for a given turn.

| Policy | Behavior | | --- | --- | | 'always_allow' | Run without prompting. | | 'ask' | If requestApproval(input, ctx) returns a string, throw ApprovalRequiredError carrying that description. The runtime catches this and surfaces the call as a tool-call event with approvalStatus: 'pending_approval'. If requestApproval returns null (or is absent), use the default Run ${name}? description, or auto-run if there is no requestApproval. | | 'blocked' | Currently equivalent to throwing in the runtime layer; this package only carries the type. |

import { ApprovalRequiredError } from '@looops/protocol/tools'

try {
  await tool.makeTool(ctx).execute(input, callOptions)
} catch (err) {
  if (err instanceof ApprovalRequiredError) {
    await sessionStore.appendPendingApproval(err)
  } else {
    throw err
  }
}

Stream handlers

The agent loop calls into a StreamHandlers object as the model produces tokens, calls tools, and emits reasoning. Implementers persist these into a session store.

import type { StreamHandlers } from '@looops/protocol/streaming'

const handlers: StreamHandlers = {
  onAssistantMessage:       async ({ generationId }) => { /* … */ },
  onAssistantMessageDelta:  async ({ generationId, delta }) => { /* … */ },
  onReasoningStart:         async ({ generationId }) => { /* … */ },
  onReasoningDelta:         async ({ generationId, delta }) => { /* … */ },
  onToolCall:               async (info) => { /* … */ },
  onToolResult:             async (info) => { /* … */ },
  onToolNotFound:           async (info) => { /* … */ },
  onToolError:              async (info) => { /* … */ },
  onToolOutputDelta:        async (info) => { /* … */ },
  onToolApprovalRequired:   async (info) => { /* … */ },
}

The exact runtime that calls these handlers will live in @looops/agent (forthcoming).

Stability

0.0.x. Shapes may shift while the surrounding packages are still being built. Once @looops/agent and @looops/sessions ship a stable release, the protocol will be pinned to 1.0 and follow semver strictly.

License

MIT