@astropods/adapter-core

中文文档
0.15.1发布于 2026/11/04Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 5302

Framework-agnostic bridge between TypeScript agents and the Astropods messaging service.

原始使用文档
文档目录15 节
  1. Installation
  2. Usage
  3. API
  4. AgentAdapter
  5. StreamHooks
  6. attachFile(input)
  7. @astropods/adapter-core/files
  8. createTraceparent(input)
  9. StreamOptions
  10. serve(adapter, options?)
  11. MessagingBridge
  12. HTTP outbound instrumentation
  13. Connections
  14. Sandboxes
  15. Limits

@astropods/adapter-core

Framework-agnostic bridge between TypeScript agents and the Astropods messaging service.

Installation

bun add @astropods/adapter-core
# or
npm install @astropods/adapter-core

Usage

If you're using a supported framework, use the pre-built adapter package instead (e.g. @astropods/adapter-mastra). Use this package directly to connect a custom or unsupported framework.

Implement the AgentAdapter interface, then call serve():

import { serve } from "@astropods/adapter-core";
import type { AgentAdapter } from "@astropods/adapter-core";

const adapter: AgentAdapter = {
  name: "My Agent",
  async stream(prompt, hooks, options) {
    try {
      hooks.onChunk("Hello!");
      hooks.onFinish();
    } catch (err) {
      hooks.onError(err as Error);
    }
  },
  getConfig() {
    return { systemPrompt: "You are a helpful assistant.", tools: [] };
  },
};

serve(adapter);

serve() blocks until SIGINT or SIGTERM. Under ast dev, GRPC_SERVER_ADDR is injected automatically.

API

AgentAdapter

MemberDescription
name: stringDisplay name used in logs and registration
stream(prompt, hooks, options): Promise<void>Stream a response, invoking hooks as the agent progresses
streamAudio?(audio, hooks, options): Promise<void>Optional — handle voice input
getConfig(): AgentConfigReturn { systemPrompt, tools } for playground display
onFeedback?(event): void | Promise<void>Optional — receive inbound platform feedback (thumbs up/down, comments, button clicks)

StreamHooks

Call these inside stream() as the agent produces output:

MethodWhen to call
onChunk(text)Each text token or fragment from the LLM
onStatusUpdate({ status })Agent state change — valid values: THINKING, SEARCHING, GENERATING, PROCESSING, ANALYZING, CUSTOM
onFinish()Response complete — call exactly once per request
onError(error)Error occurred — call instead of onFinish
onTraceContext({ traceparent })Optional — once per turn, to supply the W3C trace context of the assistant turn. The bridge stamps it on every response it emits for the turn, so a consumer can correlate any response back to its trace. Ignored if traceparent is empty.
onFile({ name, mimeType?, size? })A file the agent wrote to its files dir, delivered with the reply. Tool code calls attachFile instead.

attachFile(input)

Writes a file to the agent's files dir (AGENT_FILES_DIR) and attaches it to the reply. Web chat shows it as a download chip, and Slack uploads it into the thread. Call it from a tool while the agent handles a message, in any framework adapter. It throws outside one.

import { attachFile } from "@astropods/adapter-core";

await attachFile({ name: "account-plan.csv", data: csv, mimeType: "text/csv" });

name is one path segment. Omit data to attach a file that is already in the files dir. It returns the file's name. Every user's files share that dir, so with data a name already there gets a number: summary.md becomes summary-2.md. Return files through attachFile rather than writing into AGENT_FILES_DIR directly, because a direct write can replace another thread's file. A file over the deployment's file size limit (FILES_MAX_MB, 100 MiB when unset, 0 for no limit) throws, so the agent knows before it replies.

@astropods/adapter-core/files

Framework-neutral functions for reading the files a user attaches. They take the path from StreamOptions.attachments. Every thread's and user's files share AGENT_FILES_DIR, so the functions open only the files the current conversation received as attachments or wrote with attachFile, and they throw outside a turn. The bridge saves that list, up to 100 files for each conversation, in AGENT_FILES_DIR/.conversations, so it lasts through a restart. A file deleted and replaced at the same path is not on the list. The Mastra and AI SDK adapters wrap them as tools in fileTools().

FunctionReturns
inspectFile(path)The file's kind (pdf, docx, xlsx, zip, text, binary), its size, its text length, and for a table each sheet's columns and exact row count. The kind is sniffed from the bytes, because staged uploads have no extension. A PDF with no text layer, such as a scan, has scanned: true.
readFilePage(path, offset?)One PAGE_CHARS (20,000-character) page of a PDF, DOCX, or text file, with nextOffset (null on the last page) and totalChars. It throws for a PDF with no text layer, which needs OCR.
readTable(path, { sheet?, offset?, limit? })One page of a CSV or XLSX table: the header as columns, then limit data rows (PAGE_ROWS, 100, by default) starting at offset, with nextOffset (null on the last page). CSV cells are strings and XLSX cells keep their types. A CSV is streamed, so a page late in a large file does not load the whole file.
createFile(name, content, mimeType?)Writes a text file with attachFile and returns { attached, path }: the name the user sees and the path the read functions open in a later turn.
attachmentNote(attachments, images?)The line an adapter appends to the turn's text that names each attached file and its path.

The PDF, DOCX, XLSX, and CSV parsers load on first use, so an agent that never calls these functions does not load them.

Analyzing tables

These functions deliver a file's contents and do no analysis. To count, total, group, or join rows across a large table, give the agent a tool that runs a query engine on the attachment's path, such as DuckDB (read_csv_auto(path)) or pandas in a code interpreter. Pass the path through resolveAgentFile, which rejects any file the conversation did not receive or create. If the model writes the query, limit what the engine can read, because DuckDB and pandas can open any path on the machine.

createTraceparent(input)

Formats a native trace/span ID pair into a W3C traceparent string to hand to onTraceContext. Returns "" for invalid or all-zero IDs, so it's safe to pass raw OpenTelemetry span context. traceFlags accepts a number or hex string and defaults to 01 (sampled).

import { createTraceparent } from "@astropods/adapter-core";

const { traceId, spanId, traceFlags } = span.spanContext();
hooks.onTraceContext?.({ traceparent: createTraceparent({ traceId, spanId, traceFlags }) });

StreamOptions

Per-request context passed to stream():

FieldDescription
conversationIdStable ID for the conversation thread
userIdID of the user who sent the message
userName?string | undefined: the sender's name on the source platform, such as their full name in Slack. undefined when the platform sent none.
platformContext?PlatformContext | undefined — platform-specific fields (channel, thread, workspace, event kind). undefined for messages from non-platform sources (playground, direct gRPC).
history?HistoryInput | undefined: the turns before this message, set only when they differ from what the agent last answered. See Edited messages.

Edited messages

In the web chat a user can edit a message they sent, or switch to another version of the conversation. The next message then carries history: the turns before it on the version the user is on, oldest first. It also arrives after a turn the agent did not finish answering. The turns are text only; a user turn that carried only files reads as a line naming them. Replace what you store for conversationId with history.messages, then handle the prompt. An empty messages means there are no earlier turns. isComplete is false when older turns were left out to bound the size.

Declare that you do this with supportsHistory: true in getConfig(). The chat offers editing only for agents that set it. The packaged adapters set it and reset their framework's memory for you.

getConfig() {
  return { systemPrompt: "", tools: [], supportsHistory: true };
}

Using platformContext

PlatformContext exposes the source-event fields adapters often need to branch on — the channel and thread to reply into, the workspace, the bot's own user ID, and an eventKind enum that distinguishes a DM from an @-mention from a thread reply without having to inspect message content.

import type { AgentAdapter, PlatformContext } from "@astropods/adapter-core";

const adapter: AgentAdapter = {
  name: "My Agent",
  async stream(prompt, hooks, { platformContext }) {
    if (platformContext?.eventKind === "EVENT_KIND_APP_MENTION") {
      hooks.onChunk(`You @-mentioned me in ${platformContext.channelName ?? platformContext.channelId}.`);
    } else {
      hooks.onChunk("Hello!");
    }
    hooks.onFinish();
  },
  getConfig() {
    return { systemPrompt: "", tools: [] };
  },
};

Always null-check first — platformContext is undefined for messages from the playground or direct gRPC clients. See PlatformContext for the full field list.

serve(adapter, options?)

Connects the adapter to the messaging service and blocks until shutdown.

import { serve } from "@astropods/adapter-core";

// Override the gRPC address (default: GRPC_SERVER_ADDR env var or localhost:9090)
serve(adapter, { serverAddress: "astro-messaging:9090" });

MessagingBridge

serve() is a thin wrapper around MessagingBridge. Use it directly if you need lifecycle control:

import { MessagingBridge } from "@astropods/adapter-core";

const bridge = new MessagingBridge(adapter);
await bridge.start();

HTTP outbound instrumentation

@astropods/adapter-core/instrument patches globalThis.fetch so every outbound HTTP request becomes an OpenTelemetry CLIENT span. Spans are sent to ${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces. When that env var is unset, the import is a no-op.

Import it once, before any code that issues fetch — typically the first line of your entry point:

import "@astropods/adapter-core/instrument";
// ...the rest of your imports

Or call it explicitly:

import { instrumentHttp } from "@astropods/adapter-core";

instrumentHttp();

Both forms are idempotent and share a tracer provider with other Astropods adapters in the same process.

Connections

A connection is a third-party account, such as GitHub, that the chatting user lets the agent use as them. The agent declares it under connections in astropods.yml, the user allows it in chat, and the agent fetches a token per turn.

import { ConnectionClient, ConnectionError } from "@astropods/adapter-core/connections";

const connections = new ConnectionClient();

const { accessToken } = await connections.getToken("github", options.userId);

new ConnectionClient() reads ASTRO_AUTHZ_TOKEN from the environment and takes the server URL from that token. Pass the turn's userId: the server returns a token only for a user who allowed this agent and has messaged it in web chat in the last 15 minutes.

A token with an expiry is reused until a minute before it expires. A token with none is fetched again on every call, so a revoke takes effect at once.

A refusal throws ConnectionError with a code:

codeTell the user
not_consentedAllow the connection in chat
not_activeSend a message in web chat
not_connectedConnect the provider on their personal account
needs_reauthorizationReconnect the provider
unavailableTry again later

Sandboxes

A sandbox is an isolated machine an agent creates to run commands and hold files for one conversation. It sleeps when the conversation goes quiet and bills for the compute it uses.

import { SandboxClient } from "@astropods/adapter-core/sandbox";

const sandboxes = new SandboxClient();

const result = await sandboxes.exec(conversationId, {
  command: ["/bin/sh", "-c", "npm test"],
});
console.log(result.exitCode, result.stdout);

new SandboxClient() reads ASTRO_AUTHZ_TOKEN from the environment and takes the server URL from that token, so nothing has to be configured.

Call exec on every turn. It attaches on the first call for a name and reuses the sandbox after, and the server settles two concurrent first calls onto one sandbox rather than two. Key by conversation or thread id: two conversations sharing one sandbox share its files.

MethodDoes
exec(name, request)Attaches if needed, then runs a command
attach(name, class?)Returns a handle without running anything
get(name) / list()Records, with no credentials in them
stop(name)Stops compute early. Optional; an idle sandbox sleeps on its own
delete(name)Drops the sandbox and its files

A non-zero exit code and a timed-out command are both results, not thrown errors: check exitCode and timedOut. Thrown errors mean the request itself failed. SandboxNotEnabledError means the account has not turned sandboxes on, in account or organization settings.

The endpoint and the two credentials a sandbox needs are not part of this surface. The client refreshes them, and re-attaches once when a sandbox has been replaced, so an agent never handles either.

Limits

Output is buffered until a command exits and capped at 1 MiB per stream, and there is no way to stream it, poll a background process or signal one. A sandbox lives at most 8 hours; after that the next exec gets a fresh one with an empty workspace. Files do not survive that.

版本与发布记录

@astropods/adapter-core 共发布 42 个版本,首次发布于 2026/02/25;过去 12 个月发布了 42 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.15.1latest2026/11/043 天前
0.15.1-next.4next2026/11/043 天前
0.15.02026/11/034 天前
0.14.1-next.112026/11/034 天前
0.14.1-next.102026/11/025 天前
0.14.02026/10/1325 天前

关于 @astropods/adapter-core 的常见问题

@astropods/adapter-core 是什么?

Framework-agnostic bridge between TypeScript agents and the Astropods messaging service.

如何安装 @astropods/adapter-core?

在项目目录运行 npm install @astropods/adapter-core,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@astropods/messaging(latest)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@astropods/adapter-core 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@astropods/adapter-core 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@astropods/adapter-core 可以商用吗?

@astropods/adapter-core 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@astropods/adapter-core 的下载量有多少?

根据 npm 官方下载统计,@astropods/adapter-core 在 2026/10/07 – 2026/11/05 这 30 天内被下载 5,302 次,日均约 177 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@astropods/adapter-core 还在维护吗?

最新版本 0.15.1 发布于 2026/11/04(3 天前);至今共发布 42 个版本,其中过去 12 个月发布了 42 个;首次发布于 2026/02/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。