diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml new file mode 100644 index 00000000..acfa4aed --- /dev/null +++ b/.github/workflows/docs-pages.yml @@ -0,0 +1,88 @@ +name: Docs + +on: + push: + branches: + - main + paths: + - 'website/**' + - '.github/workflows/docs-pages.yml' + pull_request: + branches: + - main + paths: + - 'website/**' + - '.github/workflows/docs-pages.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build documentation + runs-on: ubuntu-latest + permissions: + contents: read + pages: read + defaults: + run: + working-directory: website + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: website/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Check formatting + run: npm run format:check + + - name: Lint and check language parity + run: npm run lint + + - name: Configure GitHub Pages + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + uses: actions/configure-pages@v5 + + - name: Build website + run: npm run build + env: + DOCS_BASE: /Code/ + DOCS_ORIGIN: https://a3s-lab.github.io + + - name: Check built routes and assets + run: npm run check:site + + - name: Upload GitHub Pages artifact + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v4 + with: + path: website/doc_build + + deploy: + name: Deploy documentation + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 00000000..23a4bc92 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,14 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ +doc_build/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea diff --git a/website/.prettierignore b/website/.prettierignore new file mode 100644 index 00000000..ac66857c --- /dev/null +++ b/website/.prettierignore @@ -0,0 +1,4 @@ +# Lock files +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/website/.prettierrc b/website/.prettierrc new file mode 100644 index 00000000..544138be --- /dev/null +++ b/website/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/website/AGENTS.md b/website/AGENTS.md new file mode 100644 index 00000000..02eecc1b --- /dev/null +++ b/website/AGENTS.md @@ -0,0 +1,13 @@ +## Tools + +### Prettier + +- Run `npm run format` to format your code + +## Docs + +- Rslint: https://rslint.rs/llms.txt + +### Rslint + +- Run `npm run lint` to lint your code diff --git a/website/README.md b/website/README.md new file mode 100644 index 00000000..0cf12abb --- /dev/null +++ b/website/README.md @@ -0,0 +1,26 @@ +# A3S Code website + +The official multilingual website and documentation for +[A3S Code](https://github.com/A3S-Lab/Code), built with Rspress. + +## Local development + +```bash +npm ci +npm run dev +``` + +The production site is served from `/Code/`. Override `DOCS_BASE` and +`DOCS_ORIGIN` only when previewing another deployment target. + +## Checks + +```bash +npm run format:check +npm run lint +npm run build +``` + +The published documentation currently contains only the active `v6` line. +When a new major line ships, add a version snapshot under `docs/` and +then list it in `multiVersion.versions` in `rspress.config.ts`. diff --git a/website/docs/public/a3s-code-mark.svg b/website/docs/public/a3s-code-mark.svg new file mode 100644 index 00000000..3d414830 --- /dev/null +++ b/website/docs/public/a3s-code-mark.svg @@ -0,0 +1,16 @@ + + A3S Code + + + + + + + + + + + + + + diff --git a/website/docs/public/favicon.svg b/website/docs/public/favicon.svg new file mode 100644 index 00000000..7c539c38 --- /dev/null +++ b/website/docs/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/website/docs/public/social-card.svg b/website/docs/public/social-card.svg new file mode 100644 index 00000000..b3f12909 --- /dev/null +++ b/website/docs/public/social-card.svg @@ -0,0 +1,68 @@ + + A3S Code — governed coding-agent runtime + A3S Code keeps tools, policy, events, and durable evidence explicit. + + + + + + + + + + + + + + + + + + + + + + A3S Code + + + + + Build governed + coding agents. + + + Tools, policy, events, and durable evidence + stay behind explicit runtime contracts. + + + + + + + + PRODUCT SURFACES + + + + AGENT SESSION + + + + GOVERNANCE + + + + EVENTS + EVIDENCE + + + + + + ASYNC RUST RUNTIME + TERMINAL + RUST + NODE.JS + PYTHON + MIT + + diff --git a/website/docs/v6/en/_meta.json b/website/docs/v6/en/_meta.json new file mode 100644 index 00000000..a8792bcf --- /dev/null +++ b/website/docs/v6/en/_meta.json @@ -0,0 +1,12 @@ +[ + { + "type": "dir", + "name": "guide", + "label": "Documentation" + }, + { + "type": "dir", + "name": "api", + "label": "API" + } +] diff --git a/website/docs/v6/en/_nav.json b/website/docs/v6/en/_nav.json new file mode 100644 index 00000000..4ec4f145 --- /dev/null +++ b/website/docs/v6/en/_nav.json @@ -0,0 +1,42 @@ +[ + { + "text": "Docs", + "link": "/guide/", + "activeMatch": "^/guide/" + }, + { + "text": "Examples", + "link": "/guide/examples/", + "activeMatch": "^/guide/examples/" + }, + { + "text": "API", + "link": "/api/", + "activeMatch": "^/api/" + }, + { + "text": "Resources", + "items": [ + { + "text": "GitHub", + "link": "https://github.com/A3S-Lab/Code" + }, + { + "text": "Changelog", + "link": "https://github.com/A3S-Lab/Code/blob/main/CHANGELOG.md" + }, + { + "text": "Rust API", + "link": "https://docs.rs/a3s-code-core" + }, + { + "text": "npm", + "link": "https://www.npmjs.com/package/@a3s-lab/code" + }, + { + "text": "PyPI", + "link": "https://pypi.org/project/a3s-code/" + } + ] + } +] diff --git a/website/docs/v6/en/api/index.mdx b/website/docs/v6/en/api/index.mdx new file mode 100644 index 00000000..5bf8a80c --- /dev/null +++ b/website/docs/v6/en/api/index.mdx @@ -0,0 +1,44 @@ +--- +title: APIs and packages +description: Entry points for the A3S Code Rust, Node.js, and Python APIs and their shared runtime contract. +--- + +# APIs and packages + +The four A3S Code product surfaces share one runtime kernel while serving +different integration needs. Treat each registry and +[GitHub Releases](https://github.com/A3S-Lab/Code/releases) as the source of +truth for package versions and release status. + +| Surface | Package or command | API entry | Use it for | +| -------- | ------------------ | -------------------------------------------------- | --------------------------------------------- | +| Terminal | `a3s code` | [A3S CLI](https://github.com/A3S-Lab/CLI) | Run the interactive coding product | +| Rust | `a3s-code-core` | [docs.rs](https://docs.rs/a3s-code-core) | Complete runtime API and extension traits | +| Node.js | `@a3s-lab/code` | [npm](https://www.npmjs.com/package/@a3s-lab/code) | Native N-API bindings and async event streams | +| Python | `a3s-code` | [PyPI](https://pypi.org/project/a3s-code/) | Synchronous and asynchronous PyO3 APIs | + +## Install + +```bash +# Rust +cargo add a3s-code-core + +# Node.js +npm install @a3s-lab/code + +# Python +python -m pip install a3s-code +``` + +## Cross-language contract + +Versioned events, durable snapshots, and explicit lifecycle methods form the +cross-language product boundary. Start with the +[API contract](/guide/api-contract), then continue to +[sessions](/guide/sessions), [tools](/guide/tools), +[workspace backends](/guide/workspace-backends), and +[persistence](/guide/persistence). + +Replaceable host extension boundaries include `LlmClient`, `ContextProvider`, +`MemoryStore`, `SessionStore`, workspace services, tools, permissions, +confirmations, hooks, security providers, MCP transports, and graph stores. diff --git a/website/docs/v6/en/guide/_meta.json b/website/docs/v6/en/guide/_meta.json new file mode 100644 index 00000000..aa7ef900 --- /dev/null +++ b/website/docs/v6/en/guide/_meta.json @@ -0,0 +1,72 @@ +[ + "index", + "tui", + { + "type": "dir", + "name": "examples", + "label": "Examples", + "collapsible": true, + "collapsed": true + }, + { + "type": "section-header", + "label": "Filesystem first" + }, + "filesystem-first", + "convention-over-configuration", + "agents-md", + "filesystem-instructions", + "filesystem-config", + "agent-dir", + "filesystem-agents", + "filesystem-skills", + "filesystem-tools", + "filesystem-schedules", + { + "type": "section-header", + "label": "Runtime core" + }, + "api-contract", + "sessions", + "commands", + "tools", + "verification", + "tasks", + "teams", + "orchestration", + "skills", + { + "type": "section-header", + "label": "Governance engineering" + }, + "security", + "hooks", + "limits", + "isolation", + { + "type": "section-header", + "label": "Infrastructure" + }, + "architecture", + "lane-queue", + "workspace-backends", + "multi-machine", + "cluster-extension-points", + { + "type": "section-header", + "label": "Extensions" + }, + "providers", + "mcp", + "context", + "memory", + "persistence", + "telemetry", + { + "type": "dir", + "name": "rfcs", + "label": "RFCs", + "collapsible": true, + "collapsed": true + } +] diff --git a/website/docs/v6/en/guide/agent-dir.mdx b/website/docs/v6/en/guide/agent-dir.mdx new file mode 100644 index 00000000..da6d9878 --- /dev/null +++ b/website/docs/v6/en/guide/agent-dir.mdx @@ -0,0 +1,106 @@ +--- +title: 'Agent Directory' +description: 'AgentDir structure, loading map, serve daemon, and durability boundaries' +--- + +# Agent Directory + +AgentDir is a single directory that defines a durable agent by convention. One folder contains the main agent's role, runtime config, private skills, directory-scoped tools, and recurring schedules. `AgentDir::load` reads the directory and synthesizes existing A3S Code config objects; it does not introduce a new runtime or a new prompt system. + +The deliberate design choice: `instructions.md` is injected as a prompt **slot**, not as a system-prompt override. The harness keeps `BOUNDARIES`, response-format contracts, tool visibility, the safety gate, and verification authoritative. A scheduled run is always a full harness turn through `AgentSession::send`, never a raw model call. + +This feature is Rust-side and gated behind the `serve` Cargo feature. Library-only embedders that never enable `serve` pay no extra cost. + +## Structure + +```text +my-agent/ +├── instructions.md (required) Role and guidelines. Injected as a prompt slot. +├── agent.acl (optional) Model, providers, queue, and CodeConfig. +├── skills/ (optional) Private *.md skills. +├── schedules/ (optional) Cron jobs: frontmatter + body prompt. +└── tools/ (optional) Tool specs: kind: mcp or kind: script. +``` + +Only `instructions.md` is required. Everything else is optional; missing directories simply contribute no capability. + +`AgentDir::load` maps paths onto existing objects: + +| Path | Becomes | Notes | +| ----------------- | ------------------------ | ------------------------------------------------------------------------------------------------------ | +| `instructions.md` | `SystemPromptSlots.role` | Main-agent role slot; see [instructions.md](/guide/filesystem-instructions). | +| `agent.acl` | `CodeConfig` | Model, provider, queue, and directory discovery; see [agent.acl](/guide/filesystem-config). | +| `skills/` | `skill_dirs` | AgentDir-private skills; see [skills/ Skill Directory](/guide/filesystem-skills). | +| `schedules/*.md` | `Vec` | One recurring turn per file; see [schedules/ Schedule Directory](/guide/filesystem-schedules). | +| `tools/*.md` | `Vec` | One `kind: mcp` or `kind: script` tool per file; see [tools/ Tool Directory](/guide/filesystem-tools). | + +## Serve Daemon + +`serve_agent_dir` loads schedules into independent sessions and runs their cron loops until a cancellation token fires. Every fire routes the schedule prompt through `AgentSession::send`. + +```rust +use a3s_code_core::config::AgentDir; +use a3s_code_core::serve::serve_agent_dir; +use a3s_code_core::Agent; +use tokio_util::sync::CancellationToken; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let agent_dir = AgentDir::load("./my-agent")?; + let agent = Agent::from_config(agent_dir.config.clone()).await?; + let cancel = CancellationToken::new(); + + serve_agent_dir(&agent, &agent_dir, "./workspace", None, cancel).await?; + Ok(()) +} +``` + +Enable the feature in `Cargo.toml`: + +```toml +[dependencies] +a3s-code-core = { version = "...", features = ["serve"] } +``` + +## Overriding Session Options + +The fourth argument to `serve_agent_dir` is an optional `SessionOptions` that merges into every schedule session. Use it to pin the model, LLM client, session store, or prompt slots. The daemon always assigns each schedule a stable `session_id` of `schedule:`; any `session_id` in the extra options is intentionally ignored so schedules do not collide in the same store. If `prompt_slots` is omitted, the AgentDir `instructions.md` slot is used. + +```rust +use a3s_code_core::SessionOptions; + +let extra = SessionOptions::new(); // set model, session_store, etc. +serve_agent_dir(&agent, &agent_dir, "./workspace", Some(extra), cancel).await?; +``` + +Graceful cancellation lets in-flight turns finish the current loop iteration. An AgentDir with no enabled schedules returns immediately. + +## Durability + +By default every daemon boot starts schedule sessions fresh. Pass a `SessionStore` in `SessionOptions` to resume existing `schedule:` sessions from stored conversation history. + +```rust +use a3s_code_core::SessionOptions; + +let extra = SessionOptions::new().with_file_session_store("./sessions"); +serve_agent_dir(&agent, &agent_dir, "./workspace", Some(extra), cancel).await?; +``` + +Recovery restores history only. The current `instructions.md`, `skills/`, and `tools/` are re-applied on every boot, so directory edits take effect after restart even for resumed sessions. + +## Status + +| Area | State | +| ----------------------------------------- | --------------------------------------------------------------------- | +| `instructions.md`, `agent.acl`, `skills/` | Loaded and used. | +| `schedules/` + serve daemon | Implemented (`serve` feature). | +| Rehydrate on boot | Implemented with `SessionStore`. | +| `tools/` (`kind: mcp`) | Implemented; declarative MCP servers register into schedule sessions. | +| `tools/` (`kind: script`) | Implemented; sandboxed QuickJS tool over the `program` path. | + +## Notes + +- `instructions.md` is a role slot, so harness boundaries, response contracts, and verification remain authoritative. +- AgentDir is the main-agent directory. It is different from `agent_dirs` / `registerAgentDir`, which scan [agents/ Role Directory](/guide/filesystem-agents) for worker definitions. +- Keep secrets in environment variables or host secret systems, not in the directory. +- `tools/` is installed by the serve daemon per schedule session. Normal interactive sessions should use direct tools, MCP, or SDK registration. diff --git a/website/docs/v6/en/guide/agents-md.mdx b/website/docs/v6/en/guide/agents-md.mdx new file mode 100644 index 00000000..796dd78c --- /dev/null +++ b/website/docs/v6/en/guide/agents-md.mdx @@ -0,0 +1,59 @@ +--- +title: 'AGENTS.md' +description: 'Workspace-level project instructions as versioned context' +--- + +# AGENTS.md + +`AGENTS.md` is the workspace-level project instruction file. It lets project rules live with the repo, so every prompt does not need to repeat build commands, code style, safety boundaries, and release flow. + +In the filesystem-first architecture, `AGENTS.md` explains how this project works. AgentDir `instructions.md` explains who one durable agent is. Both enter context composition, but neither can override harness permission gates, response contracts, or verification requirements. + +```md +# Project Instructions + +- Use `cargo test -p a3s-code-core` for core changes. +- Never commit real secrets from `.a3s/config.acl`. +- Prefer `rg` for search. +- Release checks must include package metadata, CI, and provider verification. +``` + +## Good Content + +- Build, test, lint, format, and release commands. +- Directory responsibilities, module boundaries, and code style. +- Safety rules for secrets, permissions, external side effects, and data handling. +- Verification policy for different kinds of changes. +- Project-specific terminology and common workflows. + +## Bad Content + +- Secrets, tokens, private credentials, or personal machine paths. +- Worker-agent role descriptions; put those in `.a3s/agents/`. +- Durable-agent identity and default output style; put those in `instructions.md`. +- Reusable checklists; put those in `.a3s/skills/`. + +## Nested Rules + +The current runtime auto-loads only the workspace-root `AGENTS.md`. Nested +`AGENTS.md` files are still useful as repository documentation or host-level +conventions, but they are not traversed automatically by A3S Code unless the +host reads them and injects their content. + +Use nested `AGENTS.md` files only when subdirectories truly differ, such as a +desktop app, API package, or SDK with a different toolchain. Do not copy the +root file just to repeat it; duplicated rules make it harder for long-running +agents to identify the current source of truth. + +## Relationship To Other Conventions + +```text +repo/ +├── AGENTS.md # project-level durable instructions +├── agent.acl # runtime config +└── .a3s/ + ├── agents/ # worker/subagent definitions + └── skills/ # reusable skills +``` + +`AGENTS.md` gives the agent project facts and working boundaries. `agent.acl` tells the runtime how to connect models and directories. `agents/` and `skills/` provide discoverable roles and reusable process. diff --git a/website/docs/v6/en/guide/api-contract.mdx b/website/docs/v6/en/guide/api-contract.mdx new file mode 100644 index 00000000..f1eee557 --- /dev/null +++ b/website/docs/v6/en/guide/api-contract.mdx @@ -0,0 +1,784 @@ +--- +title: 'API Contract' +description: 'SDK mechanisms covered by the local integration check' +--- + +# API Contract + +This page documents only the A3S Code Node SDK behavior covered by +`crates/code/scripts/docs_api_contract_smoke.mjs`. The script starts a temporary +OpenAI-compatible local test server, creates real SDK sessions, calls the native +binding, and asserts the returned values. It does not require a docs build. + +Run the contract from `crates/code`: + +```bash +node scripts/docs_api_contract_smoke.mjs +``` + +## Agent + +Verified entry points: + +```ts +const agent = await Agent.create(aclSource); +await agent.refreshMcpTools(); + +const session = agent.session(workspace, options); +const named = agent.sessionForAgent(workspace, 'explore', [], options); +``` + +`Agent.create()` accepts ACL source text or an `.acl` file path. JSON config is +not part of the verified contract. The integration check covers both +`apiKey`/`baseUrl` and `api_key`/`base_url` provider aliases against the local +OpenAI-compatible test server. +`sessionForAgent()` was verified with the built-in `explore` agent. + +The Node factory names remain synchronous at the JavaScript surface, but their +native implementation delegates resource resolution to the core async session +construction path. Rust embedders should use `SessionBuilder::build().await` or +the async factories; the synchronous Rust compatibility factory requires an +explicit pre-initialized memory store. + +The integration check also covers the ACL field that creates a file-backed +session store: + +```text +storage_backend = "file" +sessions_dir = "/tmp/a3s-doc-stores/acl-storage" +``` + +This contract does not cover `storage_url`. It is not the local file-session +persistence path; use `sessions_dir` in ACL or pass `sessionStore` in SDK +options. + +## Session Options + +The integration check covers this option shape: + +```ts +const session = agent.session(workspace, { + model: 'openai/docs-alt', + builtinSkills: true, + planningMode: 'disabled', + memoryStore: new FileMemoryStore(memoryDir), + sessionStore: new FileSessionStore(sessionDir), + sessionId: 'docs-contract', + autoSave: true, + securityProvider: new DefaultSecurityProvider(), + skillDirs: [path.join(workspace, 'skills')], + inlineSkills: [ + { + name: 'strict-release-review', + kind: 'instruction', + content: 'Always separate blockers from nice-to-have improvements.', + }, + ], + maxToolRounds: 24, + maxParseRetries: 3, + toolTimeoutMs: 120000, + circuitBreakerThreshold: 4, + autoCompact: true, + autoCompactThreshold: 0.75, + continuationEnabled: true, + maxContinuationTurns: 3, + maxExecutionTimeMs: 300000, // 5 minutes timeout + confirmationPolicy: { + enabled: true, + defaultTimeoutMs: 60000, + timeoutAction: 'reject', + }, +}); +``` + +`model` is a per-session override. The check verifies that a session created +with `model: 'openai/docs-alt'` sends `docs-alt` to the local provider. + +Basic session accessors are verified: + +```ts +console.log(session.sessionId); +console.log(session.workspace); +console.log(session.initWarning); +console.log(session.history()); +console.log(session.cancel()); +``` + +`workspace` is returned as the SDK's canonical workspace path. + +`planningMode` accepts the explicit modes documented elsewhere: `'auto'`, +`'enabled'`, and `'disabled'`. + +The check also verifies that this `permissionPolicy` shape is accepted at +session creation: + +```ts +agent.session(workspace, { + permissionPolicy: { + deny: ['write(**/.env*)', 'bash(rm -rf*)'], + ask: ['bash(git push*)', 'bash(npm publish*)'], + allow: ['read(*)', 'grep(*)', 'glob(*)', 'bash(npm run build*)'], + defaultDecision: 'ask', + enabled: true, + }, +}); +``` + +Prompt slot options are strings: + +```ts +agent.session(workspace, { + role: 'release-readiness reviewer', + guidelines: + 'Find blockers before improvements. Require command evidence for done claims.', + responseStyle: 'concise, findings first', + goalTracking: true, +}); +``` + +## Result Shape + +`session.send()` returns `AgentResult` fields on the result object itself: + +```ts +const result = await session.send('Return a short answer'); + +console.log(result.text); +console.log(result.toolCallsCount); +console.log(result.promptTokens); +console.log(result.completionTokens); +console.log(result.totalTokens); +console.log(result.verificationStatus); +console.log(result.pendingVerificationCount); +console.log(result.failedVerificationCount); +console.log(result.verificationReportCount); +console.log(result.verificationSummaryJson); +console.log(result.verificationSummaryText); +``` + +Trace events and verification reports are session APIs, not fields on +`AgentResult`. + +## Streaming + +`session.stream()` returns `EventStream`. The verified consumption contract is +`.next()`: + +```ts +const stream = await session.stream('Stream one sentence'); + +while (true) { + const { value: event, done } = await stream.next(); + if (done) break; + if (!event) continue; + if (event.text) process.stdout.write(event.text); +} +``` + +The smoke check starts another `send()` immediately after this loop. Exhaustion +therefore verifies both event delivery and release of the stream's single-flight +admission lease; no retry delay is required. + +Each SDK event is an envelope-v1 projection with `version === 1`, an open +`type` string, a complete `payload`, and optional `metadata`. Consumers must +retain a default branch for future event types. Node exposes `payloadJson` and +`metadataJson` string views; Python exposes `payload_json` and `metadata_json` +and retains `event_type` as an alias for `type`. + +Do not depend on `for await` unless your installed SDK version has separately +validated async iteration support. + +## Direct Tools + +> Full guide: [Tools](/guide/tools). + +The integration check covers these host-driven direct calls: + +```ts +await session.readFile('README.md'); +await session.glob('src/*.rs'); +await session.grep('PermissionPolicy'); +await session.bash('printf docs-bash'); +await session.tool('read', { file_path: 'README.md' }); +await session.git('status'); +await session.git('diff'); +await session.git( + 'log', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + 5, +); +await session.tool('search_skills', { query: 'release blockers', limit: 5 }); + +session.toolNames(); +session.toolDefinitions(); +session.registerAgentDir(path.join(workspace, 'agents')); +``` + +The verified `toolNames()` set includes `read`, `write`, `edit`, `patch`, +`grep`, `glob`, `ls`, `bash`, `task`, `parallel_task`, `search_skills`, `Skill`, +`program`, `git`, `batch`, `web_fetch`, and `web_search`. + +Direct host calls are privileged. Gate them in the host application before +exposing them to end users. + +## AGENTS.md + +The script writes an `AGENTS.md` file in the workspace and asserts that its +instruction token appears in the local provider request body: + +```md +# Project Instructions + +Always mention docs-contract-agents-md-token when asked for project instructions. +``` + +Keep project instructions operational and free of secrets. + +## Programmatic Tool Calling + +`session.program()` runs bounded JavaScript in the embedded QuickJS runtime: + +```ts +const result = await session.program({ + source: ` + export default async function run(ctx, inputs) { + const text = await ctx.readFile('README.md'); + const hits = await ctx.grep(inputs.q, { glob: '*.md' }); + return { summary: 'ok', hasHits: text.includes(inputs.q) && hits.includes(inputs.q) }; + } + `, + inputs: { q: 'planningMode' }, + allowedTools: ['read', 'grep'], + limits: { timeoutMs: 30000, maxToolCalls: 12, maxOutputBytes: 65536 }, +}); + +const meta = JSON.parse(result.metadataJson); +console.log(meta.script_result); +console.log(meta.program.tool_calls); +``` + +Verified `ctx` helpers: `readFile`, `read`, `grep`, `glob`, `ls`, `bash`, +`git`, and generic `tool(name, args)`. `allowedTools` limits which registered +tools the script may call. The `program` tool is not included in its own default +tool set. + +## Verification + +> Full guide: [Verification](/guide/verification). + +Verification is session-scoped: + +```ts +const report = await session.verifyCommands('docs api check', [ + { + id: 'echo', + kind: 'command', + description: 'echo works', + command: 'printf verify', + required: true, + }, +]); + +console.log(report.subject); +console.log(session.verificationReports()); +console.log(session.verificationSummary()); +console.log(session.verificationSummaryText()); +console.log(session.verificationPresets()); +console.log(formatVerificationSummary(session.verificationSummary())); +``` + +## Memory + +> Full guide: [Memory](/guide/memory). + +Node memory was verified with `FileMemoryStore`: + +```ts +const session = agent.session(workspace, { + memoryStore: new FileMemoryStore(memoryDir), +}); + +console.log(session.hasMemory); +await session.rememberSuccess('docs memory success', ['grep'], 'remembered'); +await session.rememberFailure('docs memory failure', 'expected failure', [ + 'bash', +]); +await session.memoryRecent(10); +await session.recallSimilar('docs memory', 5); +await session.recallByTags(['grep'], 10); +``` + +The verified recent-memory method is `memoryRecent()`. `recallRecent()` is not +present on the current Node SDK surface. + +## Skills + +> Full guide: [Skills](/guide/skills). + +File-backed and inline skills are verified through `search_skills`: + +```ts +const session = agent.session(workspace, { + skillDirs: [path.join(workspace, 'skills')], + inlineSkills: [ + { + name: 'strict-release-review', + kind: 'instruction', + content: 'Always separate blockers from nice-to-have improvements.', + }, + ], +}); + +await session.tool('search_skills', { query: 'release blockers', limit: 5 }); +await session.tool('search_skills', { + query: 'strict release review', + limit: 5, +}); +``` + +The skill-file check uses Markdown with YAML frontmatter and the +`allowed-tools` key. + +## Side Questions + +> Full guide: [Sessions](/guide/sessions). + +The current SDK surface has no dedicated ephemeral-question helper. Use +explicit history when the call must not mutate the session transcript: + +```ts +const snapshot = session.history(); +const side = await session.send('What is this test?', snapshot); + +console.log(side.text); +console.log(session.history().length === snapshot.length); +``` + +## Runs And Cancellation + +> Full guide: [Sessions](/guide/sessions). + +Each `send()` or `stream()` records replayable run state: + +```ts +const runs = await session.runs(); +const latest = runs.at(-1); + +if (latest) { + console.log(await session.runSnapshot(latest.id)); + console.log(await session.runEvents(latest.id)); +} + +const current = await session.currentRun(); +if (current?.id && current.status === 'running') { + await session.cancelRun(current.id); +} + +console.log(session.traceEvents()); +``` + +`currentRun()` is for the current operation. When idle, it may return +`null` or a retained snapshot depending on the preceding control flow. Use +`runs()` for completed history. + +Transcript-affecting operations are single-flight per session. An overlapping +send, stream, attachment call, slash command, or `resumeRun` fails immediately +with `SessionBusy`; it is not queued. A stream retains admission until its +producer has stopped, even when its public handle is dropped. + +## Persistence + +> Full guide: [Persistence](/guide/persistence) and [Sessions](/guide/sessions). + +File-backed session persistence was verified with stable `sessionId`, +`autoSave`, explicit `save()`, and `resumeSession()`: + +```ts +const session = agent.session(workspace, { + sessionStore: new FileSessionStore(sessionDir), + sessionId: 'docs-contract', + autoSave: true, +}); + +await session.save(); + +const resumed = agent.resumeSession('docs-contract', { + sessionStore: new FileSessionStore(sessionDir), +}); +console.log(resumed.history()); +``` + +Core persistence commits the conversation and its artifacts, traces, run +records, verification reports, and subagent task snapshots together as one +versioned `SessionSnapshotV1`. File and memory stores publish that aggregate +atomically. Legacy fragmented records remain loadable, while custom stores must +implement aggregate save explicitly. + +Use `session.close()` when a Node process should release session-scoped +background resources promptly. `close()` is the full graceful-stop entry +point: it flips `session.isClosed` to `true` (further `send` / `stream` +calls reject with a `Session closed` error), fires the session-level +`CancellationToken` so every in-flight run, delegated subagent task, and +HITL confirmation aborts. Subsequent `close()` calls are no-ops. + +For control-plane callers that only know the session ID, the same cleanup +is reachable from the agent: + +```ts +await agent.listSessions(); // ['session-a', 'session-b'] +await agent.closeSession('session-a'); // true if it was open +await agent.close(); // close every live session + disconnect global MCP +``` + +After `agent.close()`, subsequent `agent.session(...)` and +`agent.resumeSession(...)` calls reject with a `Session closed` error. +Idempotent. Use this in process-shutdown handlers to guarantee no +session-scoped workers outlive the agent. + +## Delegation + +> Full guide: [Tasks](/guide/tasks) and [Orchestration](/guide/orchestration). + +The direct helpers for the core delegation tools were verified: + +```ts +await session.task({ + agent: 'general', + description: 'docs delegated check', + prompt: 'Return a short response.', + maxSteps: 1, +}); + +await session.tasks([ + { + agent: 'general', + description: 'one', + prompt: 'Return one response.', + maxSteps: 1, + }, + { + agent: 'general', + description: 'two', + prompt: 'Return another response.', + maxSteps: 1, + }, +]); +``` + +They return `ToolResult` values from `task` and `parallel_task`. + +## Hooks + +> Full guide: [Hooks](/guide/hooks). + +The verified hook management surface is: + +```ts +session.registerHook( + 'docs-observer', + 'pre_tool_use', + { tool: 'bash' }, + { priority: 1, timeoutMs: 1000 }, + () => ({ action: 'continue' }), +); + +console.log(session.hookCount()); +session.unregisterHook('docs-observer'); +``` + +Validate the specific event path you depend on before using hook behavior as a +production enforcement gate. + +## Slash Commands + +> Full guide: [Commands](/guide/commands). + +Custom slash commands are invoked through `session.send()`: + +```ts +session.registerCommand( + 'docs_status', + 'Return docs command status', + (args, ctx) => { + return `status args=${args}; session=${ctx.sessionId}; workspace=${ctx.workspace}`; + }, +); + +console.log(session.listCommands()); +const result = await session.send('/docs_status check'); +console.log(result.text); +``` + +## Lane Queue + +> Full guide: [Lane Queue](/guide/lane-queue). + +Queue infrastructure is opt-in: + +```ts +const queued = agent.session(workspace, { + queueConfig: { enableDlq: true, enableMetrics: true }, +}); + +console.log(queued.hasQueue()); +await queued.setLaneHandler('execute', { mode: 'external', timeoutMs: 1000 }); +await queued.pendingExternalTasks(); +await queued.completeExternalTask('missing', { + success: true, + result: { ok: true }, +}); +await queued.queueStats(); +await queued.queueMetrics(); +await queued.deadLetters(); +``` + +Ordinary sessions are queue-free unless `queueConfig` is provided. + +## MCP + +> Full guide: [MCP](/guide/mcp). Idle disconnect: [Cluster Extension Points](/guide/cluster-extension-points). + +The integration check covers a live stdio MCP server: + +```ts +const count = await session.addMcp({ + name: 'echo', + transport: { + type: 'stdio', + command: process.execPath, + args: ['tools/mcp_echo_server.mjs', 'example-value'], + }, + timeoutMs: 30000, +}); + +console.log(count); +console.log(await session.mcpStatus()); +console.log( + session.toolNames().filter((name) => name.startsWith('mcp__echo__')), +); + +await session.tool('mcp__echo__echo', { message: 'docs mcp ok' }); +await session.removeMcpServer('echo'); +``` + +Tools from the server are named `mcp____`. +`addMcpServer(...)` and `addMcpServerConfig(...)` remain compatibility aliases; +new examples use the compact object-shaped `addMcp(...)` API. + +Live add/remove operations target a private manager owned by this session. +Agent-global and host-supplied managers are inherited read-only capability +sources, so a session cannot mutate a sibling or global MCP configuration. + +## Cluster-grade extension points + +> Full guide: [Cluster Extension Points](/guide/cluster-extension-points) (identity labels, budget guard, cluster events, deterministic IDs/replay, loop checkpoints, retention caps). + +These contracts let a cluster control plane wire +multi-tenancy, cost governance, and crash-tolerant runs **without +forking the framework**. The framework defines decision points and +emits structured events; the host supplies the policy implementations. + +### Identity labels + +Four optional `SessionOptions` slots are propagated through hooks, +traces, and `SessionData` but never interpreted by the framework: + +```ts +const session = agent.session(workspace, { + tenantId: 'tenant-example', + principal: 'principal-example', + agentTemplateId: 'agent-template-example', + correlationId: 'trace-example', + sessionStore: new FileSessionStore('./sessions'), +}); +session.tenantId; // -> 'tenant-example' +session.correlationId; // -> 'trace-example' +``` + +`apply_persisted_runtime_options` restores them on resume; caller- +supplied options on resume take precedence so you can relabel. + +### Budget / cost guard + +`BudgetGuard` is consulted before every run-owned provider call (and after a +successful response for usage accounting) and before every governed tool call, +including nested and trusted host-direct calls. `Deny` returns +`CodeError::BudgetExhausted { resource, reason }`; `SoftLimit` emits +an `AgentEvent::BudgetThresholdHit { kind: "soft", .. }` and proceeds. + +Wire from Rust today (Node/Python wrappers will follow): + +```rust +let guard: Arc = /* host-supplied impl */; +let opts = SessionOptions::new().with_budget_guard(guard); +``` + +### Cluster event vocabulary + +`AgentEvent` (non-exhaustive) carries platform-level events the host +emits via `HookExecutor`: + +- `BudgetThresholdHit { resource, kind, consumed, limit, message? }` +- `PassivationRequested { reason, deadline_ms? }` +- `PeerInvocation { from_session_id, from_tenant_id?, correlation_id? }` + +In-session hooks subscribe to these to react uniformly regardless of +how the host's transport delivers them. + +### Deterministic IDs / time + +`HostEnv { id_generator, clock }` replaces the default +`uuid::Uuid::new_v4()` + wall-clock pair. Replay tooling configures +`SequentialIdGenerator` + `FixedClock` to recreate a run bit-identical +on another node. + +### Loop checkpoints + run resumption + +When a `SessionStore` is configured, the agent loop persists a +`LoopCheckpoint` after each completed tool round, keyed by `run_id`. +Any node holding the same store can rehydrate a run from its last +boundary: + +```ts +// Node — host detected node A died mid-run; on node B: +const session = agentB.session(workspace, { + sessionStore: new FileSessionStore('./sessions'), + sessionId: 'session-from-node-a', +}); +const result = await session.resumeRun('run-id-from-node-a'); +``` + +```python +# Python equivalent +opts = SessionOptions() +opts.session_store = FileSessionStore('./sessions') +opts.session_id = 'session-from-node-a' +session = agent_b.session(workspace, opts) +result = session.resume_run('run-id-from-node-a') +``` + +A **new** run id is allocated for the resumed work — the framework +does not pretend the old run continues. Two distinguishable error +paths: + +- `"resume_run requires a session_store"` — host should fall back to + a fresh session. +- `"no loop checkpoint found for run 'X'"` — host can retry later + (race against checkpoint write) or treat the run as lost. + +Boundary policy: checkpoints are taken **only between tool rounds**, +never mid-tool. If a process dies mid-tool the work of that round is +lost; the LLM re-deliberates from the previous boundary. This trades +retry cost for correctness — re-executing a non-idempotent tool +across the boundary is worse than re-asking the LLM. + +### Retention caps for long-running sessions + +`SessionRetentionLimits` lets the host cap the four in-memory stores +that grow with session age: the run records, per-run event buffers, +trace events, and **terminal** subagent task snapshots. Each cap is +optional (`None` keeps the unbounded default — fine for short +sessions, a memory leak for hour- or day-long ones). Eviction is +strict FIFO; running subagent tasks are never dropped. + +```rust +use a3s_code_core::retention::SessionRetentionLimits; + +let limits = SessionRetentionLimits::new() + .with_max_runs(100) + .with_max_events_per_run(5_000) + .with_max_trace_events(10_000) + .with_max_terminal_subagent_tasks(1_000); + +let opts = SessionOptions::new().with_retention_limits(limits); +``` + +The host should pick caps from the same observability budget that caps the rest +of its in-memory state (Prometheus carries history anyway). Node exposes this +as `retentionLimits`; Python exposes it as `opts.retention_limits`. + +### MCP idle disconnect + +`Agent::disconnect_idle_mcp(threshold_ms)` walks the connected MCP +servers and drops any whose last activity is older than +`now - threshold_ms`. The server's _registered config_ stays — a +later tool call will reconnect on demand. Returns the names of +disconnected servers. + +```ts +// Node — periodically reap quiet MCP subprocesses. +setInterval(async () => { + const dropped = await agent.disconnectIdleMcp(5 * 60 * 1000); // 5 min + if (dropped.length) { + console.log('reaped idle MCP servers:', dropped); + } +}, 60_000); +``` + +```python +# Python — same shape. +dropped = agent.disconnect_idle_mcp(5 * 60 * 1000) +``` + +Activity is stamped on `connect` and on every successful `call_tool`. +Hosts that route tool traffic through a side channel can call +`McpManager.touch(name)` to manually keep a server warm. + +### BudgetGuard SDK bridges + +Both SDKs accept the same decision shape: + +| Return | Effect | +| -------------------------------------------------------- | -------------------------------------------------------------------- | +| `None` / `null` / `{decision:'allow'}` | proceed silently | +| `{decision:'soft', resource, consumed, limit, message?}` | emit `BudgetThresholdHit('soft')` event, proceed | +| `{decision:'deny', resource, reason}` | abort the call, throw `RuntimeError("Budget exhausted...")` (Python) | +| | / reject with `"Budget exhausted..."` (Node) | + +Missing methods on the guard object are treated as a permissive +default (Allow / no-op). Callback errors fall back to Allow — a +misbehaving guard cannot halt a live session. + +```python +# Python — attach via SessionOptions before agent.session(...) +class MyGuard: + def check_before_llm(self, session_id, estimated_tokens): + return {"decision": "deny", "resource": "llm_tokens", "reason": "cap"} + def record_after_llm(self, session_id, usage): + track(session_id, usage["total_tokens"]) + +opts = SessionOptions() +opts.budget_guard = MyGuard() +session = agent.session(workspace, opts) +``` + +```ts +// Node — attach via session.setBudgetGuard after construction. +// JsFunction values can't live inside the value-typed SessionOptions, +// so the guard is installed on the Session itself; takes effect on +// the next send/stream. +session.setBudgetGuard({ + checkBeforeLlm: (ctx) => { + if (overBudget(ctx.sessionId)) { + return { decision: 'deny', resource: 'llm_tokens', reason: 'cap' }; + } + return null; + }, + recordAfterLlm: (ctx) => { + track(ctx.sessionId, ctx.usage.totalTokens); + }, +}); +``` + +Node callbacks receive a single `ctx` object and must not throw; wrap handler +logic in `try/catch` and return an explicit decision. Hung or unreadable +`check*` callbacks fail closed as `deny`. Python callbacks use positional +arguments and catch exceptions as Allow. + +Pass `null` to `setBudgetGuard` (Node) or set `opts.budget_guard = +None` and re-create the session (Python) to clear. diff --git a/website/docs/v6/en/guide/architecture.mdx b/website/docs/v6/en/guide/architecture.mdx new file mode 100644 index 00000000..e00b386b --- /dev/null +++ b/website/docs/v6/en/guide/architecture.mdx @@ -0,0 +1,197 @@ +--- +title: 'Architecture' +description: 'Session construction, run scope, governed invocation, events, and persistence' +--- + +# Architecture + +A3S Code separates configuration resolution, per-run execution, stable wire +contracts, and persistence. The TUI and SDKs use the same runtime kernel; they do +not get separate execution paths. + +```text +CodeConfig + SessionOptions + -> validate and resolve async resources + -> ResolvedSessionConfig + -> AgentSession + -> single-flight run admission + -> InvocationContext + ├─ LLM invoker -> provider calls + ├─ tool invoker -> model, nested, delegated, and host-direct calls + └─ events -> EventEnvelopeV1 -> Rust / Node / Python consumers + -> SessionSnapshotV1 -> atomic store generation +``` + +## Async-First Session Construction + +`SessionOptions` is a public patch, not partially initialized runtime state. The +async construction path merges it with `CodeConfig`, validates conflicting +options, initializes async resources, and produces one internal +`ResolvedSessionConfig`. Session assembly consumes that resolved value instead +of resolving the same choice in multiple layers. + +For Rust hosts, prefer: + +```rust +let session = agent + .session_builder("/repo") + .options(options) + .build() + .await?; +``` + +`Agent::session_async`, `resume_session_async`, `session_for_agent_async`, and +`session_for_worker_async` use the same construction kernel. File-backed memory +and session stores, queues, trajectory recording, and session MCP discovery are +initialized asynchronously and return typed `SessionConfiguration` or +`SessionInitialization` errors with the failing resource. + +The synchronous `Agent::session` method exists only for compatibility with +hosts that explicitly supply a pre-initialized memory store and have already +initialized every other resource. It never starts or blocks a Tokio runtime. +Configuration that requires async work fails with +`CodeError::AsyncSessionBuildRequired`; the runtime does not silently replace +the requested resource with an easier backend. A manager passed through +`SessionOptions::with_mcp` always requires async capability discovery. The sync +path can only inherit agent-global MCP tools already cached at agent startup. + +## Single-Flight Conversation State + +Conversation history is serialized by admission, not by optimistic locking. +Only one transcript-affecting operation may be active on a session. This covers +`send`, `stream`, both attachment variants, slash commands, and `resume_run`. +An overlap fails immediately with `CodeError::SessionBusy` before history is +read or a command is dispatched. + +A stream retains its admission lease until the stream runtime finishes. Dropping +or aborting the public handle does not briefly admit a second operation while +the original producer is still writing events or history. Direct host tool +calls are control-plane operations and do not claim the conversation lease. + +## Invocation Context + +Each admitted run creates one immutable `InvocationContext` containing: + +- run ID and session ID +- the run cancellation token +- the event sender +- the governance snapshot, including the active budget guard + +That context is the source of truth for provider and tool work. It installs the +same cancellation token and session identity into `ToolContext`, so cancellation +reaches queued tools, nested `batch`/`program` calls, delegated work, planning, +structured-output repair, compaction, and other run-owned helper calls. + +## LLM Invocation Boundary + +Run-owned provider work uses one scoped LLM invoker. It checks budget and +cancellation before each provider call, records usage after each successful +response, proxies streaming completion so terminal usage is recorded, and +combines caller and run cancellation. Normal turns, planning, structured output +and repair, compaction, and memory/helper paths use this boundary instead of +maintaining independent budget logic. + +A hard budget denial is an error and is never converted into an ungoverned +fallback. Soft limits emit a `budget_threshold_hit` event and allow the call to +continue. + +## Tool Invocation Boundary + +The tool invoker is the governance kernel for model-selected, nested, +programmatic, delegated, and direct host calls. For model-owned work it applies +active-skill restrictions, permission policy, pre/post hooks, budget checks, +human confirmation, queue/timeout handling, cancellation, recursive-invocation +protection, and output sanitization. `batch` and `program` receive the scoped +invoker rather than calling a raw registry, so inner calls cannot escape those +checks. + +Direct SDK helpers have an explicit `HostDirectPolicy::TrustedControlPlane` +origin. +The host is the authority that chose the operation, so model-facing permission +and confirmation decisions are skipped. Pre-hooks can still block the call, and +budget, queue/timeout, cancellation, recursion protection, post-hooks, and +output sanitization remain active. Applications must authorize end users before +exposing this privileged path. + +Trust propagation is structural rather than ambient. Only built-in +control-plane orchestrators can convert an explicit host-direct call into the +internal trusted-nested origin for host-selected children. Public +`InvocationRuntime::invoke_tool` always creates an ordinary governed nested +call, even when the extension itself was called directly. Model dispatch also +removes inherited host-direct policy before it creates a `ToolContext`, so a +Skill, Task, or other model sub-run cannot use `batch` or `program` to amplify +its parent's authority. + +## Stable Event Protocol + +`AgentEvent` is the internal Rust runtime enum. The cross-language contract is +the lossless envelope: + +```json +{ + "version": 1, + "type": "tool_end", + "payload": {}, + "metadata": {} +} +``` + +The v1 event catalog and the exhaustive Rust mapping share one source of truth, +so adding a runtime variant without a canonical wire name is a compile-time +failure. Node and Python consume the centralized projection for convenience +fields such as `text`, `toolName`, and `tool_name`. `type` remains an open +string: unknown future types preserve their full payload and metadata rather +than collapsing to an `unknown` sentinel. + +## Atomic Session Persistence + +`SessionSnapshotV1` is one versioned persistence generation. It contains the +conversation plus artifacts, trace events, run records, verification reports, +and delegated-task snapshots. `session.save()` materializes that aggregate and +calls `SessionStore::save_snapshot` once. + +The file store writes one complete JSON envelope through a synced temporary +file and atomic replacement. The memory store replaces one aggregate entry +under one lock. Both advertise atomic snapshot capability. Historical bare +`SessionData` and fragment directories remain loadable for migration, but new +saves do not publish fragmented generations. A custom store must implement +aggregate save explicitly; the default method returns an error rather than +acknowledging a partial or no-op save. + +## MCP Ownership And Isolation + +MCP managers have explicit ownership: + +1. The agent-global manager owns servers loaded from global configuration. +2. A host-supplied manager in session options is an inherited, read-only + capability source. +3. Every session owns a new private live manager. + +Capabilities are assembled in that order, so a session-local tool can shadow an +inherited tool only inside that session. Live `add_mcp_server` and +`remove_mcp_server` calls mutate only the private manager; removing a local +shadow reveals the inherited capability again. Sibling sessions cannot mutate +one another, and delegated children inherit the ordered manager sources needed +to call the same tools without transferring ownership. + +A local stdio transport also owns the complete server process lifetime. It +starts the server as a Unix process-group leader, drains protocol output and +stderr separately, and terminates the group on close or drop. Outstanding +requests fail when either pipe closes instead of waiting for an unrelated +request deadline. + +## Programmatic Tool Calling + +The `program` tool runs JavaScript in an embedded QuickJS VM with a controlled +`ctx` object. The VM has no direct filesystem, network, subprocess, or +environment access. Its useful capabilities are tool calls routed back through +the scoped tool invoker. Use ordinary model tool calls when the next action +requires judgment; use a bounded program when the action sequence is already +known and only its result needs model interpretation. + +## Extension Points + +Use typed session options, skills, agent definitions, hooks, MCP servers, +memory/session stores, security providers, queue configuration, and workspace +services to adapt the runtime. Prefer explicit policy and replayable evidence +over alternate execution paths. diff --git a/website/docs/v6/en/guide/cluster-extension-points.mdx b/website/docs/v6/en/guide/cluster-extension-points.mdx new file mode 100644 index 00000000..ae4e0d03 --- /dev/null +++ b/website/docs/v6/en/guide/cluster-extension-points.mdx @@ -0,0 +1,192 @@ +--- +title: 'Cluster Extension Points' +description: 'The seams a cluster host uses to run long-lived agent sessions across many nodes without forking the framework.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Cluster Extension Points + +A cluster host platform runs long-lived agent sessions across many nodes. The framework does not ship a scheduler or a placement engine. Instead it exposes a small set of seams: it defines the decision points, emits structured events, and lets the host supply the policy. Everything below is something you wire from outside the framework — you never fork it. + +This page is precise about which seams are available from both SDKs (shown with Node.js + Python code) and which are configured in the Rust core today (described in prose when no SDK option exists yet). + +## Identity labels + +Every session can carry four opaque identity labels. The framework never interprets them — it propagates them to hooks, traces, and `SessionData`, and restores them on resume. This is how a host attributes a session to a tenant, a principal, an agent template, and a wider correlation chain. + +Pair identity labels with a `sessionStore` / `session_store` so the labels survive a process restart. On resume, **caller-supplied options win**, so you can relabel a session as you move it between nodes. + + + + +```ts +const session = agent.session('/path/to/project', { + tenantId: 'tenant-example', + principal: 'principal-example', + agentTemplateId: 'agent-template-example', + correlationId: 'trace-example', +}); + +// Getters return string | null +console.log(session.tenantId); // 'tenant-example' +console.log(session.principal); // 'principal-example' +console.log(session.agentTemplateId); // 'agent-template-example' +console.log(session.correlationId); // 'trace-example' +``` + + + + +```python +opts = SessionOptions() +opts.tenant_id = 'tenant-example' +opts.principal = 'principal-example' +opts.agent_template_id = 'agent-template-example' +opts.correlation_id = 'trace-example' +session = agent.session('/path/to/project', opts) + +# Getters are properties, return str | None +print(session.tenant_id) # 'tenant-example' +print(session.principal) # 'principal-example' +print(session.agent_template_id) # 'agent-template-example' +print(session.correlation_id) # 'trace-example' +``` + + + + +## Budget / cost guard + +A budget guard lets the host gate every LLM call against a cost or token budget. The framework calls your guard _before_ each LLM request and _after_ it returns. The guard is policy you own; the framework only enforces the decision you hand back. + + + + +```ts +session.setBudgetGuard({ + checkBeforeLlm(ctx) { + if (overLimit(ctx.sessionId, ctx.estimatedTokens)) { + return { + decision: 'deny', + resource: 'tokens', + reason: 'monthly cap reached', + }; + } + return { decision: 'allow' }; + }, + recordAfterLlm(ctx) { + meter(ctx.sessionId, ctx.usage); + }, +}); + +// Clear the guard +session.setBudgetGuard(null); +``` + +Node callbacks receive a single `ctx` object and **must not throw**. Wrap logic in `try/catch` and return an explicit decision. Hung or unreadable `check*` callbacks fail closed as `deny`. + + + + +```python +class MyGuard: + def check_before_llm(self, session_id, estimated_tokens): + if over_limit(session_id, estimated_tokens): + return {'decision': 'deny', 'resource': 'tokens', 'reason': 'monthly cap reached'} + return {'decision': 'allow'} + + def record_after_llm(self, session_id, usage): + meter(session_id, usage) + +opts = SessionOptions() +opts.budget_guard = MyGuard() +session = agent.session('/path/to/project', opts) + +# To clear: set opts.budget_guard = None and re-create the session. +``` + + + + +The decision shape is identical across both SDKs: + +| Return value | Effect | +| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `None` / `null` / `{ decision: 'allow' }` | Proceed with the LLM call. | +| `{ decision: 'soft', resource, consumed, limit, message? }` | Emits `BudgetThresholdHit` (kind `soft`) and proceeds. | +| `{ decision: 'deny', resource, reason }` | Aborts the LLM call. Python raises `RuntimeError("Budget exhausted...")`; Node rejects with `"Budget exhausted..."`. | + +Robustness is intentional but SDK-specific: a **missing guard method** is treated as the permissive default. Python callback errors fall back to Allow. Node callbacks must not throw; timeouts and malformed `check*` returns fail closed as `deny`. + +## Cluster event vocabulary + +The host emits cluster-level decisions as structured `AgentEvent` variants through its hook executor. In-session hooks subscribe to them uniformly — the same way they observe any other event — so policy authored at the host shows up to the agent's own hooks without special casing. + +The cluster vocabulary is: + +- **`BudgetThresholdHit { resource, kind, consumed, limit, message? }`** — a budget guard returned a `soft` decision (or the host crossed a threshold it tracks itself). `kind` distinguishes soft warnings from harder limits. +- **`PassivationRequested { reason, deadline_ms? }`** — the host is asking the session to reach a safe, persistable state so it can be evicted from this node. `deadline_ms`, when present, is the grace window before forced eviction. +- **`PeerInvocation { from_session_id, from_tenant_id?, correlation_id? }`** — another session invoked this one. The labels let the receiver attribute the call back to its origin tenant and correlation chain. + +These are observed through the same verified hook API your in-session hooks already use — `session.registerHook` in Node, `session.register_hook` in Python (see [Hooks](/guide/hooks)). Treat the three variants above as the documented contract; the host is responsible for emitting them via its hook executor. + +## Deterministic IDs and time (replay) + +A cluster that wants **bit-identical replay** of a run on a different node must remove the two sources of nondeterminism in a normal run: random IDs and the wall clock. The Rust core models both behind a `HostEnv { id_generator, clock }`. The default pairs a UUID generator with the system clock; replay tooling swaps in a `SequentialIdGenerator` and a `FixedClock` so that re-executing the same inputs produces the same IDs and timestamps, and therefore the same output, on any node. + +This is configured in the **Rust core today**. It is not yet exposed on the JS/Python option surface, so there is no Node/Python code for it — SDK wiring may follow. + +## Loop checkpoints and run resumption + +With a `sessionStore` / `session_store` configured, the agent loop persists a checkpoint after **each completed tool round**, keyed by run id. Any node that shares the same store can rehydrate the run and continue it. + + + + +```ts +import { FileSessionStore } from '@a3s-lab/code'; + +const session = agent.session(workspace, { + sessionStore: new FileSessionStore('./.a3s/sessions'), + sessionId: 'session-from-node-a', +}); + +const result = await session.resumeRun('run-id-from-node-a'); +``` + + + + +```python +from a3s_code import FileSessionStore + +opts = SessionOptions() +opts.session_store = FileSessionStore('./.a3s/sessions') +opts.session_id = 'session-from-node-a' +session = agent.session(workspace, opts) + +result = session.resume_run('run-id-from-node-a') +``` + + + + +A **new run id** is allocated for the resumed work — the original run is left intact in the store. Two error paths are worth handling: + +- **`resume_run requires a session_store`** — no store was configured; fall back to a fresh session. +- **`no loop checkpoint found for run 'X'`** — the run never reached its first checkpoint, or it was pruned; retry later or treat the run as lost. + +Because checkpoints are taken only **between tool rounds, never mid-tool**, a resumed run never replays a half-executed tool. See [Persistence](/guide/persistence) for store details. + +## Retention caps for long-running sessions + +A session that runs for hours or days accumulates state in four in-memory stores: run records, per-run event buffers, trace events, and terminal subagent task snapshots. Left unbounded, these grow with session age — fine for short-lived sessions, a real leak for long-lived ones. + +`SessionRetentionLimits` caps each of the four stores. Every cap is optional: `None` means the unbounded default. Eviction is strict **FIFO**, and **running subagent tasks are never dropped** — only terminal (completed/failed) snapshots are evicted. + +Use `retentionLimits` in Node or `opts.retention_limits` in Python. Rust hosts use `SessionOptions::with_retention_limits(...)`. See [Limits](/guide/limits) for field names and examples. + +--- + +**See also:** [Multi-machine](/guide/multi-machine) · [Persistence](/guide/persistence) · [Limits](/guide/limits) · [Hooks](/guide/hooks) diff --git a/website/docs/v6/en/guide/commands.mdx b/website/docs/v6/en/guide/commands.mdx new file mode 100644 index 00000000..0f32aedf --- /dev/null +++ b/website/docs/v6/en/guide/commands.mdx @@ -0,0 +1,70 @@ +--- +title: 'Commands' +description: 'Command surfaces and recommended control flows' +--- + +# Commands + +A3S Code is primarily SDK-driven. Product CLIs and UIs usually map commands to +session APIs rather than depending on a large public command protocol. + +This page describes the SDK command registry. For the built-in commands in the +`a3s code` terminal app, see [A3S Code TUI](/guide/tui). + +The two surfaces are intentionally different: + +| Surface | Owner | Purpose | +| ------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `a3s code` slash commands | CLI/TUI | Terminal controls such as `/model`, `/flow`, `/memory`, `/kb`, `/update`, and `/exit`. | +| SDK command registry | Host application | Product-defined commands registered with `session.registerCommand(...)` and invoked through `session.send("/name args")`. | + +SDK commands execute before the LLM sees the input. A handler receives the raw +argument string plus session metadata, and returns display text. Use commands +for thin control-plane actions; use normal SDK methods for workflows, tools, +verification, and persistence. + +Inside the TUI, `/flow` manages workflow asset files and optional host +integrations. It is not the same surface as `DynamicWorkflowRuntime`, which is +the local A3S Flow-backed runtime used by `ultracode` and `?` DeepResearch +through the model-visible `dynamic_workflow` tool. + +## Recommended Mappings + +| User action | Session API | +| --------------------------------- | ------------------------------------------------------------------------------------- | +| Send a prompt | `session.send(prompt)` | +| Stream a prompt | `session.stream(prompt)` | +| Ask a side question | Create an isolated session, or call `send` / `stream` with explicit isolated history. | +| Run a deterministic tool | `session.tool(name, args)` | +| Read history | `session.history()` | +| Save state | `session.save()` | +| Resume state | `agent.resumeSession(id, options)` | +| Inspect tools | `session.toolNames()` / `session.toolDefinitions()` | +| Verify work | `session.verifyCommands(subject, commands)` | +| Replay run state | `session.runs()` / `session.runEvents(runId)` | +| Inspect or cancel the current run | `session.currentRun()` / `session.cancelRun(runId)` | + +## Building Slash Commands + +If your app exposes slash commands, keep them thin. Register the handler, list +the available commands, and invoke the command through `session.send()`: + +```ts +session.registerCommand( + 'docs_status', + 'Return docs command status', + (args, ctx) => { + return `status args=${args}; session=${ctx.sessionId}; workspace=${ctx.workspace}`; + }, +); + +console.log(session.listCommands()); + +const result = await session.send('/docs_status check'); +console.log(result.text); +``` + +Command handlers should not hide long-running work. If a command needs to run +tools, verify output, fan out to subagents, or schedule recurring automation, +have the handler return a short acknowledgement and run that work through +explicit host code. That keeps cancellation, audit, and permissions visible. diff --git a/website/docs/v6/en/guide/context.mdx b/website/docs/v6/en/guide/context.mdx new file mode 100644 index 00000000..1b592b36 --- /dev/null +++ b/website/docs/v6/en/guide/context.mdx @@ -0,0 +1,59 @@ +--- +title: 'Context' +description: 'Budgeted context assembly for reliable coding agents' +--- + +# Context + +A3S Code treats context as a budgeted resource. The model should see the smallest useful context for the current decision, not every available file, skill, memory, and tool log. + +## Sources + +Context can come from: + +- user prompt and conversation history +- AGENTS.md project instructions +- skills and agent definitions +- memory stores +- file search and direct tool results +- MCP tools and context providers +- delegated task summaries +- trace events + +AGENTS.md injection, skills discovery, memory APIs, direct tool results, +delegated task helpers, and `traceEvents()` are part of the documented Node SDK +surface. Validate MCP context behavior against your own live integrations +before documenting it as product behavior. + +## Assembly + +```text +sources -> ContextItem -> rank -> dedupe -> budget -> render +``` + +Long grep output, logs, and child transcripts should be preserved outside the prompt and summarized into prompt-safe evidence. Use `session.traceEvents()` for compact runtime evidence. + +## Compaction + +Enable automatic compaction for long sessions: + +```ts +const session = agent.session('/repo', { + autoCompact: true, + autoCompactThreshold: 0.75, + maxContextTokens: 128_000, +}); +``` + +When `maxContextTokens` is omitted, Core uses the selected model's declared +context window when available. Before each model request, Core accounts for the +system prompt, conversation, tool calls and results, and exposed tool schemas. +At the configured threshold it bounds oversized tool output, summarizes the +older safe prefix, keeps recent messages, and continues the same task. The +summary participates in later compactions, so long sessions can roll forward +through repeated compression; this does not enlarge the model's physical +single-request context window. A successful `context_compacted` event includes +the cumulative summary so hosts that supply external history can persist the +same compact generation across turns. + +Python exposes the same override as `max_context_tokens`. diff --git a/website/docs/v6/en/guide/convention-over-configuration.mdx b/website/docs/v6/en/guide/convention-over-configuration.mdx new file mode 100644 index 00000000..788e4ec0 --- /dev/null +++ b/website/docs/v6/en/guide/convention-over-configuration.mdx @@ -0,0 +1,233 @@ +--- +title: 'Convention Over Configuration' +description: 'Build durable work units with filesystem-first agents, tools, connections, subagents, schedules, persistence, and HITL' +--- + +# Convention Over Configuration + +A3S Code's convention-over-configuration capability is not a separate runner. +It is a durable-agent composition built from +[filesystem-first](/guide/filesystem-first) conventions, the session +runtime, tool permissions, multi-agent delegation, resumable orchestration, +HITL, run replay, and the serve scheduler. It turns an agent from "a +chat-capable SDK session" into a work unit with a role, tools, collaborators, +state, and takeover points. + +If you are evaluating directory-first agent frameworks, treat A3S Code as a +lower-level and more embeddable runtime. Directory conventions, tools, +connections, child agents, schedules, persistence, and observability are all +present, but they are not tied to one frontend channel or deployment platform. +A host can connect them to managed sessions, open-platform APIs, MCP, +A3S Box, or its own task system. + +## Capability Map + +| Framework concept | A3S Code capability | Notes | +| --------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Agent as a directory | [Agent Directory](/guide/agent-dir) | `instructions.md`, `agent.acl`, `skills/`, `tools/`, and `schedules/` synthesize a runnable agent. | +| Markdown instructions | `instructions.md`, `AGENTS.md`, prompt slots | Instructions are injected as slots, so they cannot override harness boundaries, response contracts, or safety gates. | +| Markdown skills | [Skills](/guide/skills) | File skills, inline skills, and explicit registries share discovery semantics. A3S Code ships no default embedded skills. | +| TypeScript tools | `program`, direct tools, MCP tools, AgentDir script tools | A3S Code focuses on tool registration, permission gates, typed errors, and verification evidence; script tools run in a constrained QuickJS environment. | +| Sandbox | `program` sandbox, permission policy, HITL, A3S Box | A3S Code handles tool permissions and script sandboxing; process, filesystem, and network isolation belong in A3S Box. | +| Channels | Managed sessions, open-platform WebSocket/SSE, host UI | A3S Code does not hard-code channels. Hosts connect streaming events and run replay to any frontend. | +| Connections | MCP, provider config, host-injected credentials | Connections are configured and authorized by the host. Do not put tokens in the agent directory. | +| Subagents | [Tasks](/guide/tasks), [Teams](/guide/teams), `workerAgents` | Supports manual `task`, parallel `parallel_task`, automatic delegation, and dynamic worker agents. | +| Schedules | AgentDir `schedules/` + `serve_agent_dir` | Every schedule has an independent session id and can recover context after restart when a session store is configured. | +| Durable execution | `SessionStore`, run replay, `parallelResumable` | Session history, run events, and resumable workflow checkpoints can be persisted; failed steps retry on resume. | +| Human-in-the-loop | [Security](/guide/security), confirmation inheritance, tool confirmation | High-risk tools can request confirmation. Child runs can auto-approve, fail on Ask, or inherit the parent policy. | +| Evaluations | [Verification](/guide/verification), reports, regression evidence | A3S Code productizes evaluation as verification commands, evidence summaries, run replay, and release gates rather than a separate eval suite. | + +## Minimal Shape + +An interactive team agent can start from a normal repository: + +```text +repo/ +├── agent.acl +├── AGENTS.md +├── .a3s/ +│ ├── agents/ +│ │ ├── release-reviewer.md +│ │ ├── security-reviewer.md +│ │ └── verification-runner.md +│ └── skills/ +│ └── release-readiness.md +└── src/ +``` + +`agent.acl` controls models, providers, parallelism, and automatic delegation: + +```text +default_model = "provider/model-id" +max_parallel_tasks = 4 +auto_parallel = false + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "model-id" { + tool_call = true + limit = { + context = 128000 + output = 4096 + } + } +} + +agent_dirs = ["./.a3s/agents"] + +auto_delegation { + enabled = true + min_confidence = 0.72 + max_tasks = 4 + auto_parallel = false +} +``` + +The host starts a session and wires in skills, agent directories, persistence, and delegation policy: + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +const session = agent.session('/repo', { + skillDirs: ['./.a3s/skills'], + agentDirs: ['./.a3s/agents'], + autoDelegation: { enabled: true, minConfidence: 0.72, maxTasks: 4 }, + maxParallelTasks: 8, + autoParallel: false, + autoSave: true, +}); + +const result = await session.send(` +Prepare a release-readiness check: +1. Ask the exploration role to find high-risk changes. +2. Ask the security role to inspect permissions, secrets, and external side effects. +3. Ask the verification role to list required regression commands. +4. Merge everything into a blocker-first report. +`); + +console.log(result.text); +console.log(result.verificationSummaryText); +``` + +## Agent Directory Shape + +Use a filesystem-first agent when you need long-running behavior, schedules, or directory-scoped tools: + +```text +release-agent/ +├── instructions.md +├── agent.acl +├── skills/ +│ └── release-readiness.md +├── schedules/ +│ └── daily.md +└── tools/ + ├── github.md + └── search-auth.md +``` + +`instructions.md` is the role slot: + +```md +You are a release-readiness agent for this repository. +Always separate blockers from follow-up work. +Never invent versions or CI status. Read evidence from the workspace. +``` + +`schedules/daily.md` is a recurring turn: + +```md +--- +cron: '0 9 * * *' +name: daily-release-check +enabled: true +--- + +Summarize merged changes since the last run, inspect release risks, +and report only blockers plus required verification. +``` + +`serve_agent_dir` creates an independent session for every schedule. With a `SessionStore`, restart reloads the current directory config and tools while recovering prior context from the store. + +## Tools And Connections + +A3S Code exposes tools in three layers: + +| Layer | Usage | Good for | +| ------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Built-in and direct tools | `session.tool(...)`, file, shell, git, `generate_object` | The host knows exactly what to run or needs deterministic calls. | +| MCP | MCP servers | GitHub, Linear, internal systems, remote host tools, or cross-process capabilities. | +| AgentDir script tools | `tools/*.md` + QuickJS `program` | Wrapping constrained scripts as model-visible tools. | + +Tools are not unlimited just because a file exists. Visibility, permission gates, HITL, allow-lists, and sandboxing are controlled by the harness or the AgentDir loader. Give high-privilege tools only to trusted directories, and inject secrets through environment variables and host connections. + +## Subagents And Teams + +A convention-first `subagents/` concept maps to three A3S Code entry points: + +| Entry point | Who decides the lanes | Use it when | +| ------------------------------------------------------------------------------------ | --------------------- | ----------------------------------------------------------- | +| `task` / `parallel_task` | Parent agent | The model should decide whether and how to delegate. | +| `session.task(...)` / `session.tasks(...)` | Host code | The host knows the lanes but still wants agent execution. | +| `session.parallel(...)` / `session.pipeline(...)` / `session.parallelResumable(...)` | Host code | The workflow must be reproducible, testable, and resumable. | + +Automatic delegation depends on agent descriptions and confidence scoring. It fits "the user describes the goal and the runtime chooses specialists." Fixed release flows, batch reviews, and migrations are better expressed explicitly with orchestration. + +## Observability And Takeover + +Long-running agents must be observable and interruptible. A3S Code's core observation surfaces are: + +- `stream()` for incremental events. +- `runs()`, `runSnapshot()`, and `runEvents()` for active and historical runs. +- `toolNames()` / `toolDefinitions()` for the visible tool surface. +- `activeTools()` for currently running tool-call snapshots. +- `cancelRun(runId)` to interrupt an active turn. +- `traceEvents()` for compaction, delegation, tools, and verification evidence. +- `verificationSummaryText` for release or review gates. + +A host platform can translate those events into WebSocket/SSE streams, audit +records, debug panels, and workflow node states. + +## Relationship To A3S Box + +A3S Code owns the agent loop, tools, delegation, state, and verification. A3S Box owns stronger runtime isolation: MicroVMs, OCI workloads, networking, and TEE. If "the model may run shell, but the process must be isolated" is a requirement, run the tool execution through A3S Box or expose isolated capabilities through MCP. + +Typical composition: + +```text +A3S Code session + -> permission policy and HITL + -> MCP tool adapter + -> A3S Box isolated workload + -> typed result and verification evidence +``` + +## When To Use It + +Use this shape for: + +- Long-running engineering agents for release checks, dependency upgrades, and repository maintenance. +- Work that naturally splits into explore / review / verify / implement roles. +- Automatic delegation with retained permission gates, auditability, and verification evidence. +- Cron-based reports with recoverable context. +- Agents that need to connect to managed workflows, hosted sessions, or external collaboration channels. + +Avoid it when: + +- A single deterministic API call is enough; use a tool contract instead. +- The task requires full GUI automation but has no structured tool interface; provide MCP or browser tools first, then let A3S Code schedule them. +- The task needs strong OS-level isolation but only has a local shell enabled; integrate A3S Box. + +## Reading Order + +1. [Filesystem-First](/guide/filesystem-first) +2. [Agent Directory](/guide/agent-dir) +3. [agents/ Role Directory](/guide/filesystem-agents) +4. [tools/ Tool Directory](/guide/filesystem-tools) +5. [schedules/ Schedule Directory](/guide/filesystem-schedules) +6. [Tasks](/guide/tasks) +7. [Teams](/guide/teams) diff --git a/website/docs/v6/en/guide/examples/_meta.json b/website/docs/v6/en/guide/examples/_meta.json new file mode 100644 index 00000000..6814f12c --- /dev/null +++ b/website/docs/v6/en/guide/examples/_meta.json @@ -0,0 +1,22 @@ +[ + "index", + "quick-start", + "streaming", + "direct-tools", + "structured-output", + "batch", + "planning", + "orchestration", + "external-tasks", + "lane-queue", + "memory", + "auto-compact", + "prompt-slots", + "ripgrep-context", + "model-switching", + "skills", + "skill-tool", + "hooks", + "security", + "git-worktree" +] diff --git a/website/docs/v6/en/guide/examples/auto-compact.mdx b/website/docs/v6/en/guide/examples/auto-compact.mdx new file mode 100644 index 00000000..e92a2bc7 --- /dev/null +++ b/website/docs/v6/en/guide/examples/auto-compact.mdx @@ -0,0 +1,83 @@ +--- +title: 'Auto Compact' +description: 'Let the runtime keep long sessions inside the context budget automatically' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Auto Compact + +A3S Code can keep long conversations inside the model's context budget for you. +Enable `autoCompact` and the runtime watches context usage; once it crosses +`autoCompactThreshold`, older turns are compacted into a running summary so the +agent stays coherent across many steps without you managing tokens by hand. +Continuation handles the other direction: when a single response is truncated by +length, the runtime automatically continues generating to assemble the full reply. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +const session = agent.session('/repo', { + // Compact older turns once context fills past the threshold. + autoCompact: true, + autoCompactThreshold: 0.75, + // Auto-continue a single response that the model truncates by length. + continuationEnabled: true, + maxContinuationTurns: 3, +}); + +// Run a long, multi-step task. The runtime compacts older turns as needed; +// you never touch the token math. +for (let i = 0; i < 50; i++) { + await session.send(`Step ${i}: continue refactoring the parser`); +} + +// Inspect what the session is currently carrying. +console.log('history turns:', session.history().length); +console.log('recent memory:', await session.memoryRecent(5)); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") + +opts = SessionOptions() +# Compact older turns once context fills past the threshold. +opts.auto_compact = True +opts.auto_compact_threshold = 0.75 +# Auto-continue a single response that the model truncates by length. +opts.continuation_enabled = True +opts.max_continuation_turns = 3 +session = agent.session("/repo", opts) + +# Run a long, multi-step task. The runtime compacts older turns as needed; +# you never touch the token math. +for i in range(50): + session.send(f"Step {i}: continue refactoring the parser") + +# Inspect what the session is currently carrying. +print("history turns:", len(session.history())) +print("recent memory:", session.memory_recent(5)) + +session.close() +``` + + + + +Use auto-compaction for long sessions where you want the runtime to manage context +pressure for you. `autoCompactThreshold` / `auto_compact_threshold` is a fraction of +the context window (0.0–1.0, default 0.8) at which compaction kicks in; lower it to +compact earlier. Inspect the live session with `history()` (synchronous) and +`memoryRecent` / `memory_recent`. diff --git a/website/docs/v6/en/guide/examples/batch.mdx b/website/docs/v6/en/guide/examples/batch.mdx new file mode 100644 index 00000000..39e12ea8 --- /dev/null +++ b/website/docs/v6/en/guide/examples/batch.mdx @@ -0,0 +1,88 @@ +--- +title: 'Batch' +description: 'Compose deterministic SDK helpers for grouped, model-free operations' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Batch + +There is no `batch()` method in the SDK. When a host workflow or agent turn has a +clear list of independent, deterministic steps, compose them directly from the +session's deterministic helpers (`readFile`, `grep`, `glob`, `ls`, `git`) and +aggregate the results yourself. That keeps the "batch" fully under your control: +no model calls, explicit ordering, and no destructive operations unless you +invoke them. + +The example below reads package metadata, the changelog, and the release script, +then reports version mismatches without editing any files. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/path/to/project'); + +// Node helpers are async — group independent reads with Promise.all. +const [pkg, changelog, releaseScript] = await Promise.all([ + session.readFile('package.json'), + session.readFile('CHANGELOG.md'), + session.readFile('scripts/release.sh'), +]); + +const pkgVersion = JSON.parse(pkg).version; +const mismatches = []; +if (!changelog.includes(pkgVersion)) + mismatches.push(`CHANGELOG.md is missing ${pkgVersion}`); +if (!releaseScript.includes(pkgVersion)) + mismatches.push(`release.sh is missing ${pkgVersion}`); + +console.log( + mismatches.length ? mismatches.join('\n') : 'All files agree on the version.', +); + +session.close(); +``` + + + + +```python +import json +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session("/path/to/project", SessionOptions()) + +# Python helpers are synchronous — call them in sequence, no await. +pkg = session.read_file("package.json") +changelog = session.read_file("CHANGELOG.md") +release_script = session.read_file("scripts/release.sh") + +pkg_version = json.loads(pkg)["version"] +mismatches = [] +if pkg_version not in changelog: + mismatches.append(f"CHANGELOG.md is missing {pkg_version}") +if pkg_version not in release_script: + mismatches.append(f"release.sh is missing {pkg_version}") + +print("\n".join(mismatches) if mismatches else "All files agree on the version.") + +session.close() +``` + + + + +Do not mix destructive operations into these grouped reads. Any destructive host +workflow (writes, `git` commits, `bash`) should sit behind explicit application +confirmation or your automation gates, constrained with `permissionPolicy` / +`permission_policy` so no unexpected step runs silently. + +When the steps are **not** independent — each one depends on the previous result +and you want an agent to drive them — use +[`session.pipeline(...)`](/guide/examples/orchestration) instead, which runs +the work in stages where each stage receives the prior stage's output. diff --git a/website/docs/v6/en/guide/examples/direct-tools.mdx b/website/docs/v6/en/guide/examples/direct-tools.mdx new file mode 100644 index 00000000..7f82e861 --- /dev/null +++ b/website/docs/v6/en/guide/examples/direct-tools.mdx @@ -0,0 +1,157 @@ +--- +title: 'Direct Tools' +description: 'Run deterministic host tools without spending an LLM turn' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Direct Tools + +`session.tool(name, args)` (and the typed helpers like `glob`, `grep`, `readFile`) +run a host tool directly, with no model call in the loop. Use them for tests, +migrations, and host-driven workflows where you want deterministic results +instead of an agent turn. + +Direct calls are host control-plane calls. Apply your own product authorization +before invoking them; `permissionPolicy` gates model-selected tool calls inside +an agent turn, not whether your application code is allowed to call an SDK +helper. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.'); + +const allowedDirectTools = new Set(['glob', 'grep', 'read', 'generate_object']); +function assertDirectToolAllowed(name: string) { + if (!allowedDirectTools.has(name)) { + throw new Error(`direct tool not allowed here: ${name}`); + } +} + +// Glob: list files by pattern +assertDirectToolAllowed('glob'); +const files = await session.glob('**/*.ts'); +console.log(`glob found ${files.length} TypeScript files`); + +// Grep: search file contents +assertDirectToolAllowed('grep'); +const matches = await session.grep('Agent.create'); +const matchCount = matches.split('\n').filter(Boolean).length; +console.log(`grep found ${matchCount} matching lines`); + +// Read a file +assertDirectToolAllowed('read'); +const readme = await session.readFile('README.md'); +console.log(`README is ${readme.length} bytes`); + +// Direct tool call by name +assertDirectToolAllowed('read'); +const raw = await session.tool('read', { file_path: 'package.json' }); +console.log(`package.json via tool(): ${raw.output.length} bytes`); + +// Inspect available tool schemas +const schemas = session.toolDefinitions(); +console.log(`session exposes ${schemas.length} tools`); + +// Structured output: generate a schema-validated JSON object +assertDirectToolAllowed('generate_object'); +const structured = await session.tool('generate_object', { + schema: { + type: 'object', + required: ['count', 'language'], + properties: { + count: { type: 'integer' }, + language: { type: 'string' }, + }, + }, + prompt: 'How many TypeScript files are in this project?', + schema_name: 'file_stats', +}); +if (structured.exitCode !== 0) { + throw new Error(structured.output); +} +console.log('structured output:', JSON.parse(structured.output).object); + +session.close(); +``` + + + + +```python +import json + +from a3s_code import Agent + +agent = Agent.create("agent.acl") +session = agent.session('.') + +ALLOWED_DIRECT_TOOLS = {'glob', 'grep', 'read', 'generate_object'} + + +def assert_direct_tool_allowed(name: str) -> None: + if name not in ALLOWED_DIRECT_TOOLS: + raise RuntimeError(f'direct tool not allowed here: {name}') + +# Glob: list files by pattern +assert_direct_tool_allowed('glob') +files = session.glob('**/*.py') +print(f'glob found {len(files)} Python files') + +# Grep: search file contents +assert_direct_tool_allowed('grep') +matches = session.grep('Agent.create') +match_count = len([line for line in matches.splitlines() if line]) +print(f'grep found {match_count} matching lines') + +# Read a file +assert_direct_tool_allowed('read') +readme = session.read_file('README.md') +print(f'README is {len(readme)} bytes') + +# Direct tool call by name +assert_direct_tool_allowed('read') +raw = session.tool('read', {'file_path': 'pyproject.toml'}) +print(f'pyproject.toml via tool(): {len(raw.output)} bytes') + +# Inspect available tool schemas +schemas = session.tool_definitions() +print(f'session exposes {len(schemas)} tools') + +# Structured output: generate a schema-validated JSON object +assert_direct_tool_allowed('generate_object') +structured = session.tool('generate_object', { + 'schema': { + 'type': 'object', + 'required': ['count', 'language'], + 'properties': { + 'count': {'type': 'integer'}, + 'language': {'type': 'string'}, + }, + }, + 'prompt': 'How many Python files are in this project?', + 'schema_name': 'file_stats', +}) +if structured.exit_code != 0: + raise RuntimeError(structured.output) +print('structured output:', json.loads(structured.output)['object']) + +session.close() +``` + + + + +Direct tools execute under the session workspace and should be treated as +privileged host operations. Most calls (`read`, `glob`, `grep`) are purely +deterministic; `generate_object` is the exception — it still calls the model +to fill a schema-validated JSON object, but you drive it explicitly rather than +through a free-form agent turn. + +A runnable version ships at `crates/code/sdk/node/examples/basic/test_generate_object.ts` +(Python: `crates/code/sdk/python/examples/test_generate_object.py`). diff --git a/website/docs/v6/en/guide/examples/external-tasks.mdx b/website/docs/v6/en/guide/examples/external-tasks.mdx new file mode 100644 index 00000000..108ce0c6 --- /dev/null +++ b/website/docs/v6/en/guide/examples/external-tasks.mdx @@ -0,0 +1,121 @@ +--- +title: 'External Tasks' +description: 'Fulfill agent-queued work from outside the agent process and report structured evidence back' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# External Tasks + +Some work cannot run inside the agent process: it belongs to a separate worker, a CI +runner, or a human in another system. When a lane is routed to an external handler, the +tools on that lane are **queued** instead of executed — they wait as external tasks. Your +host code drains the pending queue, does the work however it likes, and reports the +outcome back with `completeExternalTask`. Reach for this only when an outside worker is +genuinely part of your architecture. + +External tasks are produced by the [lane queue](/guide/examples/lane-queue): you must +register at least one `external` (or `hybrid`) lane handler first, otherwise every task +runs in-process and there is nothing to drain. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session(process.cwd()); + +// Route a lane to an external worker so its tools are queued, not executed. +await session.setLaneHandler('execute', { + mode: 'external', + timeoutMs: 300000, +}); + +// Drain the tasks waiting for the host to fulfill. +const pending = await session.pendingExternalTasks(); + +for (const task of pending) { + console.log( + `pending: ${task.task_id} on ${task.lane} (${task.command_type})`, + ); + + try { + // ...the host does the real work here (run CI, call a service, ask a human)... + const ok = await session.completeExternalTask(task.task_id, { + success: true, + result: { + summary: 'worker completed the test run', + command: 'npm run build', + exitCode: 0, + }, + }); + console.log('completed:', ok); + } catch (err) { + await session.completeExternalTask(task.task_id, { + success: false, + error: String(err), + }); + } +} + +session.close(); +``` + + + + +```python +import os +from a3s_code import Agent + +agent = Agent.create("agent.acl") +session = agent.session(os.getcwd()) + +# Route a lane to an external worker so its tools are queued, not executed. +session.set_lane_handler("execute", "external", 300000) + +# Drain the tasks waiting for the host to fulfill. +pending = session.pending_external_tasks() + +for task in pending: + print(f"pending: {task['task_id']} on {task['lane']} ({task['command_type']})") + + try: + # ...the host does the real work here (run CI, call a service, ask a human)... + ok = session.complete_external_task( + task["task_id"], + success=True, + result={ + "summary": "worker completed the test run", + "command": "npm run build", + "exit_code": 0, + }, + ) + print("completed:", ok) + except Exception as err: + session.complete_external_task( + task["task_id"], + success=False, + error=str(err), + ) + +session.close() +``` + + + + +Notes: + +- Each pending task carries `task_id`, `session_id`, `lane`, `command_type`, `payload`, and + `timeout_ms`. Pass the `task_id` back to `completeExternalTask` / `complete_external_task` + to match the completion to the right task. +- The result shape is `{ success, result?, error? }`. `result` holds any + JSON-serializable payload; `error` is an optional message for failures. +- On success, return compact structured evidence (a summary plus the key facts), not raw + logs only — the agent reasons over the result, so keep it small and machine-readable. +- `completeExternalTask` / `complete_external_task` returns `true` when the task was found + and completed, `false` otherwise. In Python these queue methods are synchronous; in Node + `pendingExternalTasks` and `completeExternalTask` return promises. diff --git a/website/docs/v6/en/guide/examples/git-worktree.mdx b/website/docs/v6/en/guide/examples/git-worktree.mdx new file mode 100644 index 00000000..12181fbd --- /dev/null +++ b/website/docs/v6/en/guide/examples/git-worktree.mdx @@ -0,0 +1,107 @@ +--- +title: 'Git Worktree' +description: 'Drive git and git worktrees through the session git tool' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Git Worktree + +The session `git` tool runs git as a privileged host operation. It accepts a +structured command object (`command`, plus `subcommand`/`name`/`path` for +worktrees) and returns a tool result with `output` and `exitCode`. This example +inspects a repo, then creates, lists, and removes a worktree directly through the +tool surface. + + + + +```ts +import { Agent } from '@a3s-lab/code'; +import * as path from 'path'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/path/to/repo'); + +// Inspect the repo +const status = await session.git({ command: 'status' }); +console.log(status.output); + +// Create a worktree on a new branch +const wtPath = path.join('/path/to/repo', 'wt-feature-auth'); +const created = await session.git({ + command: 'worktree', + subcommand: 'create', + name: 'feature-auth', + path: wtPath, +}); +if (created.exitCode !== 0) throw new Error(`create failed: ${created.output}`); + +// List worktrees +const list = await session.git({ command: 'worktree', subcommand: 'list' }); +console.log(list.output); + +// Remove the worktree when done +const removed = await session.git({ + command: 'worktree', + subcommand: 'remove', + path: wtPath, +}); +if (removed.exitCode !== 0) throw new Error(`remove failed: ${removed.output}`); + +session.close(); +``` + + + + +```python +import os +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session("/path/to/repo", SessionOptions()) + +# Inspect the repo +status = session.git({"command": "status"}) +print(status.output) + +# Create a worktree on a new branch +wt_path = os.path.join("/path/to/repo", "wt-feature-auth") +created = session.git({ + "command": "worktree", + "subcommand": "create", + "name": "feature-auth", + "path": wt_path, +}) +if created.exit_code != 0: + raise RuntimeError(f"create failed: {created.output}") + +# List worktrees +listing = session.git({"command": "worktree", "subcommand": "list"}) +print(listing.output) + +# Remove the worktree when done +removed = session.git({ + "command": "worktree", + "subcommand": "remove", + "path": wt_path, +}) +if removed.exit_code != 0: + raise RuntimeError(f"remove failed: {removed.output}") + +session.close() +``` + + + + +Pass a command object rather than positional arguments: `{ command: 'status' }`, +`{ command: 'diff' }`, or `{ command: 'worktree', subcommand: 'list' }`. Each +call returns a tool result, so check `exitCode` (Node) / `exit_code` (Python) +before trusting the output. + +Direct git calls are privileged host operations. Put push, publish, and release +workflows behind application-level approval or automation gates. + +A runnable version ships at `crates/code/sdk/node/examples/git/test_worktree_git.ts`. diff --git a/website/docs/v6/en/guide/examples/hooks.mdx b/website/docs/v6/en/guide/examples/hooks.mdx new file mode 100644 index 00000000..a5191d2f --- /dev/null +++ b/website/docs/v6/en/guide/examples/hooks.mdx @@ -0,0 +1,94 @@ +--- +title: 'Lifecycle Hooks' +description: 'Register, count, and unregister lifecycle event callbacks that observe and gate agent activity.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Lifecycle Hooks + +Hooks let you observe and gate agent activity as it happens. You register a named +callback against a lifecycle event, the runtime invokes it at that point, and the +callback returns a decision such as `{ action: "continue" }`. Use hooks for +auditing, redaction, logging, or enforcing policy without changing the agent's +prompt. + +The lifecycle is symmetric: `registerHook` adds a callback by name, `hookCount` +tells you how many are active, and `unregisterHook` removes one by its name. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session(process.cwd()); + +// Register a named hook on a lifecycle event. The callback must NOT throw — +// always return a decision such as { action: 'continue' }. +session.registerHook( + 'observe-env-read', + 'pre_tool_use', + { pathPattern: '**/.env*' }, + { priority: 100 }, + () => ({ action: 'continue' }), +); + +console.log('active hooks:', session.hookCount()); // 1 + +await session.run('Read the project README and summarize it.'); + +// Remove the hook by name when you no longer need it. +session.unregisterHook('observe-env-read'); +console.log('active hooks:', session.hookCount()); // 0 + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session('.', SessionOptions()) + +# Register a named hook on a lifecycle event. The callback returns a decision. +session.register_hook( + 'observe-env-read', + 'pre_tool_use', + {'pathPattern': '**/.env*'}, + {'priority': 100}, + lambda: {'action': 'continue'}, +) + +print("active hooks:", session.hook_count()) # 1 + +session.run("Read the project README and summarize it.") + +# Remove the hook by name when you no longer need it. +session.unregister_hook('observe-env-read') +print("active hooks:", session.hook_count()) # 0 + +session.close() +``` + + + + +Notes: + +- A hook callback returns a decision. Return `{ action: "continue" }` + (Node) / `{"action": "continue"}` (Python) to let the agent proceed. +- The matcher (`{ pathPattern: '**/.env*' }`) scopes the hook to events whose + path matches the pattern, and `{ priority: 100 }` orders hooks on the same + event (higher runs first). +- Node hook callbacks must **not** throw — an uncaught throw can abort the + process. Keep the handler body total and always return a decision. +- `hookCount` / `hook_count` reflects the number of currently registered hooks, + which is handy in tests to assert that registration and cleanup happened. +- `unregisterHook` / `unregister_hook` takes the name you registered with. + Always tear down hooks you no longer need so they do not leak across runs. +- Validate the event path you depend on before using a hook as a production gate. diff --git a/website/docs/v6/en/guide/examples/index.mdx b/website/docs/v6/en/guide/examples/index.mdx new file mode 100644 index 00000000..857ddc2f --- /dev/null +++ b/website/docs/v6/en/guide/examples/index.mdx @@ -0,0 +1,24 @@ +--- +title: 'Examples' +description: 'Examples for the current A3S Code runtime and SDK surfaces' +--- + +# Examples + +These examples use the current A3S Code runtime concepts: ACL configuration, +environment-variable credentials, session APIs, streaming, structured output, +task-based and automatic subagent delegation, programmable orchestration +(`parallel` / `pipeline` / `parallelResumable`), `.a3s/agents`, skills, memory, +direct tools, workspace backends, verification, git workflows, +and optional MCP/queue infrastructure. + +Start with the [Quick Start](/guide/examples/quick-start), then explore by area: + +- **Sessions & runtime** — [Quick Start](/guide/examples/quick-start), [Streaming](/guide/examples/streaming), [Model switching](/guide/examples/model-switching), [Auto-compact](/guide/examples/auto-compact) +- **Structured & programmable** — [Structured output](/guide/examples/structured-output), [Orchestration](/guide/examples/orchestration), [Planning](/guide/examples/planning), [Batch](/guide/examples/batch) +- **Tools & context** — [Direct tools](/guide/examples/direct-tools), [ripgrep context](/guide/examples/ripgrep-context), [Prompt slots](/guide/examples/prompt-slots), [Git worktree](/guide/examples/git-worktree) +- **Skills & memory** — [Skills](/guide/examples/skills), [Skill tool](/guide/examples/skill-tool), [Memory](/guide/examples/memory), [Hooks](/guide/examples/hooks) +- **Security & verification** — [Security](/guide/examples/security) +- **MCP & queues** — [Lane queue](/guide/examples/lane-queue), [External tasks](/guide/examples/external-tasks) + +See the [Orchestration](/guide/examples/orchestration) example for fan-out (`parallel`), staged (`pipeline`), and resumable (`parallelResumable`) multi-agent workflows. diff --git a/website/docs/v6/en/guide/examples/lane-queue.mdx b/website/docs/v6/en/guide/examples/lane-queue.mdx new file mode 100644 index 00000000..9bf6fa0f --- /dev/null +++ b/website/docs/v6/en/guide/examples/lane-queue.mdx @@ -0,0 +1,90 @@ +--- +title: 'Lane Queue' +description: 'Route a lane to an external worker and drain its pending tasks explicitly.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Lane Queue + +By default A3S Code runs every task in-process, with no queue. The lane queue is **optional infrastructure**: register an external handler for a lane and the tools routed to that lane are queued for an outside worker instead of being executed by the agent. You then drain the pending tasks, run them however you like, and report results back. Reach for this only when an external worker is genuinely part of your architecture. + +The four lanes are `control`, `query`, `execute`, and `generate`. Each handler has a `mode` of `internal` (the default), `external`, or `hybrid`. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('./agent.acl'); +const session = agent.session(process.cwd()); + +// Route the "execute" lane to an external worker. +// Tools on this lane are NOT run by the agent; they are queued for +// an outside worker to pick up and complete. +await session.setLaneHandler('execute', { + mode: 'external', + timeoutMs: 300000, +}); + +// hasQueue() is false until at least one external/hybrid lane is registered. +console.log('queue active:', session.hasQueue()); + +// Drain whatever is waiting for an external worker. +const pending = await session.pendingExternalTasks(); +for (const task of pending) { + console.log('pending:', task.task_id, task.lane, task.command_type); + + // ... hand off to your worker, run it, then report the outcome back: + await session.completeExternalTask(task.task_id, { + success: true, + result: { note: 'done by external worker' }, + }); +} + +console.log('lane queue drained'); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +session = agent.session(".", opts) + +# Route the "execute" lane to an external worker. +# Tools on this lane are NOT run by the agent; they are queued for +# an outside worker to pick up and complete. +session.set_lane_handler("execute", "external", 300000) + +# has_queue() is False until at least one external/hybrid lane is registered. +print("queue active:", session.has_queue()) + +# Drain whatever is waiting for an external worker. +pending = session.pending_external_tasks() +for task in pending: + print("pending:", task["task_id"], task["lane"], task["command_type"]) + + # ... hand off to your worker, run it, then report the outcome back: + session.complete_external_task( + task["task_id"], + success=True, + result={"note": "done by external worker"}, + ) + +print("lane queue drained") +``` + + + + +Notes: + +- **The default path is queue-free.** `hasQueue()` / `has_queue()` returns `false` until you register at least one `external` (or `hybrid`) lane handler. If you never call `setLaneHandler` / `set_lane_handler`, every task runs in-process and there is nothing to drain. +- Each pending task carries `task_id`, `session_id`, `lane`, `command_type`, `payload`, and `timeout_ms`. Pass the `task_id` back to `completeExternalTask` / `complete_external_task` once the work is done. +- The result shape is `{ success, result?, error? }` — `result` holds any JSON-serializable payload, and `error` is an optional message for failures. `completeExternalTask` / `complete_external_task` returns `true` if the task was found and completed, `false` otherwise. +- In Python these queue methods are synchronous; in Node `setLaneHandler`, `pendingExternalTasks`, and `completeExternalTask` return promises, while `hasQueue` is synchronous. diff --git a/website/docs/v6/en/guide/examples/memory.mdx b/website/docs/v6/en/guide/examples/memory.mdx new file mode 100644 index 00000000..74358d22 --- /dev/null +++ b/website/docs/v6/en/guide/examples/memory.mdx @@ -0,0 +1,95 @@ +--- +title: 'Memory' +description: 'Remember task outcomes and recall them later by similarity, tags, or recency.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Memory + +Persistent memory lets a session record what worked (and what didn't) and pull +those facts back later. SDK sessions have a default file-backed store at +`/.a3s/memory`; the TUI defaults to `~/.a3s/memory` so its `/memory` +panel and live session browse the same durable store. Pass a `memoryStore` only +when you want to override the path or backend. You can write outcomes explicitly +with `rememberSuccess` / `rememberFailure`, then retrieve them with +`recallSimilar`, `recallByTags`, or `memoryRecent`. + +LLM extraction is also enabled by default. It runs after significant completed +turns to distill durable preferences, workflows, decisions, and failure lessons, +while trivial turns are skipped. +Default stores consolidate exact and conservative near-duplicate memories into a +canonical item, while conflict-like memories remain separate for future recall. + + + + +```ts +import { Agent, FileMemoryStore } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/repo', { + memoryStore: new FileMemoryStore('./.a3s/memory'), +}); + +// Record outcomes as the agent works. +await session.rememberSuccess( + 'refactored auth module', + ['read', 'edit', 'bash'], + 'all tests passed after extracting AuthService', +); +await session.rememberFailure( + 'migration attempt', + ['bash'], + 'psql connection refused on port 5432', +); + +// Recall later — by recency, by tool tags, or by semantic similarity. +const recent = await session.memoryRecent(10); +const byTags = await session.recallByTags(['read', 'edit'], 5); +const similar = await session.recallSimilar('auth refactor', 5); + +console.log(recent.length, byTags.length, similar.length); +``` + + + + +```python +from a3s_code import Agent, SessionOptions, FileMemoryStore + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.memory_store = FileMemoryStore("./.a3s/memory") +session = agent.session("/repo", opts) + +# Record outcomes as the agent works. Python helpers are synchronous — no await. +session.remember_success( + "refactored auth module", + ["read", "edit", "bash"], + "all tests passed after extracting AuthService", +) +session.remember_failure( + "migration attempt", + ["bash"], + "psql connection refused on port 5432", +) + +# Recall later — by recency, by tool tags, or by semantic similarity. +recent = session.memory_recent(10) +by_tags = session.recall_by_tags(["read", "edit"], 5) +similar = session.recall_similar("auth refactor", 5) + +print(len(recent), len(by_tags), len(similar)) +``` + + + + +Both `rememberSuccess` and `rememberFailure` take a short task description, the +list of tools involved (which double as searchable tags), and the outcome text. +The three recall methods are complementary: `memoryRecent(limit)` returns the +newest entries, `recallByTags(tags, limit)` filters by the tool tags you +recorded, and `recallSimilar(query, limit)` ranks entries by semantic relevance +to a query. If the default file store cannot be created, the session falls back +to in-process memory and exposes an init warning. diff --git a/website/docs/v6/en/guide/examples/model-switching.mdx b/website/docs/v6/en/guide/examples/model-switching.mdx new file mode 100644 index 00000000..63315bc3 --- /dev/null +++ b/website/docs/v6/en/guide/examples/model-switching.mdx @@ -0,0 +1,175 @@ +--- +title: 'Model Switching' +description: 'Choose the model per session, and override it per worker agent for cost and capability tuning.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Model Switching + +A session runs against whatever model you pass in the `model` option. Declare +the models your agent can reach once, then pick one per session — a fast model +for high-volume, low-stakes work and a stronger model for review. Use this when +you want to balance cost against capability without changing any of your prompts. + +## Declaring models + +Models are configured in your agent file. Each provider lists the models it +exposes, and `default_model` is used when a session does not set `model`. + +```text +default_model = "provider/fast-model" + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "fast-model" { tool_call = true } + models "review-model" { tool_call = true } +} +``` + +## Per-session model + +The `model` option is set when you open the session. Everything that session +runs — `send`, `run`, `task`, `parallel`, `pipeline` — uses that model. One +agent configuration can drive different model choices for different sessions. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +// A fast model for high-volume, low-stakes work. +const fast = agent.session('/repo', { model: 'provider/fast-model' }); +const draft = await fast.run('Draft a short README intro for this project.'); +console.log('draft:', draft); +await fast.close(); + +// A stronger model for review / higher-stakes reasoning. +const review = agent.session('/repo', { model: 'provider/review-model' }); +const critique = await review.run(`Critique this README intro:\n${draft}`); +console.log('critique:', critique); +await review.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") + +# A fast model for high-volume, low-stakes work. +fast_opts = SessionOptions() +fast_opts.model = 'provider/fast-model' +fast = agent.session('/repo', fast_opts) +draft = fast.run('Draft a short README intro for this project.') +print('draft:', draft) +fast.close() + +# A stronger model for review / higher-stakes reasoning. +review_opts = SessionOptions() +review_opts.model = 'provider/review-model' +review = agent.session('/repo', review_opts) +critique = review.run(f'Critique this README intro:\n{draft}') +print('critique:', critique) +review.close() +``` + + + + +## Per-worker-agent model override + +Worker agents are registered with their own spec. Give a worker its own `model` +so it runs on a different (often smaller, cheaper) model than the session that +delegates to it. The orchestrating session keeps its own `model`; only the +delegated work runs on the worker's model. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +const session = agent.session('/repo', { + // Orchestrator stays on the stronger model. + model: 'provider/review-model', + // High-volume exploration runs on the cheaper model. + workerAgents: [ + { + name: 'scout', + description: 'Reads files and reports findings.', + model: 'provider/fast-model', + }, + ], +}); + +// Delegate exploration to the cheaper worker, then reason on the strong model. +const findings = await session.task({ + agent: 'scout', + description: 'List public APIs', + prompt: 'List every public API in src/.', +}); +const plan = await session.run( + `Given these findings, propose a refactor:\n${findings.output}`, +); +console.log(plan); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions, WorkerAgentSpec + +agent = Agent.create("agent.acl") + +opts = SessionOptions() +# Orchestrator stays on the stronger model. +opts.model = 'provider/review-model' +# High-volume exploration runs on the cheaper model. +scout = WorkerAgentSpec( + name='scout', + description='Reads files and reports findings.', +) +scout.model = 'provider/fast-model' +opts.worker_agents = [scout] +session = agent.session('/repo', opts) + +# Delegate exploration to the cheaper worker, then reason on the strong model. +findings = session.task({ + "agent": "scout", + "description": "List public APIs", + "prompt": "List every public API in src/.", +}) +plan = session.run(f'Given these findings, propose a refactor:\n{findings.output}') +print(plan) + +session.close() +``` + + + + +Notes: + +- The `model` value is an identifier string your runtime resolves to one of the + models declared in your agent file — there are no hard-coded model names in the + SDK. +- A worker agent's `model` applies only to that agent's delegated work. The + session's own `send`/`run`/`task` calls still use the session `model`. +- A worker without a `model` inherits the session `model`, so you only override + the agents where a different model actually pays off. + +A runnable version showing the `model` option on a session ships at +`crates/code/sdk/node/examples/basic/test_api_alignment.ts`. diff --git a/website/docs/v6/en/guide/examples/orchestration.mdx b/website/docs/v6/en/guide/examples/orchestration.mdx new file mode 100644 index 00000000..0db85c70 --- /dev/null +++ b/website/docs/v6/en/guide/examples/orchestration.mdx @@ -0,0 +1,335 @@ +--- +title: 'Orchestration' +description: 'Fan out independent work with session.parallel, build per-item chains with session.pipeline, and resume journaled runs with session.parallelResumable.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Orchestration + +This page shows the programmable orchestration primitives in A3S Code: `session.parallel` for fan-out, `session.pipeline` for per-item multi-stage chains, and `session.parallelResumable` for journaled runs that survive a crash. `parallel` also takes an optional token budget so a whole fan-out shares one ledger. Use orchestration when you have several independent subagent tasks (parallel), or one transformation that flows through ordered stages per input (pipeline). + +For the conceptual model behind these primitives, see [Orchestration](/guide/orchestration). + +## Fan-out with `session.parallel` + +`parallel` takes an array of `AgentStepSpec` and runs them concurrently, returning one `StepOutcome` per spec **in input order** (not completion order). Each spec routes to a named subagent (`explore`, `plan`, `review`, `verification`, `general`, ...). Set `outputSchema` / `output_schema` on a spec to get a schema-validated `structured` result back. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.', {}); + +// Independent steps; outcomes come back in input order, not completion order. +const outcomes = await session.parallel([ + { + taskId: 'langs', + agent: 'general', + description: 'list', + prompt: 'Name three systems languages.', + maxSteps: 2, + }, + { + taskId: 'safe', + agent: 'general', + description: 'classify', + prompt: 'Is Rust memory-safe without a GC? yes/no.', + maxSteps: 2, + }, +]); + +for (const o of outcomes) { + console.log(`[parallel] ${o.taskId}: success=${o.success}`); +} + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session(".", SessionOptions()) + +# Independent steps; outcomes come back in input order, not completion order. +outcomes = session.parallel([ + { + "task_id": "langs", + "agent": "general", + "description": "list languages", + "prompt": "Name three systems programming languages, comma-separated.", + "max_steps": 2, + }, + { + "task_id": "verdict", + "agent": "general", + "description": "classify", + "prompt": "Is Rust memory-safe without a GC? Answer yes or no.", + "max_steps": 2, + # Schema-validated structured output for this step. + "output_schema": { + "type": "object", + "properties": {"memory_safe": {"type": "boolean"}}, + "required": ["memory_safe"], + }, + }, +]) + +for o in outcomes: + print(f"[parallel] {o['task_id']}: success={o['success']} structured={o.get('structured')}") + +session.close() +``` + + + + +Outcomes are dicts in Python (`o['task_id']`, `o['success']`, `o.get('structured')`) and objects in Node (`o.taskId`, `o.success`, `o.structured`). The `maxParallelTasks` / `max_parallel_tasks` session option caps concurrency; extra specs queue, and the outcome array is still returned in full, in order. + +## Per-item chains with `session.pipeline` + +`pipeline` takes a list of input `items` and an ordered list of `stages`. Each item flows through the stages independently — there is **no barrier between stages**, so a fast item can reach stage 2 while a slow item is still in stage 1. A stage callback receives a `ctx`: the first stage sees `ctx.item`, later stages see `ctx.previous` (the prior `StepOutcome`, whose `.output` you build on). Return the next spec to continue, or `null` / `None` to stop that item's chain early. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.', {}); + +// Stage 2 builds on stage 1's output. A stage callback MUST NOT throw — +// return null to stop this item's chain. +const results = await session.pipeline( + ['the Rust programming language'], + [ + (ctx) => ({ + taskId: 'sum', + agent: 'general', + description: 'summarize', + prompt: `In one sentence, what is ${ctx.item}?`, + maxSteps: 2, + }), + (ctx) => ({ + taskId: 'cls', + agent: 'general', + description: 'classify', + prompt: `Reply YES or NO: does this describe a programming language?\n\n${ctx.previous.output}`, + maxSteps: 2, + }), + ], +); + +for (const r of results) { + console.log( + `[pipeline] final=${r === null ? null : JSON.stringify(r.output.slice(0, 60))}`, + ); +} + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session(".", SessionOptions()) + +# Each item chains through stages; stage 2 builds on stage 1. Return None from a +# stage (or raise — caught and treated as None) to stop that item's chain. +results = session.pipeline( + ["the Rust programming language"], + [ + lambda ctx: { + "task_id": "summarize", + "agent": "general", + "description": "summarize", + "prompt": f"In one sentence, what is {ctx['item']}?", + "max_steps": 2, + }, + lambda ctx: { + "task_id": "classify", + "agent": "general", + "description": "classify", + "prompt": "Reply with one word YES or NO: does this describe a " + f"programming language?\n\n{ctx['previous']['output']}", + "max_steps": 2, + }, + ], +) + +for r in results: + print(f"[pipeline] final={None if r is None else r['output'][:60]!r}") + +session.close() +``` + + + + +Key difference from `parallel`: stages are ordered and dependent, but items do **not** wait for each other between stages. Node stage callbacks must never throw — return `null` on error; Python stages may raise (a raised stage is caught and treated as `None`). + +## Resumable runs with `session.parallelResumable` + +`parallelResumable` is `parallel` with a journal. It takes the `specs` first and a stable `workflowId` second; each step's outcome is journaled to the session's store, so if the process crashes mid-run you can call it again with the same `workflowId` and completed steps are replayed from the journal instead of re-executed. It **requires a session store** — pass `sessionStore` / `session_store` when opening the session, or the call throws. + + + + +```ts +import { Agent, FileSessionStore } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +// parallelResumable journals to the session store; it throws without one. +const session = agent.session('.', { + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); + +// Signature is (specs, workflowId): specs first, stable workflowId second. +const outcomes = await session.parallelResumable( + [ + { + taskId: 'deps', + agent: 'general', + description: 'audit deps', + prompt: 'Check manifests for outdated dependencies.', + maxSteps: 2, + }, + { + taskId: 'tests', + agent: 'verification', + description: 'run tests', + prompt: 'Run the test suite and summarize failures.', + maxSteps: 2, + }, + ], + 'nightly-audit', +); + +// Re-running with the same workflowId replays completed steps from the journal. +console.log(outcomes.map((o) => `${o.taskId}:${o.success}`).join(' ')); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions, FileSessionStore + +agent = Agent.create("agent.acl") +# parallel_resumable journals to the session store; it raises without one. +opts = SessionOptions() +opts.session_store = FileSessionStore("./.a3s/sessions") +session = agent.session(".", opts) + +# Signature is (specs, workflow_id): specs first, stable workflow_id second. +outcomes = session.parallel_resumable( + [ + {"task_id": "deps", "agent": "general", "description": "audit deps", "prompt": "Check manifests for outdated dependencies.", "max_steps": 2}, + {"task_id": "tests", "agent": "verification", "description": "run tests", "prompt": "Run the test suite and summarize failures.", "max_steps": 2}, + ], + "nightly-audit", +) + +# Re-running with the same workflow_id replays completed steps from the journal. +print(" ".join(f"{o['task_id']}:{o['success']}" for o in outcomes)) + +session.close() +``` + + + + +## Budgeted fan-out with `session.parallel` + +Pass a token budget as the second argument and every child agent feeds **one +shared ledger**. With a budget, `parallel` resolves to `{ outcomes, budget }` +(the ledger snapshot) instead of the plain outcomes array; once the cap is hit, a +step that starts afterwards is denied (`success: false`). It is a soft cap — a +wide fan-out can race a few in-flight turns past it; the in-flight work is never +force-killed. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.', {}); + +const specs = [ + { + taskId: 'a', + agent: 'general', + description: 'q1', + prompt: 'Reply with one word: ready.', + maxSteps: 2, + }, + { + taskId: 'b', + agent: 'general', + description: 'q2', + prompt: 'Reply with one word: go.', + maxSteps: 2, + }, +]; + +// With a budget, parallel() resolves to { outcomes, budget } — all children +// share one ledger. (Without it, parallel(specs) returns the plain array.) +const { outcomes, budget } = await session.parallel(specs, 50_000); +for (const o of outcomes) + console.log(`[budget] ${o.taskId}: success=${o.success}`); +console.log(`spent ${budget.consumedTokens} / ${budget.limitTokens} tokens`); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session(".", SessionOptions()) + +specs = [ + {"task_id": "a", "agent": "general", "description": "q1", "prompt": "Reply with one word: ready.", "max_steps": 2}, + {"task_id": "b", "agent": "general", "description": "q2", "prompt": "Reply with one word: go.", "max_steps": 2}, +] + +# With a budget, parallel() returns {"outcomes", "budget"} (a shared ledger). +res = session.parallel(specs, budget_tokens=50_000) +for o in res["outcomes"]: + print(f"[budget] {o['task_id']}: success={o['success']}") +print(f"spent {res['budget']['consumed_tokens']} / {res['budget']['limit_tokens']} tokens") + +session.close() +``` + + + + +Notes: + +- All three primitives return outcomes aligned to input order: `{ taskId, success, output, error?, structured? }` (Node objects) / `{ "task_id", "success", "output", "error"?, "structured"? }` (Python dicts). +- Set `outputSchema` / `output_schema` on a spec to get a parsed result back in `structured`. +- `maxSteps` / `max_steps` caps the steps per subagent; `maxParallelTasks` / `max_parallel_tasks` (session option) caps fan-out concurrency. +- Pass a token budget to `parallel` (2nd arg `budgetTokens` / `budget_tokens=`) to cap a whole fan-out against one shared ledger; the return shape then becomes `{ outcomes, budget }`. It is a soft cap (usage is recorded after each call). +- Node pipeline stage callbacks must never throw — return `null` on error. Python stages may raise (a raised stage is caught and treated as `None`). + +A runnable version ships at `crates/code/sdk/node/examples/orchestration/parallel-pipeline.mjs` and `crates/code/sdk/python/examples/orchestration_workflow.py`. diff --git a/website/docs/v6/en/guide/examples/planning.mdx b/website/docs/v6/en/guide/examples/planning.mdx new file mode 100644 index 00000000..63ec93af --- /dev/null +++ b/website/docs/v6/en/guide/examples/planning.mdx @@ -0,0 +1,73 @@ +--- +title: 'Planning' +description: 'Make the agent plan before it acts with planningMode' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Planning + +Planning mode tells the session to produce a structured plan before it starts +calling tools. Use it for multi-step work (refactors, release reviews, audits) +where you want the agent to decompose the goal first instead of jumping straight +into edits. + +Set it through the session option `planningMode` (Node) / `planning_mode` +(Python). The accepted values are: + +| Value | Behavior | +| ------------ | --------------------------------------------------------------- | +| `"auto"` | The runtime detects from the message when a plan is worthwhile. | +| `"enabled"` | Force a plan on every request, even simple ones. | +| `"disabled"` | Skip planning entirely for the lowest-latency path. | + +`"auto"` is the default structured pre-analysis path. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/repo', { + planningMode: 'auto', +}); + +const result = await session.send( + 'Plan and complete the release-readiness review.', +); +console.log(result.text); +console.log(`${result.toolCallsCount} tool calls executed`); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.planning_mode = "auto" +session = agent.session("/repo", opts) + +result = session.send("Plan and complete the release-readiness review.") +print(result.text) +print(f"{result.tool_calls_count} tool calls executed") + +session.close() +``` + + + + +Planning state is attached to the run, so a host UI can render a task list from +run events and update completion as the agent works. Planning organizes the +work; verification commands still provide the completion evidence. + +Runnable session examples ship at +`crates/code/sdk/node/examples/orchestration/parallel-pipeline.mjs` and +`crates/code/sdk/python/examples/orchestration_workflow.py`. diff --git a/website/docs/v6/en/guide/examples/prompt-slots.mdx b/website/docs/v6/en/guide/examples/prompt-slots.mdx new file mode 100644 index 00000000..28b29e24 --- /dev/null +++ b/website/docs/v6/en/guide/examples/prompt-slots.mdx @@ -0,0 +1,159 @@ +--- +title: 'Prompt Slots' +description: "Customize the agent's persona, guidelines, and response style without overriding core behavior." +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Prompt Slots + +Prompt slots are session options that shape the agent's system prompt declaratively. Use +them for host-level behavior — persona, coding standards, output style — that should not +live inside each user prompt. The slots layer on top of the agent's built-in instructions, +so core tool behavior (reading, writing, running commands) is preserved. + +There are four slots: + +| Slot | Purpose | +| ---------------------------------- | ------------------------------------------ | +| `role` / `role` | The persona the agent adopts. | +| `guidelines` / `guidelines` | Standards and rules the agent must follow. | +| `responseStyle` / `response_style` | How the agent should format its replies. | +| `extra` / `extra` | Freeform instructions appended verbatim. | + +## Basic usage + +Set any subset of the slots when you open a session. They apply to every turn of that +session. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +async function main() { + // create() accepts an .acl file path or inline ACL string. + const agent = await Agent.create('agent.acl'); + + const session = agent.session('/repo', { + role: 'release-readiness reviewer', + guidelines: + 'Find blockers before improvements. Require command evidence for done claims.', + responseStyle: 'concise, findings first', + }); + + const result = await session.send('Is this repo ready to ship?'); + console.log(result.text); + + session.close(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +def main(): + # create() accepts an .acl file path or inline ACL string. + agent = Agent.create('agent.acl') + + opts = SessionOptions() + opts.role = 'release-readiness reviewer' + opts.guidelines = 'Find blockers before improvements. Require command evidence for done claims.' + opts.response_style = 'concise, findings first' + session = agent.session('/repo', opts) + + result = session.send('Is this repo ready to ship?') + print(result.text) + + session.close() + +main() +``` + + + + +## Each slot in turn + +The four slots compose independently. A persona-only session, a reviewer with strict +guidelines, and a session that appends a freeform instruction all use the same option set. + + + + +```ts +// 1. Custom role only. +let session = agent.session(workspace, { + role: 'You are a senior Rust developer who specializes in async programming.', +}); + +// 2. Role + guidelines + response style. +session = agent.session(workspace, { + role: 'You are a Python code reviewer.', + guidelines: 'Always check for type hints. Flag any use of `eval()`.', + responseStyle: 'Reply in bullet points. Be concise.', +}); + +// 3. Extra freeform instructions only. +session = agent.session(workspace, { + extra: "Always end your response with '-- A3S'", +}); + +// Core tool behavior is preserved regardless of the slots. +session = agent.session(workspace, { + role: 'You are a minimalist file manager.', + guidelines: 'Only create files when explicitly asked.', +}); +const result = await session.send( + "Create a file called test.txt with the content 'prompt slots work'. Then read it back.", +); +``` + + + + +```python +# 1. Custom role only. +opts = SessionOptions() +opts.role = 'You are a senior Rust developer who specializes in async programming.' +session = agent.session(workspace, opts) + +# 2. Role + guidelines + response style. +opts = SessionOptions() +opts.role = 'You are a Python code reviewer.' +opts.guidelines = 'Always check for type hints. Flag any use of `eval()`.' +opts.response_style = 'Reply in bullet points. Be concise.' +session = agent.session(workspace, opts) + +# 3. Extra freeform instructions only. +opts = SessionOptions() +opts.extra = "Always end your response with '-- A3S'" +session = agent.session(workspace, opts) + +# Core tool behavior is preserved regardless of the slots. +opts = SessionOptions() +opts.role = 'You are a minimalist file manager.' +opts.guidelines = 'Only create files when explicitly asked.' +session = agent.session(workspace, opts) +result = session.send( + "Create a file called test.txt with the content 'prompt slots work'. Then read it back.", +) +``` + + + + +Slots customize personality and house rules; they do not disable tools or change the +agent's core loop. Keep task-specific requests in the `send` message and reserve the slots +for behavior that should hold across every turn of the session. + +A runnable version ships at `crates/code/sdk/node/examples/skills/test_prompt_slots.ts`. diff --git a/website/docs/v6/en/guide/examples/quick-start.mdx b/website/docs/v6/en/guide/examples/quick-start.mdx new file mode 100644 index 00000000..b499b151 --- /dev/null +++ b/website/docs/v6/en/guide/examples/quick-start.mdx @@ -0,0 +1,62 @@ +--- +title: 'Quick Start' +description: 'Create an agent, open a session, run one turn, and read the result.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Quick Start + +The smallest useful program: create an agent from an ACL file, open a session on a +project directory, run one turn with `send`, print the reply text, and inspect the +verification summary the runtime produced for that turn. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +// Agent.create accepts an .acl file path or inline ACL string. +const agent = await Agent.create('agent.acl'); +const session = agent.session('.'); + +const result = await session.send('List the files in this directory.'); +console.log(result.text); + +// What the runtime checked while producing that turn. +console.log(result.verificationSummaryText); + +session.close(); +``` + + + + +```python +from a3s_code import Agent + +# Agent.create accepts an .acl file path or inline ACL string. +agent = Agent.create('agent.acl') +session = agent.session('.') + +result = session.send('List the files in this directory.') +print(result.text) + +# What the runtime checked while producing that turn. +print(result.verification_summary_text) + +session.close() +``` + + + + +`Agent.create()` accepts either an `.acl` file path or inline ACL source text in both +SDKs. Always `close()` the session when you are done so the runtime can flush state +and release resources. + +## Next steps + +- [Streaming](/guide/examples/streaming) — read tokens as they arrive +- [Sessions](/guide/sessions) — persist and resume conversations diff --git a/website/docs/v6/en/guide/examples/ripgrep-context.mdx b/website/docs/v6/en/guide/examples/ripgrep-context.mdx new file mode 100644 index 00000000..752dcbcb --- /dev/null +++ b/website/docs/v6/en/guide/examples/ripgrep-context.mdx @@ -0,0 +1,77 @@ +--- +title: 'Ripgrep Context Builder' +description: 'Use grep and glob to gather code context before asking the agent.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Ripgrep Context Builder + +Fast code search with `session.grep` and `session.glob` lets you gather the +relevant files and matching lines, then feed them into a prompt — a lightweight +retrieval step before the agent reasons. Use this when you want to scope the +agent to a specific slice of a large codebase instead of letting it explore from +scratch. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.'); + +// 1. Find candidate files by glob pattern (returns a list of paths). +const files = await session.glob('src/**/*.ts'); + +// 2. Search the workspace for the symbol we care about (returns ripgrep text). +const hits = await session.grep('createSession'); + +// 3. Build a context string and feed it into a focused prompt. +const context = [ + `Files in scope:\n${files.join('\n')}`, + `Matches for "createSession":\n${hits}`, +].join('\n\n'); + +const answer = await session.run( + `Using only this context, explain how createSession is wired up:\n\n${context}`, +); +console.log(answer); +``` + + + + +```python +from a3s_code import Agent + +agent = Agent.create("agent.acl") +session = agent.session('.') + +# 1. Find candidate files by glob pattern (returns a list of paths). +files = session.glob('src/**/*.ts') + +# 2. Search the workspace for the symbol we care about (returns ripgrep text). +hits = session.grep('createSession') + +# 3. Build a context string and feed it into a focused prompt. +context = '\n\n'.join([ + 'Files in scope:\n' + '\n'.join(files), + 'Matches for "createSession":\n' + hits, +]) + +answer = session.run( + f'Using only this context, explain how createSession is wired up:\n\n{context}' +) +print(answer) +``` + + + + +`glob` returns a list of matching file paths, while `grep` returns the raw +ripgrep output as a single string. Both run locally and return quickly, so you +can chain several searches to assemble context cheaply before spending a model +turn. Pair them with `session.readFile` when you need the full body of a file +rather than just the matching lines. diff --git a/website/docs/v6/en/guide/examples/security.mdx b/website/docs/v6/en/guide/examples/security.mdx new file mode 100644 index 00000000..d9f2f74f --- /dev/null +++ b/website/docs/v6/en/guide/examples/security.mdx @@ -0,0 +1,204 @@ +--- +title: 'Security' +description: 'Gate privileged operations with a permission policy, a human-in-the-loop confirmation flow, and a security provider' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Security + +Every side effect an agent can produce — writing files, running `bash`, pushing +git — flows through a permission policy. Start from an `ask` or `deny` +fallback, then list the patterns that should be `allow`-ed, `deny`-ed, or sent +to the `ask` path. To keep a human in the loop, add a confirmation policy: +`ask` decisions pause on a `confirmation_required` event so your application +(or a person) can approve or reject each call. Use this whenever an agent runs +against a real repository. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +const session = agent.session(process.cwd(), { + permissionPolicy: { + allow: ['read(*)', 'grep(*)', 'glob(*)', 'ls(*)', 'bash(git status:*)'], + deny: ['write(**/.env*)', 'bash(rm -rf*)'], + ask: [ + 'write(*)', + 'edit(*)', + 'bash(git push:*)', + 'bash(npm publish:*)', + 'bash(*)', + ], + defaultDecision: 'ask', + }, + // Turn the `ask` patterns into a human-in-the-loop confirmation flow. + confirmationPolicy: { + enabled: true, + defaultTimeoutMs: 120000, + timeoutAction: 'reject', + }, +}); + +// Stream execution and resolve confirmations as they arrive. +const stream = await session.stream('Bump the version and push the release'); +while (true) { + const next = await stream.next(); + if (next.done || !next.value) break; + + const event = next.value; + if (event.type === 'confirmation_required') { + // Look up the pending request for richer display. + const [pending] = await session.pendingConfirmations(); + const toolId = pending?.toolId ?? event.toolId; + console.log(`[confirm] ${pending?.toolName ?? event.toolName}`); + console.log(JSON.stringify(pending?.args ?? {}, null, 2)); + + // In a real app, prompt the user here. + const approved = false; // deny risky operations by default + if (toolId) + await session.confirmToolUse(toolId, approved, 'Reviewed by host'); + } +} + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions, PermissionPolicy, ConfirmationPolicy + + +def main() -> None: + agent = Agent.create("agent.acl") + + opts = SessionOptions() + opts.permission_policy = PermissionPolicy( + allow=["read(*)", "grep(*)", "glob(*)", "ls(*)", "bash(git status:*)"], + deny=["write(**/.env*)", "bash(rm -rf*)"], + ask=["write(*)", "edit(*)", "bash(git push:*)", "bash(npm publish:*)", "bash(*)"], + default_decision="ask", + ) + # Turn the `ask` patterns into a human-in-the-loop confirmation flow. + opts.confirmation_policy = ConfirmationPolicy( + enabled=True, + default_timeout_ms=120_000, + timeout_action="reject", + ) + + session = agent.session(".", opts) + + # Stream execution and resolve confirmations as they arrive. + for event in session.stream("Bump the version and push the release"): + if event.event_type == "confirmation_required": + # Look up the pending request for richer display. + pending = session.pending_confirmations() + first = pending[0] if pending else {} + tool_id = first.get("tool_id") or event.tool_id + print(f"[confirm] {first.get('tool_name') or event.tool_name}") + + # In a real app, prompt the user here. + approved = False # deny risky operations by default + if tool_id: + session.confirm_tool_use(tool_id, approved, "Reviewed by host") + + session.close() + + +if __name__ == "__main__": + main() +``` + + + + +## Add a security provider + +A `DefaultSecurityProvider` enables input taint tracking and output sanitisation, +screening tool I/O independently of the permission policy. Pass one through +`securityProvider` (Node) / `security_provider` (Python); omit it to disable +security entirely. + + + + +```ts +import { Agent, DefaultSecurityProvider } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +const session = agent.session(process.cwd(), { + securityProvider: new DefaultSecurityProvider(), + permissionPolicy: { + allow: ['bash(echo:*)'], + ask: ['bash(*)'], + defaultDecision: 'ask', + }, +}); + +// Privileged host operations run through the provider + policy. +const out = await session.bash('echo "screened by the security provider"'); +console.log(out); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions, PermissionPolicy, DefaultSecurityProvider + + +def main() -> None: + agent = Agent.create("agent.acl") + + opts = SessionOptions() + opts.security_provider = DefaultSecurityProvider() + opts.permission_policy = PermissionPolicy( + allow=["bash(echo:*)"], + ask=["bash(*)"], + default_decision="ask", + ) + + session = agent.session(".", opts) + + # Privileged host operations run through the provider + policy. + out = session.bash('echo "screened by the security provider"') + print(out) + + session.close() + + +if __name__ == "__main__": + main() +``` + + + + +## Notes + +- `defaultDecision` is the fallback for any pattern not matched by `allow` / `deny` + / `ask` (one of `allow`, `deny`, or `ask`). Prefer `ask` for real repositories + and open up only what automation needs. +- A `confirmationPolicy` with `enabled: true` is what turns `ask` decisions into a + pausing `confirmation_required` event. Resolve each one with + `session.confirmToolUse(toolId, approved, reason?)`; if no answer arrives within + `defaultTimeoutMs`, `timeoutAction` (`reject`) decides the outcome. +- Keep release and publish actions (`bash(git push*)`, `bash(npm publish*)`) on the + `ask` or `deny` path unless automation owns the final step. +- Direct host calls such as `session.tool()`, `session.bash()`, and `session.git()` + are privileged host operations initiated by your application code. Authorize + them in the host before calling the SDK; the permission policy above gates + model-selected tool calls inside `send`, `run`, and `stream`. + +A runnable confirmation loop ships at +`crates/code/sdk/node/examples/streaming/hitl_confirmation_loop.ts` and +`crates/code/sdk/python/examples/hitl_confirmation_loop.py`. diff --git a/website/docs/v6/en/guide/examples/skill-tool.mdx b/website/docs/v6/en/guide/examples/skill-tool.mdx new file mode 100644 index 00000000..be118085 --- /dev/null +++ b/website/docs/v6/en/guide/examples/skill-tool.mdx @@ -0,0 +1,95 @@ +--- +title: 'Skill Tool' +description: 'Invoke a registered skill as a callable tool' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Skill Tool + +Skills surface to the model as two core tools: `search_skills` (find a skill by intent) +and `Skill` (invoke a skill by name). A tool-kind skill runs its handler; an +instruction-kind skill returns its body for the model to apply. You can let the model +call these tools during a run, or invoke a skill directly from the SDK with +`session.tool('Skill', ...)`. + +Register a skill directory (a folder of `SKILL.md` files) via `skillDirs` / `skill_dirs`, +or pass inline skills through session options. A3S Code no longer ships default +embedded skills, so `builtinSkills` / `builtin_skills` is only a compatibility +flag. Registered skills become visible through `Skill` and `search_skills`. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session(process.cwd(), { + skillDirs: ['./skills'], // a folder of SKILL.md files +}); + +// Skill and search_skills are core tools — confirm they're on the surface. +console.log(session.toolNames()); + +// Option A: let the model search for and apply a skill during a run. +const run = await session.run( + 'Search available skills, then apply the most relevant one.', +); +console.log(run.text); + +// Option B: invoke a skill directly as a callable tool. +// Canonical args: { skill_name, prompt? }. +const result = await session.tool('Skill', { + skill_name: 'release-review', + prompt: 'Review this release patch for blockers and verification gaps.', +}); +if (result.exitCode !== 0) { + throw new Error(result.output); +} +console.log(result.output); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.skill_dirs = ['./skills'] # a folder of SKILL.md files +session = agent.session('.', opts) + +# Skill and search_skills are core tools — confirm they're on the surface. +print(session.tool_names()) + +# Option A: let the model search for and apply a skill during a run. +run = session.run('Search available skills, then apply the most relevant one.') +print(run.text) + +# Option B: invoke a skill directly as a callable tool. +# Canonical args: { skill_name, prompt? }. +result = session.tool('Skill', { + 'skill_name': 'release-review', + 'prompt': 'Review this release patch for blockers and verification gaps.', +}) +if result.exit_code != 0: + raise RuntimeError(result.output) +print(result.output) + +session.close() +``` + + + + +A `SKILL.md` declares its `kind` in frontmatter (`tool`, `instruction`, or `agent`). +For a tool-kind skill, the `Skill` tool runs the skill's handler and returns its output; +for an instruction-kind skill, it returns the body for the model to apply. Skill +administration is handled by SDK registration, skill directories, or project files — not +by a model-visible management tool. + +A runnable version ships at `crates/code/sdk/node/examples/skills/test_custom_skills_agents.ts`. diff --git a/website/docs/v6/en/guide/examples/skills.mdx b/website/docs/v6/en/guide/examples/skills.mdx new file mode 100644 index 00000000..45fbe54e --- /dev/null +++ b/website/docs/v6/en/guide/examples/skills.mdx @@ -0,0 +1,95 @@ +--- +title: 'Skills & Custom Agents' +description: 'Load project skills and custom subagents from filesystem conventions.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Skills & Custom Agents + +A3S Code does not ship default embedded skills. Load your own skills and +subagents from directories on disk. Use `skillDirs` for Markdown skills and +`agentDirs` for worker/subagent definitions. `registerAgentDir` can add more +agent definition directories after the session exists; skill directories are +loaded when the session is created. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +// Add project skills and agents explicitly. +const session = agent.session('/path/to/project', { + skillDirs: ['./.a3s/skills'], + agentDirs: ['./.a3s/agents'], +}); + +// You can register more agent definition directories after the session exists. +session.registerAgentDir('./team/shared-agents'); + +// Inspect what the session loaded. +console.log('Tools:', session.toolNames()); +console.log('Commands:', session.listCommands()); + +// The agent now has access to the project skills. +const result = await session.run( + 'Use the project conventions skill to scaffold a new module.', +); +console.log(result.text); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") + +# Add project skills and agents explicitly. +opts = SessionOptions() +opts.skill_dirs = ['./.a3s/skills'] +opts.agent_dirs = ['./.a3s/agents'] + +session = agent.session('/path/to/project', opts) + +# You can register more agent definition directories after the session exists. +session.register_agent_dir('./team/shared-agents') + +# Inspect what the session loaded. +print('Tools:', session.tool_names()) +print('Commands:', session.list_commands()) + +# The agent now has access to the project skills. +result = session.run( + 'Use the project conventions skill to scaffold a new module.', +) +print(result.text) + +session.close() +``` + + + + +## Skill Registry Behavior + +The default effective skill registry contains no embedded skills. +`builtinSkills: true` / `builtin_skills = True` is accepted for compatibility, +but it currently adds nothing. Add skills with `skillDirs` / `skill_dirs`, +inline skills, or an explicit custom `SkillRegistry`. + +For day-to-day projects, keep durable reusable behavior in `.a3s/skills` or a +configured skill directory. + +Custom subagents loaded from `agentDirs` can be referenced by name in +[`session.parallel(...)`](/guide/examples/orchestration) and +[`session.pipeline(...)`](/guide/examples/orchestration) alongside the built-in +registry agents (`explore`, `plan`, `general`, `verification`, `review`). + +A runnable version ships at `crates/code/sdk/node/examples/skills/test_custom_skills_agents.ts`. diff --git a/website/docs/v6/en/guide/examples/streaming.mdx b/website/docs/v6/en/guide/examples/streaming.mdx new file mode 100644 index 00000000..be9a93ee --- /dev/null +++ b/website/docs/v6/en/guide/examples/streaming.mdx @@ -0,0 +1,99 @@ +--- +title: 'Streaming' +description: 'Read incremental AgentEvent values as a turn runs' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Streaming + +`session.stream(prompt)` yields incremental events as the turn runs, so you can render text as it arrives and react to tool activity in real time. Use it when you want a live UI or a CLI that prints output token-by-token instead of waiting for the full result from `send` or `run`. + +Each event carries the stable envelope fields `version`, `type`, `payload`, and optional `metadata`. The common kinds are `agent_start`, `text_delta` / `reasoning_delta`, `tool_start` / `tool_end`, `agent_end`, and `error`. Verification data is carried by the `agent_end` payload and convenience fields. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session(process.cwd(), { planningMode: 'disabled' }); + +const stream = await session.stream( + 'Use the bash tool to run the tests, then summarize the result.', +); + +while (true) { + const next = await stream.next(); + if (next.done || !next.value) break; + + const event = next.value; + if (event.type === 'text_delta' && event.text) { + process.stdout.write(event.text); + } else if (event.type === 'tool_start') { + console.log(`\n[tool:start] ${event.toolName ?? 'unknown'}`); + } else if (event.type === 'tool_end') { + console.log( + `\n[tool:end] ${event.toolName ?? 'unknown'} exit=${event.exitCode ?? 0}`, + ); + } else if (event.type === 'agent_end') { + console.log(`\n[verification] ${event.verificationSummaryText ?? ''}`); + } else if (event.type === 'error') { + throw new Error(event.error ?? 'stream error'); + } +} + +console.log('\n[stream] complete'); +session.close(); +``` + + + + +```python +import os + +from a3s_code import Agent, SessionOptions + + +def main() -> None: + agent = Agent.create("agent.acl") + + opts = SessionOptions() + opts.planning_mode = "disabled" + session = agent.session(".", opts) + + prompt = "Use the bash tool to run the tests, then summarize the result." + + try: + for event in session.stream(prompt): + if event.type == "text_delta" and event.text: + print(event.text, end="", flush=True) + elif event.type == "tool_start": + print(f"\n[tool:start] {event.tool_name or 'unknown'}") + elif event.type == "tool_end": + print(f"\n[tool:end] {event.tool_name or 'unknown'} exit={event.exit_code or 0}") + elif event.type == "agent_end": + print(f"\n[verification] {event.verification_summary_text or ''}") + elif event.type == "error": + raise RuntimeError(event.error or "stream error") + print("\n[stream] complete") + finally: + session.close() + + +if __name__ == "__main__": + main() +``` + + + + +Notes: + +- Node iterates the stream manually with `stream.next()`, checking `next.done` and `next.value`. In the current build the Python SDK exposes streaming as a synchronous iterator, so you consume it with a plain `for` loop (orchestration APIs such as `parallel` and `pipeline` remain `async`). +- Both SDKs expose the canonical `event.type`; Python retains `event.event_type` as a compatibility alias. `event.payload` is the complete lossless payload, and unknown future types are preserved. Convenience fields follow each language's casing: `toolName` / `exitCode` / `verificationSummaryText` in Node, `tool_name` / `exit_code` / `verification_summary_text` in Python. +- Streamed events can also include human-in-the-loop confirmation signals (`confirmation_required`, `confirmation_received`, `confirmation_timeout`) when a confirmation policy is enabled. + +Runnable streaming examples ship under `crates/code/sdk/node/examples/streaming/`. A complete human-in-the-loop confirmation loop ships at `crates/code/sdk/node/examples/streaming/hitl_confirmation_loop.ts`, and the matching Python version ships at `crates/code/sdk/python/examples/hitl_confirmation_loop.py`. diff --git a/website/docs/v6/en/guide/examples/structured-output.mdx b/website/docs/v6/en/guide/examples/structured-output.mdx new file mode 100644 index 00000000..3e9492f0 --- /dev/null +++ b/website/docs/v6/en/guide/examples/structured-output.mdx @@ -0,0 +1,292 @@ +--- +title: 'Structured Output' +description: 'Generate schema-validated JSON objects with the generate_object tool.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Structured Output + +The built-in `generate_object` tool asks the configured LLM for a JSON object, +validates the response against a JSON Schema you supply, and returns the +validated object only on a zero-exit result. Use it whenever you need +machine-readable results: extraction, classification, config generation, or +feeding another program. + +You can call it directly through `session.tool('generate_object', ...)`. The +tool result carries the validated object as JSON on `result.output` — parse it +and read the `object` field. The same tool also supports agent-driven +invocation, where the model decides to call it during a `send`. + +## Direct tool call + +The simplest path: call `generate_object` directly, check the tool exit code, +and parse the validated object out of the result. + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.'); + +const result = await session.tool('generate_object', { + schema: { + type: 'object', + required: ['name', 'age', 'skills'], + properties: { + name: { type: 'string' }, + age: { type: 'integer', minimum: 0 }, + skills: { + type: 'array', + items: { type: 'string' }, + minItems: 1, + }, + }, + }, + prompt: 'Extract: "Alice is 28, skilled in Rust, TypeScript, and Python."', + schema_name: 'developer', + mode: 'tool', +}); + +if (result.exitCode !== 0) { + throw new Error(result.output); +} + +const { object } = JSON.parse(result.output); +console.log(object); +// { name: "Alice", age: 28, skills: ["Rust", "TypeScript", "Python"] } + +session.close(); +``` + + + + +```python +import json +from a3s_code import Agent + +agent = Agent.create('agent.acl') +session = agent.session('.') + +result = session.tool("generate_object", { + "schema": { + "type": "object", + "required": ["name", "age", "skills"], + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer", "minimum": 0}, + "skills": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1, + }, + }, + }, + "prompt": 'Extract: "Alice is 28, skilled in Rust, TypeScript, and Python."', + "schema_name": "developer", + "mode": "tool", +}) + +if result.exit_code != 0: + raise RuntimeError(result.output) + +obj = json.loads(result.output)["object"] +print(obj) +# {"name": "Alice", "age": 28, "skills": ["Rust", "TypeScript", "Python"]} + +session.close() +``` + + + + +The validated value lives on the `object` key of the parsed output. When +`result.exitCode` (Node) / `result.exit_code` (Python) is zero, fields declared +in `required` have passed runtime validation. If the model cannot satisfy the +schema after repair attempts, the tool reports a non-zero exit code. + +## Enum classification + +Constrain a field to a fixed set with `enum`. This turns a free-form model +classification into a schema-gated result. + + + + +```ts +const result = await session.tool('generate_object', { + schema: { + type: 'object', + required: ['sentiment', 'confidence'], + properties: { + sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] }, + confidence: { type: 'number', minimum: 0, maximum: 1 }, + }, + }, + prompt: 'Classify sentiment: "This is the worst product I have ever used."', + schema_name: 'sentiment', +}); + +const { object } = JSON.parse(result.output); +console.log(object.sentiment, object.confidence); // "negative" 0.97 +``` + + + + +```python +result = session.tool("generate_object", { + "schema": { + "type": "object", + "required": ["sentiment", "confidence"], + "properties": { + "sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]}, + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, + }, + }, + "prompt": 'Classify sentiment: "This is the worst product I have ever used."', + "schema_name": "sentiment", +}) + +obj = json.loads(result.output)["object"] +print(obj["sentiment"], obj["confidence"]) # "negative" 0.97 +``` + + + + +## Nested schemas and arrays + +Schemas can nest objects and arrays to any depth, and the runtime validates the whole structure. This models real config files, manifests, or API payloads in one call. + + + + +```ts +const result = await session.tool('generate_object', { + schema: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + minItems: 3, + maxItems: 5, + items: { + type: 'object', + required: ['name', 'category'], + properties: { + name: { type: 'string' }, + category: { type: 'string', enum: ['fruit', 'vegetable', 'grain'] }, + }, + }, + }, + }, + }, + prompt: 'List 3 food items with their categories.', + schema_name: 'food_list', +}); + +const { items } = JSON.parse(result.output).object; +console.log( + items.length, + items.map((i) => i.name), +); +``` + + + + +```python +result = session.tool("generate_object", { + "schema": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "minItems": 3, + "maxItems": 5, + "items": { + "type": "object", + "required": ["name", "category"], + "properties": { + "name": {"type": "string"}, + "category": {"type": "string", "enum": ["fruit", "vegetable", "grain"]}, + }, + }, + }, + }, + }, + "prompt": "List 3 food items with their categories.", + "schema_name": "food_list", +}) + +items = json.loads(result.output)["object"]["items"] +print(len(items), [i["name"] for i in items]) +``` + + + + +## Agent-driven invocation + +You can also let the agent decide when to use structured output. Ask it to call `generate_object` during a `send`; it gathers context first, then emits the object. + + + + +```ts +const result = await session.send( + 'Use the generate_object tool to extract the following into an object ' + + 'with fields "title" (string), "year" (integer), "genre" (string): ' + + 'The movie "Inception" was released in 2010 and is a sci-fi thriller.', +); + +console.log( + `tool calls: ${result.toolCallsCount}, tokens: ${result.totalTokens}`, +); +``` + + + + +```python +result = session.send( + 'Use the generate_object tool to produce a JSON object with schema ' + '{"type":"object","required":["language","paradigm"],"properties":' + '{"language":{"type":"string"},"paradigm":{"type":"string"}}} ' + 'for: "Rust is a systems programming language with a focus on safety."' +) + +print(f"tool calls: {result.tool_calls_count}, tokens: {result.total_tokens}") +``` + + + + +## Schema validation coverage + +The built-in validator supports: + +- `type` (including nullable arrays like `["string", "null"]`) +- `required`, `properties`, `additionalProperties` +- `enum`, `const` +- `anyOf`, `oneOf` +- `minLength`, `maxLength`, `pattern` +- `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum` +- `minItems`, `maxItems`, `items` +- Nested object and array validation + +## Notes + +- The validated value is on the `object` key of the parsed `result.output`. Pass `mode: 'tool'` for the current cross-provider default, or `mode: 'prompt'` for a prompt-only fallback. `auto`, `strict`, and `json` currently resolve to `tool` in this runtime path. +- List every field you depend on in `required` — the runtime enforces it, so missing or mistyped fields fail validation instead of silently returning partial data. +- `generate_object` is a built-in tool registered independently of built-in skills. +- Direct `session.tool(...)` calls are host control-plane calls. Use `permissionPolicy` when you let the model choose tools inside `send` / `run` / `stream`; use host authorization before direct SDK calls. + +A runnable version ships at `crates/code/sdk/node/examples/basic/test_generate_object.ts` and `crates/code/sdk/python/examples/test_generate_object.py`. diff --git a/website/docs/v6/en/guide/filesystem-agents.mdx b/website/docs/v6/en/guide/filesystem-agents.mdx new file mode 100644 index 00000000..67c42172 --- /dev/null +++ b/website/docs/v6/en/guide/filesystem-agents.mdx @@ -0,0 +1,72 @@ +--- +title: 'agents/ Role Directory' +description: 'Define worker agents for task, parallel_task, and automatic delegation' +--- + +# agents/ Role Directory + +`agents/` stores worker/subagent definitions. Prefer `.a3s/agents/` for new A3S projects. Migration projects may still read `.claude/agents/`, but new docs and projects should use `.a3s/agents/`. + +```text +repo/ +└── .a3s/ + └── agents/ + ├── explorer.md + ├── security-reviewer.md + └── verification-runner.md +``` + +These files are not the main AgentDir. They are invoked by `task`, `parallel_task`, `session.task(...)`, `session.tasks(...)`, or `autoDelegation`. The parent session still owns final synthesis, verification, and permission boundaries. + +## Agent File + +```md +--- +name: security-reviewer +description: Use for permission, secret, and external side-effect review +tools: Read, Grep, Glob, Bash(rg *) +disallowedTools: + - Write + - Bash(git push *) +--- + +Review security risks first. Return blockers, evidence paths, and required verification. +``` + +`name` is the call name. `description` drives automatic routing. The body describes the worker role. Tool fields narrow visible capabilities; do not rely on the worker merely promising not to do risky things. + +## Manual Delegation + +```ts +const session = agent.session('/repo', { + agentDirs: ['./.a3s/agents'], + maxParallelTasks: 4, +}); + +await session.task({ + agent: 'security-reviewer', + description: 'Review release side effects', + prompt: 'Check changed auth, permission, and external API paths.', +}); +``` + +Fixed flows are better as manual delegation or programmable orchestration. Automatic delegation fits goals where the parent agent should choose specialists. + +## Automatic Delegation + +```ts +const session = agent.session('/repo', { + agentDirs: ['./.a3s/agents'], + autoDelegation: { enabled: true, minConfidence: 0.72, maxTasks: 4 }, +}); +``` + +Automatic delegation depends on descriptions and confidence scoring. Write descriptions that say when to use the agent, not just what the role is called. + +## Practices + +- Keep one role per file. +- Make descriptions useful for routing and bodies useful for execution. +- Ask workers to return evidence, risks, and next steps. +- Keep publish, delete, push, and other high-risk permissions out of default workers. +- Use `workerAgents` or `registerWorkerAgent()` for dynamic one-off workers. diff --git a/website/docs/v6/en/guide/filesystem-config.mdx b/website/docs/v6/en/guide/filesystem-config.mdx new file mode 100644 index 00000000..871dd5bb --- /dev/null +++ b/website/docs/v6/en/guide/filesystem-config.mdx @@ -0,0 +1,91 @@ +--- +title: 'agent.acl' +description: 'File-based runtime config for models, providers, queues, skill dirs, and worker agent dirs' +--- + +# agent.acl + +`agent.acl` is the runtime configuration entry point for filesystem-first agents. It turns model, provider, queue, storage, skill directory, and worker agent directory policy into versioned configuration. + +An SDK host may pass any `.acl` file explicitly with `Agent.create("agent.acl")`. +The `a3s code` TUI discovers `.a3s/config.acl` from the workspace and then +`~/.a3s/config.acl`; it does not require a root `agent.acl`. An AgentDir-local +`agent.acl` serves that durable agent. The format is the same; discovery and +scope are different. + +## Basic Config + +```text +default_model = "provider/model-id" +max_parallel_tasks = 4 +auto_parallel = false + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "model-id" { + tool_call = true + limit = { + context = 128000 + output = 4096 + } + } +} +``` + +`apiKey` / `api_key` and `baseUrl` / `base_url` are accepted aliases. The +runtime does not hard-code model names; `default_model` and session-level +`model` overrides must match the `provider/model-id` values declared here. + +Inject tokens through environment variables. Do not commit credentials. Once `agent.acl` lives in the repo, it is product behavior and should be reviewed like code. + +## Directory Discovery + +```text +skill_dirs = ["./.a3s/skills"] +agent_dirs = ["./.a3s/agents"] + +auto_delegation { + enabled = true + min_confidence = 0.72 + max_tasks = 4 + auto_parallel = false +} +``` + +`skill_dirs` points at reusable skills. `agent_dirs` points at worker/subagent definitions. Automatic delegation decides whether the model may choose a worker; it does not remove parent-session permission policy, tool visibility, or verification requirements. + +## Session Storage + +```text +storage_backend = "file" +sessions_dir = ".a3s/sessions" +``` + +`sessions_dir` is the local file-session persistence path used when the session +does not receive an explicit SDK `sessionStore`. `storage_backend = "memory"` +keeps sessions ephemeral. `storage_url` is parsed as custom storage metadata, +but it does not create a local `FileSessionStore` by itself. + +## Inside AgentDir + +AgentDir `agent.acl` is optional. When present, `AgentDir::load` parses it into `CodeConfig` and combines it with `instructions.md`, `skills/`, `tools/`, and `schedules/`. + +```text +release-agent/ +├── instructions.md +├── agent.acl +├── skills/ +├── tools/ +└── schedules/ +``` + +Good AgentDir config includes the agent's default model, providers, limits, queue policy, and private skill dirs. Keep environment differences outside the file; use env vars or host injection for development, staging, and production differences. + +## Boundaries + +- Config decides what can be connected and how the runtime starts; it does not bypass permission gates. +- Prefer workspace-relative or AgentDir-relative paths. +- Tune automatic delegation together with high-quality `agents/` descriptions. +- High-risk tools should still go through HITL or allow-lists. diff --git a/website/docs/v6/en/guide/filesystem-first.mdx b/website/docs/v6/en/guide/filesystem-first.mdx new file mode 100644 index 00000000..33f05f5e --- /dev/null +++ b/website/docs/v6/en/guide/filesystem-first.mdx @@ -0,0 +1,68 @@ +--- +title: 'Filesystem-First' +description: 'Persist roles, config, skills, tools, schedules, and team definitions as reviewable filesystem conventions' +--- + +# Filesystem-First + +A3S Code treats the filesystem as the first product interface for agents. Roles, tools, skills, schedules, and team definitions that need to last are written as files first, then loaded by convention. The goal is not fewer knobs; the goal is to make agent behavior reviewable, versioned, reusable, and portable. + +Filesystem-first has two common shapes: + +```text +repo/ +├── AGENTS.md # durable project instructions +├── agent.acl # model, provider, queue, and delegation config +└── .a3s/ + ├── agents/ # worker agents for task / autoDelegation + └── skills/ # reusable skills + +release-agent/ +├── instructions.md # role slot for one durable agent +├── agent.acl # runtime config for that agent +├── skills/ # private skills +├── tools/ # MCP / script tool specs +└── schedules/ # recurring turns +``` + +The first shape is a workspace convention for interactive development, team delegation, and project knowledge. The second shape is an AgentDir convention for long-running agents, scheduled work, and directory-scoped tools. + +## Path Map + +| Path | Purpose | Use it when | +| ------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `AGENTS.md` | Stable workspace instructions | Code style, verification commands, safety boundaries, and release flow must persist. | +| `instructions.md` | Role slot for an AgentDir main agent | A single directory should load as a durable agent. | +| `agent.acl` | Model, provider, queue, skill dirs, and delegation config | Runtime policy should be versioned with the repo or agent. | +| `.a3s/agents/` | Worker/subagent definitions | A parent agent needs `task`, `parallel_task`, or automatic delegation. | +| `.a3s/skills/`, `skills/` | Reusable skills | Many tasks share checklists, domain flow, or operating rules. | +| `tools/` | AgentDir MCP or script tools | A connector or constrained script belongs to scheduled agent sessions. | +| `schedules/` | Recurring turns | Daily reports, patrols, sync jobs, and regression checks should run on cron. | + +These conventions are not a new prompt system. `AGENTS.md`, `instructions.md`, and skills enter A3S Code's context composition path; tool visibility, permission gates, HITL, response contracts, and verification remain harness-controlled. + +## Loading Order + +A typical session parses `agent.acl`, binds a workspace, then loads project instructions, skills, agent definitions, direct tools, MCP connections, and runtime policy. `serve_agent_dir` loads an AgentDir by combining `instructions.md`, local `agent.acl`, `skills/`, `tools/`, and `schedules/`, then creates one independent session for every schedule. + +The closer a path is to a specific agent, the more local its meaning should be. Root `AGENTS.md` describes the whole project, `.a3s/agents/*.md` describes one worker, and AgentDir `instructions.md` describes the directory's main agent. + +## Design Rules + +- Conventions discover and assemble behavior; they do not bypass safety. +- Files should be code-reviewable. Model, provider, tool, schedule, and role changes should be visible in diffs. +- Never commit secrets. Use environment variables, host connections, or a secret manager. +- Keep one-off experiments in SDK options; commit files when behavior must be reused, audited, or migrated. +- AgentDir is the main-agent directory; `.a3s/agents/` is the worker-agent definition directory. + +## Reading Order + +1. [Convention Over Configuration](/guide/convention-over-configuration) +2. [AGENTS.md](/guide/agents-md) +3. [instructions.md](/guide/filesystem-instructions) +4. [agent.acl](/guide/filesystem-config) +5. [Agent Directory](/guide/agent-dir) +6. [agents/ Role Directory](/guide/filesystem-agents) +7. [skills/ Skill Directory](/guide/filesystem-skills) +8. [tools/ Tool Directory](/guide/filesystem-tools) +9. [schedules/ Schedule Directory](/guide/filesystem-schedules) diff --git a/website/docs/v6/en/guide/filesystem-instructions.mdx b/website/docs/v6/en/guide/filesystem-instructions.mdx new file mode 100644 index 00000000..054fd6d4 --- /dev/null +++ b/website/docs/v6/en/guide/filesystem-instructions.mdx @@ -0,0 +1,50 @@ +--- +title: 'instructions.md' +description: 'The AgentDir main-agent role slot and its boundary with AGENTS.md' +--- + +# instructions.md + +`instructions.md` is the role file for an AgentDir main agent. It is the only required file in an AgentDir, and its body is injected as `SystemPromptSlots.role` into every scheduled session. + +It has a different scope from `AGENTS.md`: + +| File | Scope | Good for | +| ----------------- | ------------------------- | --------------------------------------------------------------------------- | +| `AGENTS.md` | Workspace or subdirectory | Project rules, verification commands, code style, and safety requirements. | +| `instructions.md` | One AgentDir main agent | Role identity, work goals, output preferences, and durable task boundaries. | + +`instructions.md` is plain Markdown with no frontmatter. It does not override harness boundaries, response format, tool permissions, or verification requirements. + +## Recommended Shape + +```md +You are a release-readiness agent for this repository. + +Responsibilities: + +- Track release blockers and risky changes. +- Separate shipped changes from follow-up work. +- Never invent CI status, versions, or owners. + +Output: + +- Start with blockers. +- Include evidence paths. +- End with required verification commands. +``` + +Keep it short, stable, and reviewable. Put project commands in `AGENTS.md`, reusable process in `skills/`, and recurring prompts in `schedules/*.md`. `instructions.md` should answer: who is this durable agent, and how does it work by default? + +## Where It Is Used + +`serve_agent_dir` reads `instructions.md` at startup and applies it to every enabled schedule session. With a `SessionStore`, history comes from the store, but the current `instructions.md` is reloaded on every boot, so role edits take effect after restart. + +Interactive sessions can use SDK prompt slots for temporary roles. Commit `instructions.md` when the role should be reviewed, reused, or shared by several schedules. + +## Do Not Put + +- Secrets, tokens, private endpoints, or personal paths. +- Instructions to bypass safety or auto-approve high-risk actions. +- Large project manuals; link to docs or skills instead. +- Schedule prompts; every recurring task belongs in `schedules/*.md`. diff --git a/website/docs/v6/en/guide/filesystem-schedules.mdx b/website/docs/v6/en/guide/filesystem-schedules.mdx new file mode 100644 index 00000000..55fdcc97 --- /dev/null +++ b/website/docs/v6/en/guide/filesystem-schedules.mdx @@ -0,0 +1,74 @@ +--- +title: 'schedules/ Schedule Directory' +description: 'Declare cron turns, independent sessions, and recoverable context with Markdown schedule files' +--- + +# schedules/ Schedule Directory + +`schedules/` declares recurring turns for AgentDir. Each Markdown file is one schedule: frontmatter defines cron metadata, and the body is the prompt sent to the agent on every fire. + +```text +release-agent/ +└── schedules/ + ├── daily.md + └── weekly-risk-review.md +``` + +## Schedule File + +```md +--- +cron: '0 9 * * *' +name: daily-release-check +enabled: true +--- + +Summarize merged changes since the last run, inspect release risks, +and report only blockers plus required verification. +``` + +| Field | Required | Default | Meaning | +| --------- | -------- | --------- | ---------------------------------------------------- | +| `cron` | yes | - | 5-field or 6-field cron expression. | +| `name` | no | file stem | Schedule name and session id suffix. | +| `enabled` | no | `true` | Set `false` to keep the file but pause the schedule. | + +5-field cron is normalized to 6 fields by prefixing `0` seconds. Times are evaluated in UTC. + +## Independent Sessions + +Every schedule uses a stable session id: `schedule:`. Repeated fires of the same schedule accumulate context; different schedules stay isolated. Every session receives AgentDir `instructions.md`, `agent.acl`, `skills/`, and `tools/`. + +Daily and weekly schedules can share one AgentDir without sharing chat history. Use external storage, A3S Memory, or explicit tools when schedules need shared state. + +## Serve Daemon + +```rust +use a3s_code_core::config::AgentDir; +use a3s_code_core::serve::serve_agent_dir; +use a3s_code_core::Agent; +use tokio_util::sync::CancellationToken; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let agent_dir = AgentDir::load("./release-agent")?; + let agent = Agent::from_config(agent_dir.config.clone()).await?; + let cancel = CancellationToken::new(); + + serve_agent_dir(&agent, &agent_dir, "./workspace", None, cancel).await?; + Ok(()) +} +``` + +`serve_agent_dir` runs every enabled schedule until the cancellation token fires. Graceful cancellation lets an in-flight turn finish the current loop iteration. + +## Recovery And Limits + +With a `SessionStore`, daemon restart restores the conversation history for existing `schedule:` sessions. Current `instructions.md`, `skills/`, and `tools/` are re-applied on every boot. + +Know the limits: + +- Recovery restores history; it does not catch up missed fires during downtime. +- The store directory is a trust boundary for recovered history and workspace state. +- Invalid cron fails when the scheduler is built and names the offending schedule. +- An AgentDir with no enabled schedules returns immediately. diff --git a/website/docs/v6/en/guide/filesystem-skills.mdx b/website/docs/v6/en/guide/filesystem-skills.mdx new file mode 100644 index 00000000..c719ab66 --- /dev/null +++ b/website/docs/v6/en/guide/filesystem-skills.mdx @@ -0,0 +1,67 @@ +--- +title: 'skills/ Skill Directory' +description: 'Organize reusable skills, checklists, and domain process in workspace or AgentDir skill directories' +--- + +# skills/ Skill Directory + +`skills/` stores reusable skills. Skills express stable process, checklists, domain terms, and tool-use guidance. A skill is not a worker agent and does not start an independent session. + +A3S Code commonly uses two locations: + +```text +repo/.a3s/skills/ # workspace-level skills +release-agent/skills/ # AgentDir-private skills +``` + +Workspace skills are loaded through `skillDirs` or `agent.acl` `skill_dirs`. +Agent definitions use `agentDirs`; do not put skill directories in `agentDirs`. +AgentDir-private skills are injected when `serve_agent_dir` creates schedule +sessions. + +## Skill File + +```md +--- +name: release-readiness +description: Check whether a repository is ready to release +allowed-tools: read(*), grep(*), bash(pnpm test*), bash(cargo test*) +--- + +Always inspect: + +- package or crate version changes +- migration compatibility +- release notes +- required verification commands + +Return blockers first, then risks, then follow-up work. +``` + +Frontmatter helps discovery and filtering. The body describes how to execute. +Keep `allowed-tools` minimal. When the skill is invoked through the `Skill` +tool, omitted `allowed-tools` grants no tools, so the invocation remains +fail-secure. Skills guide the model; they should not widen permissions. + +## When To Use skills/ + +Use skills for repeated review checklists, product or protocol process, release and migration workflows, recommended tool order, and shared background for several worker agents. + +Do not use skills for independent roles that should be invoked by `task`; put those in `agents/`. Do not use skills for recurring jobs; put those in `schedules/`. Do not use skills to declare external capabilities; use `tools/` or MCP. + +## Loading + +```ts +const session = agent.session('/repo', { + skillDirs: ['./.a3s/skills'], +}); +``` + +The model can use `search_skills` to find relevant skills. File skills and inline skills share discovery semantics. As directories grow, keep `name` and `description` searchable and unambiguous. + +## Maintenance + +- Keep skill files short and stable. +- Use minimal runnable examples, not long logs. +- Put agent-specific skills in that AgentDir's `skills/`. +- Put repository-wide skills in `.a3s/skills/` and document their boundary in `AGENTS.md`. diff --git a/website/docs/v6/en/guide/filesystem-tools.mdx b/website/docs/v6/en/guide/filesystem-tools.mdx new file mode 100644 index 00000000..02690b61 --- /dev/null +++ b/website/docs/v6/en/guide/filesystem-tools.mdx @@ -0,0 +1,67 @@ +--- +title: 'tools/ Tool Directory' +description: 'Declare AgentDir MCP and script tools while preserving permissions, HITL, and allow-list boundaries' +--- + +# tools/ Tool Directory + +`tools/` declares directory-scoped tools for AgentDir. Each `tools/.md` describes one model-visible capability. A3S Code currently supports `kind: mcp` and `kind: script`. + +```text +release-agent/ +└── tools/ + ├── github.md + └── search-auth.md +``` + +Tool definitions come from the filesystem, but visibility, execution, confirmation, and audit still belong to the harness, permission policy, and AgentDir loader. A file existing is not unlimited permission. + +## MCP Tool + +```md +--- +kind: mcp +name: github +transport: stdio +command: npx +args: ['-y', '@modelcontextprotocol/server-github'] +env: + GITHUB_TOKEN: '${GITHUB_TOKEN}' +--- + +GitHub issues and pull request tools. +``` + +Every enabled schedule session connects the MCP server at startup and receives namespaced `mcp__github__*` tools. Inject secrets through environment variables, not the tool file. + +## Script Tool + +```md +--- +kind: script +name: search-auth +path: scripts/search-auth.js +allowed_tools: [grep, glob, read] +limits: + timeoutMs: 30000 + maxToolCalls: 30 + maxOutputBytes: 65536 +--- + +Find authentication-related files and return an evidence list. +``` + +`kind: script` exposes a pre-parameterized QuickJS `program` call as a model-visible tool. The source must define `async function run(ctx, inputs)`. It has no filesystem, network, process, or environment access; it can only call allow-listed tools through `ctx.tool(...)`. + +## Safety Boundary + +- `allowed_tools` is the script's internal capability boundary; keep it minimal. +- Unknown `kind`, workspace-escaping paths, duplicate tool names, and illegal limits should fail at load time. +- Do not let untrusted directories declare high-privilege MCP servers or scripts. +- High-risk tools should still use HITL, allow-lists, and audit. + +## Current Scope + +`tools/` is installed by `serve_agent_dir` per schedule session. It serves durable agents and recurring work. Normal interactive sessions should use host direct tools, MCP connections, or SDK `session.tool(...)` registration. + +If a capability is a project-wide connector, prefer host config or MCP. If it belongs only to one durable scheduled agent, put it in that AgentDir's `tools/`. diff --git a/website/docs/v6/en/guide/hooks.mdx b/website/docs/v6/en/guide/hooks.mdx new file mode 100644 index 00000000..80b4bf15 --- /dev/null +++ b/website/docs/v6/en/guide/hooks.mdx @@ -0,0 +1,56 @@ +--- +title: 'Hooks' +description: 'Lifecycle interception and policy callbacks' +--- + +# Hooks + +Hooks register lifecycle callbacks inside a session. The registration lifecycle +is `registerHook()`, `hookCount()`, and `unregisterHook()`. + +## Events + +Supported event types are: + +```text +pre_tool_use +post_tool_use +generate_start +generate_end +session_start +session_end +skill_load +skill_unload +pre_prompt +post_response +on_error +``` + +## Registration Example + +```ts +session.registerHook( + 'release-publish-observer', + 'pre_tool_use', + { tool: 'bash', commandPattern: 'npm publish|twine upload|cargo publish' }, + { priority: 50, timeoutMs: 1000 }, + () => ({ action: 'continue' }), +); +``` + +A handler may return `{ action: 'continue' }`, `{ action: 'skip' }`, +`{ action: 'block', reason }`, or `null`/`undefined` to continue. Validate the +specific event path you depend on before using a hook as a production gate. + +## Propagation + +Delegation and automatic subagent fan-out use the `task` and `parallel_task` +tools. When a product depends on hook behavior across delegated runs, cover that +product path with an integration test. + +## Management + +```ts +console.log(session.hookCount()); +session.unregisterHook('release-publish-observer'); +``` diff --git a/website/docs/v6/en/guide/index.mdx b/website/docs/v6/en/guide/index.mdx new file mode 100644 index 00000000..203ef645 --- /dev/null +++ b/website/docs/v6/en/guide/index.mdx @@ -0,0 +1,196 @@ +--- +title: 'Overview' +description: 'Rust coding-agent runtime and the execution core behind the a3s code terminal workspace' +--- + +# A3S Code + +A3S Code is the Rust runtime behind `a3s code`. It keeps coding-agent sessions +observable while one runtime kernel owns context assembly, governed provider +and tool invocation, delegation, dynamic workflow execution, workspace access, +memory, persistence, verification evidence, and run replay. + +`a3s-code-core` is the embeddable Rust runtime. `a3s code` is the ready terminal +workspace shipped by the [`a3s` CLI](https://github.com/A3S-Lab/Cli); it drives +A3S Code sessions and renders their event stream with the +[`a3s-tui`](https://github.com/A3S-Lab/TUI) framework. + +Install the CLI when you want the interactive product. Install the Rust crate +when you are building another host, runner, IDE bridge, or controlled product +surface around the same runtime. + +## Surfaces + +| Name | Use it for | Repository | +| --------------- | ----------------------------------------------------------------------------- | ----------------------------------------------- | +| `a3s-code-core` | Embedding coding-agent sessions through the Rust runtime API. | [A3S-Lab/Code](https://github.com/A3S-Lab/Code) | +| `a3s code` TUI | Running the ready terminal coding-agent workspace. | [A3S-Lab/Cli](https://github.com/A3S-Lab/Cli) | +| `a3s-tui` | Building terminal UI surfaces; it is the UI framework, not the agent runtime. | [A3S-Lab/TUI](https://github.com/A3S-Lab/TUI) | +| A3S Flow | Durable workflow engine used by `DynamicWorkflowRuntime`. | [A3S-Lab/Flow](https://github.com/A3S-Lab/Flow) | +| A3S monorepo | Product docs, release orchestration, submodule pins, and related crates. | [A3S-Lab/a3s](https://github.com/A3S-Lab/a3s) | + +## Capability Map + +| Area | Current capability | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Agent sessions | Async-first `SessionBuilder` construction produces workspace-bound `AgentSession` values. Transcript operations are fail-fast single-flight and expose `send`, `stream`, run state, cancellation, persistence, memory, verification, and lifecycle cleanup. | +| TUI | [`a3s code`](/guide/tui) streams runtime events into a terminal workspace with tool activity, approvals, memory, files, sessions, local assets, effort profiles, DynamicWorkflowRuntime, and trusted view controls. | +| Filesystem-first | [Filesystem-first](/guide/filesystem-first) organizes `AGENTS.md`, ACL config, `.a3s/agents/`, `skills/`, `tools/`, and `schedules/` into a reviewable agent product interface. | +| Tools | Stable tool names cover files, search, shell, git, web fetch/search, structured output, QuickJS PTC programs, skills, MCP tools, child-task delegation, and native host-side `parallel_task`. Model, nested, delegated, and trusted host-direct calls use one invocation kernel with explicit origin policy. | +| Dynamic workflows | `DynamicWorkflowRuntime` uses A3S Flow to record replayable per-turn workflow and step history for `ultracode` and DeepResearch. It is separate from `/flow`, which manages workflow asset files and optional host integrations. | +| Delegation | Built-in roles and custom Markdown/YAML agents are available through `task`, `parallel_task`, and automatic delegation controls. | +| Memory | Default file-backed memory, recall, LLM extraction, `/memory`, `/ctx`, `/ctx save`, and `/sleep` support cross-session facts without flooding every prompt. | +| Safety | One run invocation context propagates identity, cancellation, events, and governance. Permission policies, HITL confirmation, budgets, workspace path checks, tool timeouts, hooks, and output sanitization feed the same provider/tool boundaries. | +| Workspaces | [Workspace Backends](/guide/workspace-backends) let built-in tools target local files, host-provided workspaces, optional S3-compatible storage, and optional remote-git services. | +| Events | `EventEnvelopeV1` is a lossless Rust/Node/Python wire shape with an open event type and preserved payload/metadata for future events. | +| Persistence | Atomic `SessionSnapshotV1` generations, memory/file stores, session IDs, auto-save, run snapshots/events, trace artifacts, loop/workflow checkpoints, memory recall, and retention caps support resumable sessions and replayable product state. | +| Verification | [Verification](/guide/verification) covers explicit verification commands, presets, structured reports, summaries, artifacts, trace events, and run replay evidence. | + +The execution shape is: + +```text +Agent / AgentSession + -> context assembly + -> optional planning and goal tracking + -> selected tools, delegated child tasks, or dynamic workflow steps + -> permission and confirmation policy + -> execution + -> trace, artifacts, memory, and verification evidence + -> compaction and persistence +``` + +## Install + +Install the CLI when you want the interactive terminal workspace: + +```bash +brew install A3S-Lab/tap/a3s + +# or from crates.io +cargo install a3s + +# or from the CLI repository +cargo install --git https://github.com/A3S-Lab/Cli +``` + +Install the Rust runtime crate when embedding A3S Code: + +```bash +cargo add a3s-code-core +``` + +## Configure + +A3S Code uses ACL. Keep real API keys, private model endpoints, local config +paths, and tenant/user identifiers out of commits. Commit templates that resolve +credentials from the environment. + +```text +default_model = "provider/model-id" +max_parallel_tasks = 4 +auto_parallel = false + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "model-id" { + tool_call = true + limit = { + context = 128000 + output = 4096 + } + } +} + +storage_backend = "file" +sessions_dir = ".a3s/sessions" +``` + +`auto_parallel = false` disables automatic parallel child-agent fan-out only. +Manual `parallel_task` remains available unless manual delegation is disabled +separately. + +## Use The TUI + +Run `a3s code` from the workspace you want the agent to inspect: + +```bash +a3s code +a3s code resume +a3s code update +``` + +The TUI discovers config from `A3S_CONFIG_FILE`, then `.a3s/config.acl` walking +upward from the current directory, then `~/.a3s/config.acl`. + +Common first-run flow: + +```text +/init # inspect the repository and create or update AGENTS.md +/model # pick a configured provider or account-backed model +/effort # choose low, medium, high, xhigh, max, or ultracode +/ide # open the workspace tree and terminal editor +/help # open the full command and shortcut guide +``` + +## Rust Runtime Quick Start + +```rust +use a3s_code_core::{Agent, AgentEvent, SessionOptions}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let agent = Agent::new("agent.acl").await?; + let session = agent + .session_builder("/path/to/workspace") + .options( + SessionOptions::new() + .with_planning(true) + .with_max_parallel_tasks(4) + .with_tool_timeout(120_000), + ) + .build() + .await?; + + let result = session + .send("Find the authentication entry points.", None) + .await?; + println!("{}", result.text); + + let (mut rx, _handle) = session + .stream("Summarize the test strategy.", None) + .await?; + while let Some(event) = rx.recv().await { + match event { + AgentEvent::TextDelta { text } => print!("{text}"), + AgentEvent::End { .. } => break, + _ => {} + } + } + + Ok(()) +} +``` + +Rust construction is async-first. The synchronous `Agent::session` method is a +strict compatibility path for an explicitly pre-initialized memory store and +other already-ready resources; async-only options return +`CodeError::AsyncSessionBuildRequired`. A session-option MCP manager always uses +the async path for capability discovery. + +Direct Rust host tool calls are privileged control-plane operations. Gate them +in the embedding application before exposing them to users. + +## Entry Points + +- [A3S Code TUI](/guide/tui) explains install, config discovery, slash commands, effort profiles, dynamic workflows, optional runtime tools, and trusted views. +- [A3S CLI Code TUI](https://a3s-lab.github.io/a3s/docs/cli/code-tui) covers CLI startup, resume, update, smoke mode, and terminal lifecycle boundaries. +- [Filesystem-first](/guide/filesystem-first) covers reviewable project and AgentDir conventions. +- [Sessions](/guide/sessions) covers lifecycle, streaming, run state, persistence, and cancellation. +- [Commands](/guide/commands) distinguishes TUI commands from host command-registry handlers. +- [Tools](/guide/tools) covers direct tools, typed tool errors, structured output, and QuickJS programs. +- [Memory](/guide/memory) and [Persistence](/guide/persistence) cover reusable facts, session snapshots, and recovery paths. +- [Security](/guide/security) covers permissions, confirmations, hooks, and verification gates. +- [Verification](/guide/verification) covers command evidence, reports, and summaries. +- [Tasks](/guide/tasks) covers model-driven child-task delegation. diff --git a/website/docs/v6/en/guide/isolation.mdx b/website/docs/v6/en/guide/isolation.mdx new file mode 100644 index 00000000..cc91e425 --- /dev/null +++ b/website/docs/v6/en/guide/isolation.mdx @@ -0,0 +1,44 @@ +--- +title: 'Isolation' +description: 'Workspace, context, and delegated-task isolation' +--- + +# Isolation + +Isolation starts with the session boundary: each session binds to one +workspace, and each delegated child run receives bounded context. Direct host +tool calls are privileged host operations; gate them in the host application +before exposing them to users. + +## Workspace Boundary + +```ts +const session = agent.session('/repo'); +``` + +Relative file, search, shell, and git operations are evaluated from the session +workspace. Security providers and hooks are session options; validate the exact +policy path you depend on before using them as a production boundary. + +## Delegated Context + +`task`, `parallel_task`, and automatic subagent delegation isolate child reasoning. The parent receives compact results instead of full transcripts. This avoids prompt pollution and makes evidence review easier. + +## Storage Isolation + +Use separate memory and session store directories for different products, tenants, or test suites: + +```ts +import { FileMemoryStore, FileSessionStore } from '@a3s-lab/code'; + +const session = agent.session('/repo', { + memoryStore: new FileMemoryStore('./.a3s/memory'), + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); +``` + +## External Harness + +Hooks and permission policy are exposed as integration points. Test live +organization policy with your own harness before treating it as a production +boundary. diff --git a/website/docs/v6/en/guide/lane-queue.mdx b/website/docs/v6/en/guide/lane-queue.mdx new file mode 100644 index 00000000..7080024b --- /dev/null +++ b/website/docs/v6/en/guide/lane-queue.mdx @@ -0,0 +1,50 @@ +--- +title: 'Lane Queue' +description: 'Optional external and hybrid dispatch infrastructure' +--- + +# Lane Queue + +Ordinary sessions are queue-free. Lane queues are optional advanced infrastructure for applications that need external or hybrid dispatch. They are separate from `task`, `parallel_task`, and automatic subagent delegation. + +## When To Use It + +Use a queue when you need: + +- external workers to complete selected tasks +- hybrid routing between local and remote executors +- queue metrics, dead letters, or dispatch observability +- multi-machine work distribution + +## Configure A Session + +```ts +const session = agent.session('/repo', { + queueConfig: { + enableDlq: true, + enableMetrics: true, + }, +}); + +await session.setLaneHandler('execute', { + mode: 'external', + timeoutMs: 300000, +}); +``` + +Lane names are `control`, `query`, `execute`, and `generate`. + +## External Completion + +```ts +const pending = await session.pendingExternalTasks(); + +if (pending.length > 0) { + await session.completeExternalTask(pending[0].id, { + success: true, + result: { summary: 'completed by worker' }, + }); +} +``` + +Keep queue use explicit. Do not introduce it for normal local agent sessions. diff --git a/website/docs/v6/en/guide/limits.mdx b/website/docs/v6/en/guide/limits.mdx new file mode 100644 index 00000000..6dddb549 --- /dev/null +++ b/website/docs/v6/en/guide/limits.mdx @@ -0,0 +1,173 @@ +--- +title: 'Limits' +description: 'Runtime limits, compaction, timeouts, and circuit breakers' +--- + +# Limits + +Limit options are session-level controls for long-running work, noisy tools, and +provider failures. + +## Session Options + +```ts +const session = agent.session('/repo', { + maxToolRounds: 24, + maxParseRetries: 3, + toolTimeoutMs: 120000, + circuitBreakerThreshold: 4, + autoCompact: true, + autoCompactThreshold: 0.75, + continuationEnabled: true, + maxContinuationTurns: 3, +}); +``` + +## Option Intent + +- `maxToolRounds` is the tool-iteration budget for a turn. +- `maxParseRetries` is the malformed tool-call recovery budget. +- `toolTimeoutMs` is the per-tool timeout in milliseconds. +- `circuitBreakerThreshold` is the repeated provider failure threshold. +- `autoCompact` and `autoCompactThreshold` enable context compaction behavior. +- `continuationEnabled` and `maxContinuationTurns` control continuation injection. + +## Practical Defaults + +Use strict limits for CI, release, and user-facing automation. Use larger budgets for exploratory local coding sessions, but keep verification commands explicit and required when the task has side effects. + +## Retention Limits + +A session keeps its run history, trace events, and subagent task snapshots in +memory. For short runs that is fine; for a session that lives for hours or days +under a cluster workload those in-memory stores grow without bound. +`SessionRetentionLimits` (CHANGELOG [3.3.0] "SessionRetentionLimits") adds +optional FIFO caps so they don't leak. **The default is unbounded — setting no +cap changes no behavior.** + +Four independent caps; cap any subset, leave the rest unbounded: + +| Field | Effect when capped | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `max_runs_retained` | When a new run pushes past the cap, the **oldest** run and all of its events are dropped. | +| `max_events_per_run` | The oldest events in a run are FIFO-dropped. The run snapshot's `event_count` is **not** decremented — it stays the cumulative total ever recorded. | +| `max_trace_events` | The oldest event in the trace sink is dropped on each new write past the cap. | +| `max_terminal_subagent_tasks` | The oldest **terminal** (completed / failed / cancelled) subagent task snapshot is dropped past the cap. **Running tasks are never dropped.** | + +All caps are soft: enforcement drops the oldest entry on insert and never +returns an error. + +```ts +const session = agent.session('/repo', { + retentionLimits: { + maxRunsRetained: 100, + maxEventsPerRun: 5000, + maxTraceEvents: 20000, + maxTerminalSubagentTasks: 500, + }, +}); +``` + +```python +opts = SessionOptions() +opts.retention_limits = { + 'max_runs_retained': 100, + 'max_events_per_run': 5000, + 'max_trace_events': 20000, + 'max_terminal_subagent_tasks': 500, +} +session = agent.session('/repo', opts) +``` + +## Budget Guard + +`BudgetGuard` (CHANGELOG [3.3.0] "BudgetGuard") is a host-supplied cost / quota +contract. The framework does not enforce budgets itself — it defines the +decision points and consults a guard the host plugs in. Three hooks are wired at +the LLM / tool call site: + +- `check_before_llm` — before each LLM call. +- `record_after_llm` — after each successful LLM call, with the actual provider + usage, so the host keeps its running spend total accurate. +- `check_before_tool` — before each tool call. + +Each `check_*` returns one of three decisions: + +- `Allow` — proceed normally, no event. +- `SoftLimit { resource, consumed, limit, message }` — emits an + `AgentEvent::BudgetThresholdHit { kind: "soft" }` and **proceeds**. In-session + hooks can react (auto-compact, swap to a cheaper model next turn). +- `Deny { resource, reason }` — aborts the call with + `CodeError::BudgetExhausted`. **The session stays open** — the caller can retry + later or after the host re-allocates budget. + +### Node — `session.setBudgetGuard({...})` + +Each callback takes a **single `ctx` object** (not positional arguments) and +returns a decision dict (or `null` / `{ decision: 'allow' }` to allow): + +```ts +session.setBudgetGuard({ + checkBeforeLlm: (ctx) => { + // ctx.sessionId, ctx.estimatedTokens + if (overMonthlyCap(ctx.sessionId)) { + return { + decision: 'deny', + resource: 'llm_tokens', + reason: 'monthly cap', + }; + } + return { decision: 'allow' }; + }, + recordAfterLlm: (ctx) => { + // ctx.sessionId, ctx.usage — usage keys are camelCase: + // promptTokens, completionTokens, totalTokens, cacheReadTokens, cacheWriteTokens + addSpend(ctx.sessionId, ctx.usage.totalTokens); + }, + checkBeforeTool: (ctx) => { + // ctx.sessionId, ctx.toolName + return { decision: 'allow' }; + }, + timeoutMs: 5000, // optional, default 5000 +}); +``` + +The Node bridge **fails closed**: a `check_*` callback that does not return +within `timeoutMs`, or returns something unreadable, is treated as a **deny** — +a budget control must never silently disable itself when the guard stalls +(CHANGELOG [3.3.0] Fixed "Node BudgetGuard fail-open"). + +The callback **MUST NOT throw.** Due to a napi-rs constraint a thrown exception +aborts the host process at return-value conversion. Wrap your logic in +try/catch and return a decision (e.g. a deny) instead of throwing. Hangs are +handled safely by the fail-closed timeout (CHANGELOG [3.3.0] Known limitations). + +### Python — `budget_guard` session option + +Python supplies a `BudgetGuard`-shaped object on the `budget_guard` +`SessionOptions` field. Methods that aren't defined behave as Allow / no-op. +Python callbacks use **positional arguments** and the framework catches any +exception they raise (a `check_*` that raises defaults to Allow): + +```python +class MyBudgetGuard: + def check_before_llm(self, session_id, est_tokens): + if over_monthly_cap(session_id): + return {'decision': 'deny', 'resource': 'llm_tokens', 'reason': 'monthly cap'} + return {'decision': 'allow'} + + def record_after_llm(self, session_id, usage): + # usage is a dict with snake_case keys: + # total_tokens, cache_read_tokens (plus prompt_tokens, completion_tokens, cache_write_tokens) + add_spend(session_id, usage['total_tokens']) + + def check_before_tool(self, session_id, tool_name): + return {'decision': 'allow'} + +opts = SessionOptions() +opts.budget_guard = MyBudgetGuard() +session = agent.session('/repo', opts) +``` + +The decision return dict `{"decision": "deny", "resource": ..., "reason": ...}` +(and `"soft"` / `"allow"`) is the same shape on both SDKs. diff --git a/website/docs/v6/en/guide/mcp.mdx b/website/docs/v6/en/guide/mcp.mdx new file mode 100644 index 00000000..8a23c4ab --- /dev/null +++ b/website/docs/v6/en/guide/mcp.mdx @@ -0,0 +1,124 @@ +--- +title: 'MCP' +description: 'Model Context Protocol server integration' +--- + +# MCP + +MCP connects A3S Code sessions to external tool servers. A stdio server can be +added to a live session, inspected, called through its registered tools, and +removed. Tools from a server are registered into the session and named +`mcp____`. + +## Add A Server + +Use the object-shaped `addMcp(...)` API for new code. It maps directly to the +core `McpServerConfig` shape and avoids the positional overload's argument +ordering. + +```ts +const count = await session.addMcp({ + name: 'echo', + transport: { + type: 'stdio', + command: process.execPath, + args: ['tools/mcp_echo_server.mjs', 'example-value'], + }, + timeoutMs: 30000, +}); + +console.log('registered tools:', count); +``` + +Python exposes the same object-shaped API as `session.add_mcp({...})`. +`addMcpServer(...)` / `add_mcp_server(...)` and +`addMcpServerConfig(...)` / `add_mcp_server_config(...)` remain compatibility +aliases. + +For remote servers, use a nested transport object with `type: 'http'` or +`type: 'streamable-http'`. Supply credentials from the host environment or a +secret manager, and validate the server before relying on it in production docs +or release notes. + +## Inspect And Remove + +```ts +console.log(await session.mcpStatus()); +console.log(session.toolNames().filter((name) => name.startsWith('mcp__'))); +await session.tool('mcp__echo__echo', { message: 'docs mcp ok' }); +await session.removeMcpServer('echo'); +``` + +## Ownership And Session Isolation + +MCP capability discovery and live mutation have different owners: + +- The agent-global manager owns servers loaded from global configuration. +- A manager supplied through Rust `SessionOptions` is an inherited, read-only + capability source for that session. +- Every built session owns a new private manager for live `addMcp` and + `removeMcpServer` operations. + +Session assembly reads tool definitions from inherited sources without merging +session configuration back into those managers. A locally added server can +shadow an inherited server with the same fully qualified tool names, but only +inside that session. Removing the local server reveals the inherited tools +again. It does not unregister, disconnect, or otherwise mutate the global or +host-owned source, and sibling sessions remain unchanged. + +Delegated child agents receive the ordered capability sources so they can call +the same MCP tools without taking ownership. `session.close()` disconnects only +the private manager's servers. `agent.close()` and `disconnectIdleMcp(...)` +remain responsible for the agent-global manager. + +For Rust, a host-supplied MCP source requires async session construction so +discovery happens without blocking: + +```rust +let session = agent + .session_builder("/repo") + .options(SessionOptions::new().with_mcp(manager)) + .build() + .await?; +``` + +## Idle Disconnect + +A connected stdio MCP server holds resources — file descriptors and a +background worker — even while it sits idle. In a long-lived cluster session +those quiet servers accumulate. `disconnectIdleMcp` (CHANGELOG [3.3.0] "MCP idle +disconnect") reaps them on demand: it disconnects every global MCP server whose +last activity is older than the threshold, releasing the FDs and workers, **while +keeping each server's registered config** so a later tool call reconnects on +demand. + +This is an agent-level method (it operates only on the agent's global MCP manager, +backed by `McpManager::disconnect_idle(threshold_ms)`). It returns the list of +disconnected server names. + +```ts +// Reap servers idle longer than 5 minutes. Returns disconnected names. +const dropped = await agent.disconnectIdleMcp(5 * 60 * 1000); +console.log('disconnected:', dropped); +``` + +```python +# Reap servers idle longer than 5 minutes. Returns disconnected names. +dropped = agent.disconnect_idle_mcp(5 * 60 * 1000) +print('disconnected:', dropped) +``` + +Hosts running thousands of long-lived sessions should call this periodically +from a sweeper (e.g. every 60s with a 5-min threshold). A subsequent tool call +on a disconnected server re-establishes the connection from its retained config +— no re-registration needed. + +Disconnect also purges orphan timestamps: a `touch()`-without-connect entry +(recorded for a server that was never actually connected) is swept on each +`disconnect_idle` call so the activity map cannot grow unbounded over a +long-running manager's lifetime (CHANGELOG [3.3.0] Fixed "MCP timestamp leak"). + +## Security + +Treat external tool servers as privileged integrations and keep secrets in +environment variables. diff --git a/website/docs/v6/en/guide/memory.mdx b/website/docs/v6/en/guide/memory.mdx new file mode 100644 index 00000000..e7d4f782 --- /dev/null +++ b/website/docs/v6/en/guide/memory.mdx @@ -0,0 +1,114 @@ +--- +title: 'Memory' +description: 'Session memory stores and explicit recall' +--- + +# Memory + +Memory records reusable facts about previous work. It should help the harness recall patterns without flooding every prompt. + +## Default Store + +Every session gets a memory store by default. Plain SDK sessions use a +file-backed store at `/.a3s/memory` if you do not pass one. The +`a3s code` TUI uses the same `memory_dir` setting that its `/memory` panel +browses; by default that is `~/.a3s/memory`, so memories carry across projects +unless you configure a project-local path. Set `memory_dir` in config or pass a +typed store object to override the backend for one session. If the file store +cannot be created, the session falls back to an in-memory store and exposes an +init warning. + +## Override Stores + +```ts +import { FileMemoryStore } from '@a3s-lab/code'; + +const session = agent.session('/repo', { + memoryStore: new FileMemoryStore('./.a3s/memory'), +}); +``` + +## LLM Extraction + +LLM memory extraction is enabled by default when memory is available. The agent +submits every completed non-empty session turn to the active model. The model, +not a keyword list or tool-type heuristic, decides whether the turn contains +anything that can change a future answer or action. It must return an empty +`items` array when nothing qualifies. Tool calls and tool results are context +for that judgment; they are never mechanically copied into long-term memory. + +Automatic output is deliberately narrow. The runtime accepts only `semantic` +memories for durable facts, preferences, and decisions, or `procedural` +memories for reusable workflows and failure lessons. Each item must include a +validated `source`, `importance`, `confidence`, `scope`, and a non-empty +future-value `reason`. Stored metadata also records the workspace, session ID, +and `a3s.memory.durable.v1` schema. The runtime rejects malformed output and +obvious API keys, tokens, password assignments, and private keys; these checks +validate structure and safety rather than judging semantic value. + +The extraction prompt includes a small set of related existing memories. The +model can use `supersedes` for a directly replaced or consolidated memory and +`conflicts_with` for a contradiction that should remain visible. The runtime +accepts only relation IDs it supplied to the model. It removes accepted +superseded items, preserves conflicts, and includes relation annotations in +future recall. The storage layer otherwise merges only normalized exact +duplicates; it does not infer semantic equivalence from term overlap. + +Streaming hosts such as the TUI and Code Web register extraction before they +publish the final event, then process completed turns in FIFO order in the +background. A slow extraction does not delay the final response, and a later +turn is queued rather than dropped. Graceful session close waits up to its +bounded extraction deadline for work accepted before close, preventing an +immediate UI shutdown from silently losing the completed turn. + +`rememberSuccess` and `rememberFailure` remain explicit SDK operations for a +host that intentionally wants to store those records. The normal agent runtime +does not call them for each tool result. + +## Store Hygiene + +Default memory stores return the canonical item for normalized exact duplicate +content, raising importance and preserving useful tags, provenance, and +relation metadata. Distinct but related wording remains separate unless the LLM +explicitly consolidates it through `supersedes`. + +Automatic pruning removes stale, low-importance memories when configured, but it +hard-protects curated memories: pinned/protected items, frequently recalled +items, consolidated memories, and memories carrying `supersedes` or +`conflicts_with` relation metadata. + +You can tune extraction limits in `config.acl`: + +```text +memory { + llmExtraction = true + llmExtractionMaxItems = 5 + llmExtractionMaxInputChars = 8000 +} +``` + +## Write Memory + +```ts +await session.rememberSuccess( + 'release preflight', + ['bash', 'grep'], + 'Release checks passed after provider verification', +); + +await session.rememberFailure( + 'provider verification', + 'missing PROVIDER_BASE_URL', + ['bash'], +); +``` + +## Recall + +```ts +const related = await session.recallSimilar('release provider verification', 5); +const recent = await session.memoryRecent(10); +const tagged = await session.recallByTags(['release'], 10); +``` + +Use recalled memory as supporting context. Verification evidence still comes from current commands and traces. diff --git a/website/docs/v6/en/guide/multi-machine.mdx b/website/docs/v6/en/guide/multi-machine.mdx new file mode 100644 index 00000000..eb9d793f --- /dev/null +++ b/website/docs/v6/en/guide/multi-machine.mdx @@ -0,0 +1,256 @@ +--- +title: 'Multi-Machine' +description: 'Placing orchestration steps across machines via the AgentExecutor seam' +--- + +# Multi-Machine + +A3S Code runs multi-agent orchestration as a _grammar_ expressed in code, then +places the resulting steps wherever you want them to run. The split is drawn +along the **framework / host boundary**, introduced in `[3.4.0]`: + +- The **framework** owns the orchestration grammar and the serializable data + contracts. It never decides where a step runs. +- The **host** owns placement, transport, and scheduling — which node executes + a step, how the spec gets there, and how concurrency maps to a cluster. + +The single point of contact between the two is one trait, `AgentExecutor`. + +## The framework / host boundary + +The framework's contract is two serializable types: + +- `AgentStepSpec` — _what_ to run, independent of _where_: `task_id`, `agent`, + `description`, `prompt`, and optional `max_steps`, `parent_session_id`, + `output_schema`. +- `StepOutcome` — the result of running one spec: `task_id`, `session_id`, + `agent`, `output`, `success`, and optional `structured`. + +Both serialize cleanly, so a host may ship a spec to another node and persist an +outcome in a checkpoint. The combinators that compose specs are written purely +against the `AgentExecutor` trait and never observe where a step ran, so the +same orchestration scales from one process to a cluster without changing. + +## The AgentExecutor seam + +`AgentExecutor` is the boundary between the grammar and the host: + +```text +combinators (parallel / pipeline / resumable) + -> AgentExecutor::execute_step(spec) -> StepOutcome + ├─ in-box TaskExecutor: runs the step as a local child agent + └─ host executor: places the step on a remote node +``` + +The in-box `TaskExecutor` runs each step as a child agent locally — in-process, +on Tokio — inheriting the session's agent registry, LLM client, workspace, MCP +tools, and subagent tracker. A host such as a cluster runtime substitutes its own executor +to place steps across a cluster; the combinators are unaffected. + +`concurrency_hint()` is **advisory, not a hard local bound**. The local default +returns the session's `max_parallel_tasks`; a scheduler-backed host may return +its cluster-wide target. Because it is a hint rather than a ceiling, +orchestration scales past a single process. + +A session exposes the in-box seam directly: + +- `AgentSession::agent_executor()` returns a session-backed `AgentExecutor`. +- `AgentSession::session_store()` returns the session's store (when one is + configured), which the resumable combinator needs to journal progress. + +The SDK grammar below calls `agent_executor()` for you; you only reach for these +when implementing or substituting a custom executor. + +## Parallel: barrier fan-out + +`execute_steps_parallel` fans `specs` out across the executor and awaits all of +them (a barrier). Results preserve input order, a panicked branch becomes a +failed `StepOutcome` instead of dropping the batch, and concurrency is bounded +by the executor's concurrency hint. + +```ts +const outcomes = await session.parallel([ + { + taskId: 'a', + agent: 'explore', + description: 'survey', + prompt: 'Map the auth module', + }, + { + taskId: 'b', + agent: 'review', + description: 'audit', + prompt: 'Review error handling', + }, +]); + +for (const o of outcomes) { + console.log(o.taskId, o.success, o.output); +} +``` + +```python +outcomes = session.parallel([ + {"task_id": "a", "agent": "explore", "description": "survey", "prompt": "Map the auth module"}, + {"task_id": "b", "agent": "review", "description": "audit", "prompt": "Review error handling"}, +]) + +for o in outcomes: + print(o["task_id"], o["success"], o["output"]) +``` + +## Pipeline: per-item chains, no inter-stage barrier + +`execute_pipeline` runs each item through a chain of `PipelineStage`s +independently. There is **no barrier between stages** — item A can be in stage 3 +while item B is still in stage 1 — so wall-clock is the slowest single chain, +not the sum of the slowest step per stage. + +A stage is a `(ctx) => spec | null` callback where `ctx` carries the previous +outcome and the original item. Return a spec to run the next step, or `null` to +stop that item's chain early; a chain also stops when a step fails (later stages +would only build on a failed result). The bridges **fail closed**: a stage that +hangs, returns `null`, or raises stops only its own chain. + +> A Node stage callback **must not throw** — wrap your logic in `try`/`catch` +> and return `null` on error (the same constraint as `setBudgetGuard`). A stage +> that hangs past the timeout fails closed for that chain. A Python stage that +> raises is caught and treated as `null`. + +```ts +const outcomes = await session.pipeline( + ['src/auth', 'src/api'], + [ + (ctx) => ({ + taskId: 's1', + agent: 'explore', + description: 'survey', + prompt: `Survey ${ctx.item}`, + }), + (ctx) => + ctx.previous?.success + ? { + taskId: 's2', + agent: 'review', + description: 'review', + prompt: `Review: ${ctx.previous.output}`, + } + : null, + ], +); +``` + +```python +def survey(ctx): + return {"task_id": "s1", "agent": "explore", "description": "survey", + "prompt": f"Survey {ctx['item']}"} + +def review(ctx): + prev = ctx["previous"] + if prev and prev["success"]: + return {"task_id": "s2", "agent": "review", "description": "review", + "prompt": f"Review: {prev['output']}"} + return None + +outcomes = session.pipeline(["src/auth", "src/api"], [survey, review]) +``` + +## Resumable: cross-node resume + +`execute_steps_parallel_resumable` is `parallel` plus a journal. At each step +boundary it writes a `WorkflowCheckpoint` to the `SessionStore` under a +`workflowId`. On resume it skips already-completed steps and re-dispatches the +rest. It records **only successful steps**, so a failed step retries on resume — +its effect did not complete. + +A `SessionStore` is required. The Node bridge rejects with +`"parallelResumable requires a sessionStore"` when none is configured; Python +raises the equivalent. + +```ts +import { Agent, FileSessionStore } from '@a3s-lab/code'; + +const session = agent.session('/repo', { + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); + +const outcomes = await session.parallelResumable( + [ + { + taskId: 'a', + agent: 'explore', + description: 'survey', + prompt: 'Map the auth module', + }, + { + taskId: 'b', + agent: 'review', + description: 'audit', + prompt: 'Review error handling', + }, + ], + 'release-audit', +); +``` + +```python +from a3s_code import Agent, FileSessionStore, SessionOptions + +opts = SessionOptions() +opts.session_store = FileSessionStore("./.a3s/sessions") +session = agent.session("/repo", opts) + +outcomes = session.parallel_resumable([ + {"task_id": "a", "agent": "explore", "description": "survey", "prompt": "Map the auth module"}, + {"task_id": "b", "agent": "review", "description": "audit", "prompt": "Review error handling"}, +], "release-audit") +``` + +Because the checkpoint is serializable and the executor is a parameter, a host +can resume an interrupted workflow on a **different node** by passing that node's +executor — the framework migrates the _what_, the host supplies the _where_. + +## Schema-forced step output + +A spec carrying an `output_schema` (`outputSchema` in Node) must return a value +conforming to that JSON Schema; the validated object lands in +`StepOutcome.structured`. The executor reuses the structured-output coercion and +repair machinery. A coercion failure **demotes the step to unsuccessful**, so a +caller never treats unvalidated text as the promised object. + +```ts +const [finding] = await session.parallel([ + { + taskId: 'classify', + agent: 'review', + description: 'classify', + prompt: 'Classify this defect', + outputSchema: { + type: 'object', + properties: { severity: { type: 'string' }, summary: { type: 'string' } }, + required: ['severity', 'summary'], + }, + }, +]); + +if (finding.success && finding.structured) { + console.log(finding.structured.severity); +} +``` + +## Placing orchestration steps across machines + +Lane queues remain a valid transport — but they are now **one option behind the +`AgentExecutor` seam, not the sole integration point**. To distribute work, a +host implements `AgentExecutor::execute_step` over whatever transport fits its +platform (HTTP, message queues, a job system, lane queues, internal RPC), wires +`concurrency_hint()` to its cluster target, and hands that executor to the +combinators. The grammar — parallel, pipeline, resumable — stays identical; only +placement moves. + +The result: a coordinator session owns the conversation, final synthesis, and +release decision, while its orchestration steps execute wherever the host places +them, with cross-node resume carried by the serializable checkpoint. + +See [Orchestration](/guide/orchestration) for the combinator grammar in +depth and [Persistence](/guide/persistence) for the checkpoint store. diff --git a/website/docs/v6/en/guide/orchestration.mdx b/website/docs/v6/en/guide/orchestration.mdx new file mode 100644 index 00000000..fdc5faf4 --- /dev/null +++ b/website/docs/v6/en/guide/orchestration.mdx @@ -0,0 +1,470 @@ +--- +title: 'Orchestration' +description: 'Programmable, deterministic multi-agent orchestration: parallel fan-out, barrier-free pipelines, and resumable workflows' +--- + +# Orchestration + +Orchestration is the _programmable_ sibling of model-driven delegation. With +[Tasks](/guide/tasks) and [Teams](/guide/teams) the LLM decides, at +run time, to call `task` / `parallel_task` — the shape of the fan-out is +whatever the model chose. Orchestration moves that decision into your code: you +express fan-out, pipelines, and verification panels as a grammar, so the shape +is reproducible, testable, budget-bounded, and resumable — independent of what +the model picks. + +Reach for orchestration when the _structure_ of the work is known to the host +ahead of time (run these three reviewers in parallel; flow each candidate +through explore → verify → review; resume this batch after a crash). Reach for +Tasks/Teams when you want the model to decide whether and how to delegate. + +## The framework / host boundary (the seam) + +Everything in this layer is written against a single seam, `AgentExecutor`: +"run this step, give me the result." That seam splits responsibilities cleanly: + +- The **framework** owns the _grammar_ — which steps exist, how they compose, + the concurrency _hint_, and the serializable contracts `AgentStepSpec` / + `StepOutcome`. +- The **host** owns _placement_ — transport, scheduling, and where a + step actually runs. + +The in-box default executor (`TaskExecutor`) runs every step locally, +in-process, on tokio. A host substitutes its own `AgentExecutor` to place steps +across a cluster; the combinators never observe where a step ran, so the same +orchestration scales from one process to a cluster without change. + +`concurrency_hint()` is **advisory**, not a hard local bound — it is the lever +that lets orchestration scale past a single process (a scheduler-backed host +returns its cluster-wide target instead of a local cap). + +The SDK wires this up for you: `AgentSession::agent_executor()` returns the +session-backed executor (it runs each step as a child agent on this node, +inheriting the session's agent registry, LLM client, workspace, and MCP tools), +and `session_store()` returns the session's store. The `parallel` / +`pipeline` / `parallelResumable` methods call these for you. + +## Step contracts + +A step is described by an `AgentStepSpec` and resolves to a `StepOutcome`. Both +are serializable on purpose: a host may ship a spec to another node, and the +resumable combinator persists outcomes into checkpoints. + +`AgentStepSpec` fields: + +- `task_id` — stable id for the step (you assign it); flows into lifecycle + events and checkpoints. +- `agent` — registry key of the agent to run (e.g. `explore`, `review`). +- `description` — short human label for display/tracking. +- `prompt` — the instruction handed to the child agent. +- `max_steps` _(optional)_ — per-step tool-round cap. +- `parent_session_id` _(optional)_ — parent session id for event correlation. +- `output_schema` _(optional)_ — when set, the step must return a value + conforming to this JSON Schema (see + [Schema-forced step output](#schema-forced-step-output)). + +`StepOutcome` fields: + +- `task_id` — the originating step's id. +- `session_id` — the child run's session id (failed steps remain addressable). +- `agent` — the agent that ran. +- `output` — the step's text output. +- `success` — `false` for a failed or panicked step (never a dropped sibling). +- `structured` _(optional)_ — schema-validated object, present only when the + spec carried an `output_schema`. + +The key casing differs by SDK: + +| Concept | Node (camelCase) | Python (snake_case) | +| -------------- | ----------------- | ------------------- | +| step id | `taskId` | `task_id` | +| tool-round cap | `maxSteps` | `max_steps` | +| parent session | `parentSessionId` | `parent_session_id` | +| forced schema | `outputSchema` | `output_schema` | + +`agent`, `description`, `prompt`, `output`, `success`, and `structured` are +spelled the same in both SDKs. + +## parallel — barrier fan-out + +`session.parallel(specs)` runs every spec as a fan-out and resolves with one +`StepOutcome` per spec, **in input order**. It maps to the core +`execute_steps_parallel` combinator. It is a **barrier**: it awaits every step +before returning. + +Each branch is isolated — a step that fails _or panics_ becomes +`success: false`; it never drops a sibling. Concurrency is bounded by the +executor's concurrency hint (the session's configured parallelism by default). + +```ts +const outcomes = await session.parallel([ + { + taskId: 'explore', + agent: 'explore', + description: 'Risky changes', + prompt: 'Find risky changed files in this diff.', + }, + { + taskId: 'verify', + agent: 'verification', + description: 'Test gaps', + prompt: 'Identify missing or weak verification.', + }, + { + taskId: 'review', + agent: 'review', + description: 'Correctness', + prompt: 'Review the diff for correctness risks.', + }, +]); + +for (const outcome of outcomes) { + if (outcome.success) { + console.log(outcome.taskId, outcome.output); + } else { + console.warn('failed:', outcome.taskId, outcome.output); + } +} +``` + +```python +outcomes = session.parallel([ + {"task_id": "explore", "agent": "explore", "description": "Risky changes", + "prompt": "Find risky changed files in this diff."}, + {"task_id": "verify", "agent": "verification", "description": "Test gaps", + "prompt": "Identify missing or weak verification."}, + {"task_id": "review", "agent": "review", "description": "Correctness", + "prompt": "Review the diff for correctness risks."}, +]) + +for outcome in outcomes: + if outcome["success"]: + print(outcome["task_id"], outcome["output"]) + else: + print("failed:", outcome["task_id"], outcome["output"]) +``` + +## pipeline — no barrier between stages + +`session.pipeline(items, stages)` flows each item through a chain of stages +**independently** — there is no barrier between stages, so item A can be in +stage 3 while item B is still in stage 1. Wall-clock time is the slowest _single +chain_, not the sum-of-slowest-per-stage that a per-stage barrier would incur. + +Stages are spec-builders, not specs: each stage receives the prior outcome and +the original item and returns the next step to run, or `null` / `None` to stop +that item's chain early. A failed step also stops the chain (a later stage would +only build on a failed result). The callback shapes: + +- Node: `(ctx) => spec | null` where `ctx = { previous: StepOutcome | null, item }` +- Python: `stage(ctx) -> spec | None` where `ctx = {"previous": , "item": }` + +A stage can branch on the prior outcome — e.g. "verify the finding the review +stage produced". + +Constraints (from the source): + +- Per-stage `output_schema` is **not supported** on pipeline stages — use + [`parallel`](#parallel--barrier-fan-out) for schema-validated steps. +- **Node:** a stage callback **must not throw** — a throw aborts the process + (the same constraint as `setBudgetGuard`). Wrap your logic in `try/catch` and + return `null` on error. +- **Node:** a stage that hangs past `timeoutMs` (the 3rd argument, default + `30000`) fails closed — it is treated as `null`, stopping only that chain. +- **Python:** a stage callable that raises is caught and treated as `None` + (stops only that chain). + +> A Node pipeline stage callback **must not throw.** In this napi version a JS +> throw at return-conversion aborts the process (the same fail-closed constraint +> as `setBudgetGuard`). Always wrap stage logic in `try/catch` and `return null` +> on error. + +```ts +const outcomes = await session.pipeline( + ['src/auth.ts', 'src/payments.ts'], + [ + (ctx) => ({ + taskId: `explore-${ctx.item}`, + agent: 'explore', + description: 'Inspect file', + prompt: `Summarize the responsibilities and risks of ${ctx.item}.`, + }), + (ctx) => { + try { + if (!ctx.previous) return null; + return { + taskId: `review-${ctx.item}`, + agent: 'review', + description: 'Review of prior finding', + prompt: `Review this summary for correctness risks:\n${ctx.previous.output}`, + }; + } catch { + return null; // stages must not throw + } + }, + ], +); +``` + +```python +def explore_stage(ctx): + item = ctx["item"] + return { + "task_id": f"explore-{item}", + "agent": "explore", + "description": "Inspect file", + "prompt": f"Summarize the responsibilities and risks of {item}.", + } + +def review_stage(ctx): + prev = ctx["previous"] + if prev is None: + return None + item = ctx["item"] + return { + "task_id": f"review-{item}", + "agent": "review", + "description": "Review of prior finding", + "prompt": f"Review this summary for correctness risks:\n{prev['output']}", + } + +outcomes = session.pipeline( + ["src/auth.ts", "src/payments.ts"], + [explore_stage, review_stage], +) +``` + +## Resumable / migratable workflows + +`session.parallelResumable(specs, workflowId)` (Node) / +`session.parallel_resumable(specs, workflow_id)` (Python) is `parallel` plus a +journal. It maps to `execute_steps_parallel_resumable`. + +At each step boundary it writes a `WorkflowCheckpoint` to the session store. On +resume it skips already-completed steps (reusing their cached outcomes) and +re-dispatches only the rest. It records **only successful steps** — a failed +step is not journaled, so it retries on resume. On full success the checkpoint +is deleted; only a crash leaves one behind for resume. + +Because the checkpoint is serializable and the executor is a parameter, a host +can resume an interrupted workflow on a **different node** (migration) by +passing that node's executor. + +This combinator **requires a configured session store** — both SDK methods +reject/raise without one (the Node error message is +`parallelResumable requires a sessionStore`). + +The `WorkflowCheckpoint` schema is `schema_version` / `workflow_id` / `steps` / +`checkpoint_ms`. A checkpoint written by a _future_, incompatible +`schema_version` is rejected on load (`ensure_loadable`). That failure is +fail-safe, not fatal: an unreadable checkpoint logs a warning and the workflow +re-runs from scratch rather than resuming from state it can't interpret. + +See [Persistence](/guide/persistence) for the store and +[Multi-Machine](/guide/multi-machine) for the migration path. + +```ts +import { Agent, FileSessionStore } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/repo', { + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); + +// First attempt — may be interrupted partway through. +let outcomes = await session.parallelResumable(specs, 'release-batch-42'); + +// After a crash/restart: same workflowId resumes, skipping completed steps. +outcomes = await session.parallelResumable(specs, 'release-batch-42'); +``` + +```python +from a3s_code import Agent, FileSessionStore, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.session_store = FileSessionStore("./.a3s/sessions") +session = agent.session("/repo", opts) + +# First attempt — may be interrupted partway through. +outcomes = session.parallel_resumable(specs, "release-batch-42") + +# After a crash/restart: same workflow_id resumes, skipping completed steps. +outcomes = session.parallel_resumable(specs, "release-batch-42") +``` + +## Shared budget across a fan-out + +By default each child agent counts its own LLM cost. Pass a token budget to +`parallel` and every child instead feeds **one shared ledger** — a single cap +for the whole fan-out. It maps to the core `WorkflowBudget`, an aggregating +`BudgetGuard` installed onto each child run. + +The budget is an **optional argument**, so it is backward compatible: + +- Without a budget, `parallel(specs)` returns the plain outcomes array, exactly + as before. +- With a budget, `parallel(specs, budgetTokens)` resolves to `{ outcomes, budget }`, + where `budget` is the ledger snapshot (`consumedTokens` / `limitTokens`). + +Once the cap is reached, a step that _starts_ afterwards is denied — its outcome +is `success: false` with a budget-exhausted message. It is a **soft cap**: +because usage is recorded _after_ each LLM call, a wide fan-out can race a few +in-flight turns past the cap before the ledger catches up. The framework never +force-kills an in-flight fan-out; an exhausted budget simply denies the _next_ +LLM call. + +```ts +// No budget → plain outcomes array (unchanged). +const outcomes = await session.parallel(specs); + +// With a budget → { outcomes, budget }. All children share one ledger. +const { outcomes: out, budget } = await session.parallel(specs, 500_000); +console.log(budget.consumedTokens, budget.limitTokens); // e.g. 48213, 500000 +// TS return type is a union: Array | { outcomes, budget }. +``` + +```python +# No budget → plain list (unchanged). +outcomes = session.parallel(specs) + +# With a budget → {"outcomes": [...], "budget": {"consumed_tokens", "limit_tokens"}}. +res = session.parallel(specs, budget_tokens=500_000) +print(res["budget"]["consumed_tokens"], res["budget"]["limit_tokens"]) +``` + +## Looping until done (`execute_loop`) + +For unknown-length, iterate-until-converge work (loop-until-dry, refine-until-good), +the core grammar adds `execute_loop`. Each round is a barrier (`execute_steps_parallel`); +a host-supplied predicate sees the round's outcomes and returns +`LoopDecision::Continue(next_specs)` or `LoopDecision::Stop`. A required +`max_iterations` is a **hard cap** — once reached the loop stops even if the +predicate would continue, so an LLM-driven loop can never run away. + +```rust +use a3s_code_core::orchestration::{execute_loop, AgentStepSpec, LoopDecision}; + +let outcomes = execute_loop(executor, initial_specs, /* max_iterations */ 5, None, |round| { + // Stop when the round produced no new findings; otherwise fan out follow-ups. + let follow_ups = derive_follow_ups(round); + if follow_ups.is_empty() { + LoopDecision::Stop + } else { + LoopDecision::Continue(follow_ups) + } +}) +.await; +``` + +> From the host SDKs you don't need a dedicated `loop` verb — write the loop in +> your own language (`while`/`for`) around `parallel`, deciding the next round +> from the outcomes. `execute_loop` exists for the Rust grammar and to give the +> loop a single, enforced termination guard. + +## The Workflow facade (Rust / embedding) + +`session.workflow()` returns a cheaply-clonable `Workflow` that pre-wires the +session's executor, persistence store, per-step event stream, and a stable, +session-derived root id. It is the programmable handle that bundles everything +above; control flow is ordinary Rust — `await` a verb, inspect the outcomes, +decide what runs next. + +- **Verbs** — `agent` (one step), `parallel` (barrier fan-out), `phase` (a + _named_, resumable barrier that emits milestones), `pipeline` (per-item + chains), and the non-failing `log`. Each delegates to exactly one combinator. +- **Phases & events** — `phase(name, specs)` derives a deterministic checkpoint + id (`{root}/{index}:{name}`), runs the resumable barrier when a store is + present, and emits `WorkflowEvent::PhaseStart` / `PhaseEnd` on a broadcast you + read with `subscribe()`. `log()` emits `WorkflowEvent::Log`. +- **Budget** — `session.workflow_with_token_budget(Some(limit))` installs a + shared `WorkflowBudget`; `budget_snapshot()` reads the ledger and a + `WorkflowEvent::BudgetExhausted` fires once the cap is hit. + +```rust +let wf = session.workflow(); // or session.workflow_with_token_budget(Some(500_000)) +let mut events = wf.subscribe(); + +// One step, then a *variable* fan-out computed from its result — the "dynamic" +// part: the shape is decided at run time, not declared up front. +let plan = wf.agent(AgentStepSpec::new("plan", "plan", "plan", goal)).await; +let specs = derive_specs(&plan); // your code +let done = wf.phase("implement", specs).await; // resumable barrier + milestones +let reviews = wf.phase("review", to_review(&done)).await; // budget shared across phases + +if let Some(b) = wf.budget_snapshot() { + println!("spent {} / {:?} tokens", b.consumed_tokens, b.limit_tokens); +} +``` + +The SDKs expose the flat `parallel` / `pipeline` / `parallelResumable` verbs (and +the `parallel` budget overload above); the full `Workflow` handle — phases, +event subscription, the loop combinator — is a Rust/embedding API. + +## Schema-forced step output + +A spec carrying `output_schema` (`outputSchema` in Node) forces the step to +return a value conforming to that JSON Schema; the validated object lands in +`StepOutcome.structured`. This reuses the same structured-output coercion + +repair machinery as the rest of A3S Code. A coercion failure **demotes the step +to unsuccessful** (`success: false`), so callers never treat unvalidated text as +the promised object. + +Forced schema applies to `parallel` / `parallelResumable` specs only — **not** +pipeline stages. + +```ts +const [outcome] = await session.parallel([ + { + taskId: 'triage', + agent: 'review', + description: 'Structured triage', + prompt: 'Triage this diff.', + outputSchema: { + type: 'object', + properties: { + severity: { type: 'string', enum: ['low', 'medium', 'high'] }, + summary: { type: 'string' }, + }, + required: ['severity', 'summary'], + }, + }, +]); + +if (outcome.success) { + console.log(outcome.structured.severity, outcome.structured.summary); +} +``` + +```python +outcomes = session.parallel([ + { + "task_id": "triage", + "agent": "review", + "description": "Structured triage", + "prompt": "Triage this diff.", + "output_schema": { + "type": "object", + "properties": { + "severity": {"type": "string", "enum": ["low", "medium", "high"]}, + "summary": {"type": "string"}, + }, + "required": ["severity", "summary"], + }, + }, +]) + +outcome = outcomes[0] +if outcome["success"]: + print(outcome["structured"]["severity"], outcome["structured"]["summary"]) +``` + +## Cost governance & lifecycle + +Orchestrated steps run through the same session, so the session's controls apply +to them directly. `setBudgetGuard` (Node) / `budget_guard` (Python) bounds the +LLM cost of every step; `close()` cancels in-flight steps along with the rest of +the session's work; and the host-provided identity labels (`tenant_id`, +`principal`, `agent_template_id`, `correlation_id`) flow through each step for +host-side aggregation and billing. See [Sessions](/guide/sessions) and +[Limits](/guide/limits) for the details of those controls. diff --git a/website/docs/v6/en/guide/persistence.mdx b/website/docs/v6/en/guide/persistence.mdx new file mode 100644 index 00000000..f89c5168 --- /dev/null +++ b/website/docs/v6/en/guide/persistence.mdx @@ -0,0 +1,156 @@ +--- +title: 'Persistence' +description: 'Saving and resuming sessions' +--- + +# Persistence + +Persistence lets a session survive process restarts and gives product surfaces a stable session ID. + +A3S Code persists three related but different things: + +| Object | Written by | Resumed by | Purpose | +| ------------------- | ------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `SessionSnapshotV1` | `session.save()` or `autoSave` | `agent.resumeSession(id, options)` | Rehydrate one versioned generation containing the conversation, artifacts, traces, run records, verification reports, and subagent task snapshots. | +| Loop checkpoint | Agent loop while a run is in progress | `session.resumeRun(runId)` | Continue an interrupted run from the last completed tool-round boundary. Completed in-process runs delete this checkpoint. | +| Workflow checkpoint | `parallelResumable` / workflow phases | `parallelResumable(specs, workflowId)` | Skip already-completed orchestration steps after a process restart. | + +## File Session Store + +```ts +import { FileSessionStore } from '@a3s-lab/code'; + +const session = agent.session('/repo', { + sessionId: 'release-review', + sessionStore: new FileSessionStore('./.a3s/sessions'), + autoSave: true, +}); + +await session.send('Review release readiness'); +await session.save(); +``` + +## Atomic Snapshot Generations + +`session.save()` gathers the current persisted state into one +`SessionSnapshotV1` and calls `SessionStore::save_snapshot` once. The envelope +contains: + +- `schema_version` and `SessionData` +- tool artifacts +- trace events and run records +- verification reports +- delegated subagent task snapshots + +The file store writes one complete JSON envelope to a synced temporary file and +atomically replaces `.json`. Readers therefore observe the previous +generation or the next one, not a new conversation paired with old run or trace +fragments. The memory store publishes the same aggregate under one lock. Both +report `SessionStoreCapabilities { atomic_session_snapshots: true }`. + +Historical files remain readable. A bare `SessionData` file is combined with +the legacy artifact/trace/run/verification/subagent fragment locations during +load, then restored through the v1 in-memory shape. Once a new aggregate is +saved, the single envelope is authoritative. A document that already looks like +an aggregate but has a malformed or unsupported schema is rejected rather than +reinterpreted as legacy data. + +Custom stores must implement `save_snapshot` explicitly. Its default returns an +error; it does not fan an aggregate out into independent writes or silently +acknowledge a no-op. The default `load_snapshot` exists only as best-effort +legacy assembly, and `capabilities()` lets a host distinguish that behavior +from an atomic backend. + +## Resume + +```ts +const resumed = agent.resumeSession('release-review', { + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); +``` + +`resumeSession` restores a saved session snapshot. It is not the same as +`resumeRun`: use `resumeSession` when the user is continuing a saved +conversation, and use `resumeRun(runId)` only when a run checkpoint exists for +an interrupted run. Resume validates the snapshot schema before restoring any +history or runtime evidence. + +## Memory And Sessions + +Session persistence stores conversation and replay evidence. Memory stores +reusable task facts. Use both when you want resumable workflows that also learn +from repeated tasks. + +## Loop Checkpoints and Run Resumption + +When a `SessionStore` is configured, the agent loop persists a `LoopCheckpoint` +after each completed tool round. The boundary policy is strict: checkpoints are +taken **only** between tool rounds, never mid-tool. If a process dies while a +tool is executing, that round's work is lost on resume and the LLM +re-deliberates from the previous checkpoint — re-running a non-idempotent tool +(write, bash) on the wrong side of the boundary is worse than re-asking the LLM. + +`session.resumeRun(runId)` (Node) / `session.resume_run(run_id)` (Python) — core +`AgentSession::resume_run(checkpoint_run_id)` — loads the latest checkpoint +stored under that run ID and replays the loop from the last boundary. Because +the checkpoint lives in the shared store, resume can happen on **any** node that +shares it. Cumulative accounting continues rather than restarting at zero: +`total_usage` and `tool_calls_count` carry forward from the checkpoint. A new run +ID is allocated for the resumed work; the old-to-new relationship is host +metadata, not interpreted by the framework. + +Completed runs are not resumed through `resumeRun`; their final state is +available through `runs()`, `runEvents(runId)`, artifacts, verification reports, +and session snapshots. + +```ts +const result = await session.resumeRun('run-abc123'); +console.log(result.totalTokens); +``` + +```python +result = session.resume_run('run-abc123') +print(result.total_tokens) +``` + +`resume_run` rejects when the session has no `sessionStore` configured (or when +no checkpoint exists for the given ID). `SessionStore` gains +`save_loop_checkpoint` / `load_loop_checkpoint` / `delete_loop_checkpoint`; the +file store's writes are crash-atomic. `LoopCheckpoint::ensure_loadable()` is +called right after deserialization and rejects checkpoints from a future, +incompatible schema version, so neither `resume_run` nor the live-run sink acts +on an unreadable checkpoint. + +See CHANGELOG `[3.3.0]` — "Loop checkpoints + run resumption" — and `[3.4.0]` +"LoopCheckpoint::ensure_loadable()". + +## Workflow Checkpoints + +`WorkflowCheckpoint` is the step-boundary analogue of the tool-round +`LoopCheckpoint`, one level up: it journals completed orchestration steps so an +interrupted workflow resumes from the longest completed prefix. Its fields are +`schema_version`, `workflow_id`, `steps`, and `checkpoint_ms`, with the schema +pinned by the `WORKFLOW_CHECKPOINT_SCHEMA_VERSION` constant. A resuming run skips +the recorded steps and re-dispatches only the rest. + +`SessionStore` gains `save_workflow_checkpoint` / `load_workflow_checkpoint` / +`delete_workflow_checkpoint` (default no-ops; the file store writes +crash-atomically). Loads from a future, incompatible schema version are rejected +via `WorkflowCheckpoint::ensure_loadable()`. + +This pairs with the orchestration grammar — see +[Orchestration](/guide/orchestration) and +[Multi-Machine](/guide/multi-machine). + +See CHANGELOG `[3.4.0]` — "WorkflowCheckpoint". + +## Resuming on a Different Node + +Both checkpoint types are serializable. Combined with a shared `SessionStore` +and a pluggable executor, this lets a host resume an interrupted run or workflow +on a **different** node from the one that started it — the framework owns the +serializable contract, the host owns placement and transport. + +## Operational Notes + +Keep session stores out of public commits when they may contain prompts, tool output, or private file paths. diff --git a/website/docs/v6/en/guide/providers.mdx b/website/docs/v6/en/guide/providers.mdx new file mode 100644 index 00000000..ecc6e3ec --- /dev/null +++ b/website/docs/v6/en/guide/providers.mdx @@ -0,0 +1,113 @@ +--- +title: 'Providers' +description: 'ACL provider configuration and environment injection' +--- + +# Providers + +A3S Code reads runtime configuration from ACL. A config source can be an +`.acl` file path or an inline ACL string. JSON and legacy HCL configs are not +part of the current config surface. + +## Basic Shape + +```text +default_model = "provider/model-id" +max_parallel_tasks = 4 +auto_parallel = false + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "model-id" { + name = "Human readable model name" + tool_call = true + limit = { + context = 128000 + output = 4096 + } + } +} + +storage_backend = "file" +sessions_dir = ".a3s/sessions" +``` + +`apiKey` and `api_key` are accepted aliases. `baseUrl` and `base_url` are +accepted aliases. Commit templates and non-secret defaults only; keep real API +keys, private endpoints, and account-specific model names in the environment or +in a secret manager. + +## Provider Families + +The built-in factory covers three paths: + +| Provider name | Client path | Notes | +| ---------------------------- | -------------------------- | ------------------------------------------------------------- | +| `anthropic` / `claude` | Anthropic client | Uses the configured model id and optional provider base URL. | +| `openai` / `gpt` | OpenAI-compatible client | Use this for OpenAI-compatible Chat Completions endpoints. | +| `glm` / `zhipu` / `bigmodel` | Zhipu-compatible client | Use environment variables for the key and base URL. | +| Any other provider name | OpenAI-compatible fallback | Useful for private or self-hosted OpenAI-compatible services. | + +The runtime does not hard-code model names. `default_model` and per-session +`model` values are identifiers in the `provider/model-id` form that must match +the provider blocks you define. + +## Delegation Controls + +```text +agent_dirs = ["./.a3s/agents"] + +auto_delegation { + enabled = true + auto_parallel = false + allow_manual_delegation = true + min_confidence = 0.72 + max_tasks = 4 +} +``` + +`max_parallel_tasks` limits bounded sibling fan-out. `auto_delegation.enabled` +turns on automatic subagent delegation. The top-level `auto_parallel = false` +overrides `auto_delegation.auto_parallel` and disables only automatic parallel +child-agent fan-out; manual `task` and `parallel_task` remain available. When +`allow_manual_delegation = false`, both model-visible child-agent tools are +removed from the session tool surface. + +## Storage + +```text +storage_backend = "memory" +storage_backend = "file" +sessions_dir = ".a3s/sessions" +``` + +Use memory storage for short-lived tests. Use `storage_backend = "file"` plus +`sessions_dir` for resumable local sessions loaded from ACL. `storage_url` is +parsed for custom storage metadata, but it does not create a file-backed +session store by itself. SDK hosts can always pass +`sessionStore: new FileSessionStore(...)` / +`opts.session_store = FileSessionStore(...)` directly. + +## Private Provider Checks + +Real-provider smoke tests should point at a local, git-ignored ACL file through +`A3S_CONFIG_FILE`. Do not copy provider values into commands, logs, docs, pull +requests, or committed fixtures. + +```bash +A3S_CONFIG_FILE=/path/to/local/config.acl \ + scripts/real_config_env_integration.sh +``` + +SDK parity is covered by a separate real-provider check: + +```bash +A3S_CONFIG_FILE=/path/to/local/config.acl \ + scripts/sdk_real_config_env_integration.sh +``` + +Keep the provider evidence with the release or CI artifact, not in the public +documentation. For the complete local API surface, see +[API Contract](/guide/api-contract). diff --git a/website/docs/v6/en/guide/rfcs/_meta.json b/website/docs/v6/en/guide/rfcs/_meta.json new file mode 100644 index 00000000..04371f2a --- /dev/null +++ b/website/docs/v6/en/guide/rfcs/_meta.json @@ -0,0 +1 @@ +["workspace-remote-git"] diff --git a/website/docs/v6/en/guide/rfcs/workspace-remote-git.mdx b/website/docs/v6/en/guide/rfcs/workspace-remote-git.mdx new file mode 100644 index 00000000..966667e7 --- /dev/null +++ b/website/docs/v6/en/guide/rfcs/workspace-remote-git.mdx @@ -0,0 +1,537 @@ +--- +title: 'RFC: Remote Workspace Git Backend' +description: 'HTTP/JSON protocol for serving git operations to non-local workspace backends' +--- + +# RFC: Remote Workspace Git Backend + +| Field | Value | +| ------- | -------------------------------------------------------------------------- | +| Status | **Implemented** in v2.6.x (`crates/code/core/src/workspace/remote_git.rs`) | +| Owner | `crates/code` workspace subsystem | +| Tracks | Phase 4.1 of S3 / non-local workspace hardening | +| Related | `S3WorkspaceBackend`, `WorkspaceGit*` trait family | + +This document is retained as the protocol specification — clients and +servers speaking this surface should match the wire shapes defined +below. Subsequent revisions should land as separate amendment documents +rather than editing this one in place, so the "what was shipped" history +stays auditable. + +This RFC proposes a protocol and a Rust client that let `a3s-code` run the +built-in `git` tool against workspaces backed by something other than a local +filesystem — most importantly S3 and future container / DFS backends, which +cannot host a `.git` directory directly. + +## 1. Motivation + +The workspace abstraction already lets the built-in tools talk to any +filesystem implementation via `WorkspaceFileSystem`. For `bash`, `grep`, +`glob`, and `git` we use **capability gating**: if the backend cannot +service the operation, the corresponding tool is not registered and the +model never sees it. + +That works for `bash` (object storage really cannot run a shell) and is +acceptable for `grep` (we added a degraded `LIST + GET + regex` path). It is +**painful for `git`**: many a3s-code workflows expect to query branch / +commit state, diff working tree, create branches, and stash changes. Hiding +`git` everywhere except local sessions splits the user experience and +prevents cloud workspaces from being first-class. + +The proposal is to introduce a **remote `WorkspaceGit` backend** that +delegates these operations over the network to an external service the host +operates. The service owns a real working tree (or a libgit2-backed +implementation) and exposes a small HTTP API. The a3s-code client speaks +that API and presents it to tools as just another `WorkspaceGit` provider. + +```text + ┌──────────────────┐ + │ a3s-code │ + model │ │ + ──────► │ git tool │ HTTP/JSON + │ │ │ ┌────────────────┐ + │ ▼ │ │ │ + │ WorkspaceGit ───┼──►│ gitserver │ + │ (RemoteGit…) │ │ (libgit2 / sh)│ + │ │ │ │ + │ WorkspaceFs ────┼──►│ (S3, etc.) │ + └──────────────────┘ └────────────────┘ +``` + +## 2. Non-Goals + +- **Hosting a gitserver.** This RFC defines a client and a protocol the + client speaks. Implementations of the server side (libgit2 wrapper, + shell-out, Gitea-API adapter, ...) are out of scope. +- **Replacing local git.** When `WorkspaceFs` is `LocalWorkspaceBackend`, + the existing `LocalWorkspaceBackend` `WorkspaceGit` impl remains the + default. Hosts mix and match per session. +- **Push / pull.** The `WorkspaceGit` trait surface is read-only against + remotes (`list_remotes` returns the configured set; no `git push` / + `git fetch`). A future RFC may add those if a tool needs them. +- **Worktrees.** Worktrees are a local-filesystem concept; the remote + backend will **not** implement `WorkspaceGitWorktreeProvider`. See §8. + +## 3. Protocol Choice — HTTP/JSON + +**Decision:** HTTP/JSON, not gRPC. + +| Criterion | HTTP/JSON | gRPC | +| ---------------------- | ------------------------------------- | ------------------------------ | +| Existing deps | `reqwest` already in tree | `tonic` + proto toolchain new | +| Schema strictness | manual `serde` types | `.proto` typed | +| Mocking in tests | trivial (`wiremock`) | needs gRPC mock infra | +| Operator debuggability | `curl` works | `grpcurl` (less ubiquitous) | +| Streaming | chunked / SSE | native bidi streams | +| Operation count | ~12 ops, all req/resp | small; streaming rarely needed | +| Server impl freedom | trivial wrapper around `git`, `gitea` | requires a gRPC server | + +The operation set is small (~12 RPCs) and the request/response shapes are +flat. Streaming would only meaningfully help `log` and `diff`, both of +which are bounded by client-side limits already. The mocking and operator +debuggability wins of HTTP/JSON outweigh the schema benefits of gRPC at +this scale. If the operation surface grows substantially or streaming +becomes load-bearing, gRPC can be revisited. + +We use **`POST` for every operation**. Git operations are imperative, not +resource-CRUD, and request bodies make versioning trivial (add optional +fields, ignore unknown). `GET` would force everything into query strings. + +## 4. Repository Identity + +The client knows it is talking about a specific repository; the server +needs to route. We encode the repo identity in the **URL path**: + +``` +POST /v1/repos/{repo_id}/git/ +``` + +`repo_id` is an opaque, URL-safe string negotiated out of band between the +host that configures the client and the gitserver operator. Typical values: + +- `users/{user_id}/sessions/{session_id}` (1:1 with an S3 workspace prefix) +- A UUID +- A path to a working tree on the gitserver + +The client treats `repo_id` as opaque; the server is responsible for +mapping it to an actual working tree / bare repo. + +## 5. Endpoint Reference + +All endpoints are `POST`. All requests and responses are `application/json`. +Field naming is `snake_case` to match Rust serde defaults. + +### 5.1 Status — `WorkspaceGit::status` + +``` +POST /v1/repos/{repo_id}/git/status +→ 200 { + "branch": "main", + "commit": "abc123...", + "is_worktree": false, + "is_dirty": true, + "dirty_count": 3 + } +→ 404 {"error":{"code":"REPO_NOT_FOUND", ...}} +``` + +### 5.2 Log — `WorkspaceGit::log` + +``` +POST /v1/repos/{repo_id}/git/log +{"max_count": 10} +→ 200 { + "commits": [ + {"id":"abc...", "message":"feat: ...", "author":"Alice ", "date":"2026-05-19T..."} + ] + } +``` + +### 5.3 List Branches — `WorkspaceGit::list_branches` + +``` +POST /v1/repos/{repo_id}/git/branches +→ 200 {"branches":[{"name":"main", "is_current":true}, ...]} +``` + +### 5.4 Create Branch — `WorkspaceGit::create_branch` + +``` +POST /v1/repos/{repo_id}/git/branches/create +{"name":"feat/x", "base":"main"} +→ 201 {} +→ 409 {"error":{"code":"BRANCH_EXISTS", ...}} +→ 404 {"error":{"code":"BASE_NOT_FOUND", ...}} +``` + +### 5.5 Checkout — `WorkspaceGit::checkout` + +``` +POST /v1/repos/{repo_id}/git/checkout +{"refspec":"feat/x", "force":false} +→ 200 {"stdout":"Switched to branch 'feat/x'"} +→ 409 {"error":{"code":"WORKING_TREE_DIRTY", ...}} +``` + +### 5.6 Diff — `WorkspaceGit::diff` + +``` +POST /v1/repos/{repo_id}/git/diff +{"target": null} // null = working tree vs index +{"target": "main"} // diff against ref +→ 200 {"diff":"", "truncated": false} +``` + +`truncated` is `true` when the server clipped the body — see §9. The +client surfaces this in the `git diff` tool result. + +### 5.7 List Remotes — `WorkspaceGit::list_remotes` + +``` +POST /v1/repos/{repo_id}/git/remotes +→ 200 {"remotes":[{"name":"origin", "url":"git@github.com:...", "direction":"fetch"}]} +``` + +### 5.8 Is Repository — `WorkspaceGit::is_repository` + +``` +POST /v1/repos/{repo_id}/git/exists +→ 200 {"is_repository": true} +``` + +Distinct from "repo not found" (404): the server may be configured to +serve repo IDs that map to non-git directories; `is_repository` lets the +client probe without inferring intent from a 404. + +### 5.9 List Stashes — `WorkspaceGitStashProvider::list_stashes` + +``` +POST /v1/repos/{repo_id}/git/stashes +→ 200 {"stashes":[{"index":0, "message":"WIP on main: ..."}]} +``` + +### 5.10 Stash — `WorkspaceGitStashProvider::stash` + +``` +POST /v1/repos/{repo_id}/git/stashes/create +{"message":"wip", "include_untracked":true} +→ 201 {} +→ 409 {"error":{"code":"NOTHING_TO_STASH", ...}} +``` + +## 6. Authentication + +The client supports two transport modes, configurable per-session: + +1. **Bearer token (default).** `Authorization: Bearer `. Token + provisioning is the host's responsibility (e.g. short-lived JWT minted + by the same identity layer that gates S3 access). +2. **mTLS.** Pass both `client_cert_pem` and `client_key_pem` paths via + the backend config. The client reads both files at construction, + concatenates them, and hands the result to `reqwest::Identity::from_pem`. + The `rustls-tls` backend expects the key in PKCS#8 PEM format. + Setting only one of the pair fails at construction with a clear error. + +Both are mutually compatible — a deployment that uses both for defence in +depth simply sets both. + +No-auth mode (for localhost development) is supported by configuring an +empty bearer token (and no mTLS); the client emits a `tracing::warn!` +on construction to make this visible. + +## 7. Error Model + +HTTP status code is the **transport** signal. The error **kind** is in +the JSON body: + +```json +{ + "error": { + "code": "BRANCH_EXISTS", + "message": "branch 'feat/x' already exists" + } +} +``` + +Client mapping: + +| HTTP | Default outcome | +| ------- | ------------------------------------------- | +| 200/201 | `Ok(...)` | +| 400 | `Err(anyhow!("bad request: {message}"))` | +| 401/403 | `Err(anyhow!("auth failed: {message}"))` | +| 404 | `Err(anyhow!("not found: {message}"))` | +| 409 | typed conflict — see below | +| 5xx | `Err(anyhow!("gitserver internal: {...}"))` | + +The client introduces one typed error for backwards-compatible recovery: + +```rust +#[derive(Debug, Clone, thiserror::Error)] +#[error("remote git conflict: {code}: {message}")] +pub struct RemoteGitConflict { + pub code: String, + pub message: String, +} +``` + +Tools that want to recover from `BRANCH_EXISTS` / `WORKING_TREE_DIRTY` / +`NOTHING_TO_STASH` downcast `anyhow::Error::downcast_ref::()`, +the same pattern `edit` / `patch` use for `WorkspaceVersionConflict` on +S3 compare-and-swap writes. + +Error codes documented in the public schema (extensible): + +| Code | Origin | +| -------------------- | ---------------------------------------------------- | +| `REPO_NOT_FOUND` | `repo_id` not served | +| `NOT_A_REPOSITORY` | path exists but is not a git repo | +| `BRANCH_EXISTS` | `create_branch` with existing name | +| `BRANCH_NOT_FOUND` | checkout / diff target missing | +| `BASE_NOT_FOUND` | `create_branch` base ref missing | +| `WORKING_TREE_DIRTY` | `checkout` would lose changes (and `force` is false) | +| `NOTHING_TO_STASH` | stash on a clean tree | +| `RATE_LIMITED` | per-tenant throttle hit (server-defined) | + +## 8. Optional Traits + +`WorkspaceGit` is implemented in full. +`WorkspaceGitStashProvider` is implemented. +`WorkspaceGitWorktreeProvider` is **deliberately not implemented** by the +remote backend. Worktrees are a local-filesystem concept that map poorly +onto a remote service: + +- "Create a worktree at path X" — the client has no path concept; the + server's path layout is opaque. +- The local tool flow that uses worktrees (parallel agent runs against + isolated copies) is better served on cloud workspaces by spinning up + **separate sessions with separate `repo_id`s**, not by emulating a + filesystem feature. + +Tools that depend on `WorkspaceGitWorktreeProvider` see `None` from +`services.git_worktree()` and emit a clear "worktrees unavailable on +remote git workspaces" error. + +## 9. Size and Cost Bounds + +Following the S3 backend's pattern, the remote git client enforces +client-side ceilings so the model cannot trigger unbounded responses: + +| Setting | Default | Applies to | +| ------------------------ | ------- | ---------------------------------------------------- | +| `max_diff_bytes` | 1 MiB | `diff` response body | +| `max_log_entries` | 200 | `log` `max_count` cap | +| `request_timeout` | 30 s | every HTTP call | +| `operation_timeout` (WS) | 60 s | the `WorkspaceServices`-level wrapper applies on top | + +The server is expected to honour these too — the client passes the relevant +caps in the request where applicable (`max_log_entries`), and surfaces +`truncated: bool` returned by the server on `diff` so the tool can hint +"large diff truncated; narrow your target". + +## 10. Rust Client Design + +```rust +// crates/code/core/src/workspace/remote_git.rs (feature = "remote-git") + +#[derive(Debug, Clone)] +pub struct RemoteGitBackendConfig { + pub base_url: String, // https://git.example.invalid + pub repo_id: String, // path-segment safe + pub bearer_token: Option, + pub client_cert_pem: Option, // mTLS + pub client_key_pem: Option, + pub request_timeout: Option, // default 30s + pub max_diff_bytes: Option, // default 1 MiB + pub max_log_entries: Option, // default 200 +} + +#[derive(Debug, Clone)] +pub struct RemoteGitBackend { + http: reqwest::Client, + base_url: String, + repo_id: String, + max_diff_bytes: u64, + max_log_entries: usize, +} + +#[async_trait] +impl WorkspaceGit for RemoteGitBackend { /* see §5 */ } + +#[async_trait] +impl WorkspaceGitStashProvider for RemoteGitBackend { /* see §5 */ } +``` + +Composition factory mirrors the S3 path: + +```rust +impl WorkspaceServices { + /// Attach a remote git provider to an existing filesystem backend. + pub fn with_remote_git( + self: Arc, + cfg: RemoteGitBackendConfig, + ) -> Arc { ... } +} +``` + +Or as a top-level convenience for an S3 + remote-git workspace: + +```rust +pub fn s3_with_remote_git( + s3: S3BackendConfig, + git: RemoteGitBackendConfig, +) -> Arc { ... } +``` + +Wiring follows the existing builder pattern: + +```rust +let backend = Arc::new(RemoteGitBackend::new(cfg)); +let git: Arc = backend.clone(); +let stash: Arc = backend; + +WorkspaceServices::builder(workspace_ref, fs) + .file_system_ext(fs_ext) // S3 ETag CAS + .git(git) + .git_stash(stash) + // no git_worktree — see §8 + .operation_timeout(Duration::from_secs(60)) + .build() +``` + +Capability gating then registers the `git` tool automatically. + +## 11. Per-Call Observability + +Every HTTP call emits a `tracing::debug!` event with the same field +shape `S3WorkspaceBackend` uses for its per-call metering (see +`emit_s3_call_event` in `crates/code/core/src/workspace/s3.rs`): + +| Field | Example | +| ------------- | ----------------------------- | +| `op` | `git.status`, `git.diff`, ... | +| `repo_id` | `sessions/example` | +| `outcome` | `ok` \| `error` | +| `status` | HTTP status code | +| `bytes` | response body length | +| `duration_ms` | wall-clock | + +Hosts that already meter S3 cost can extend the same subscriber to meter +gitserver cost; no new dependency or hook surface is needed. + +## 12. Composition Examples + +### S3 workspace + remote git + +```rust +let ws = WorkspaceServices::s3_with_remote_git( + S3BackendConfig::new("workspace", "sessions/example", access_key, secret_key) + .endpoint("https://s3.example.invalid") + .force_path_style(true) + .enable_search(true), + RemoteGitBackendConfig::new( + "https://git.example.invalid", + "sessions/example", + ) + .bearer_token(token), +); + +let session = agent + .session_builder("s3://workspace/sessions/example") + .options(SessionOptions::new().with_workspace_backend(ws)) + .build() + .await?; +``` + +Tools registered for this session: `read`, `write`, `edit`, `patch`, `ls`, +`grep`, `glob`, `git`. (`bash` remains hidden — object storage cannot +host a shell.) + +### Local filesystem + remote git (mixed) + +Useful when CI runs against a local checkout but the host wants to route +git operations through a sandboxed service (e.g. for audit / rate limiting). + +```rust +let local = WorkspaceServices::local("/workspaces/repo"); +let ws = local.with_remote_git(remote_cfg); // overrides local git provider +``` + +## 13. Open Questions + +These should be resolved before Phase 4.2 (implementation) starts. + +1. **Diff format pinning.** The local backend returns raw `git diff` + stdout. Should the remote API guarantee a specific diff dialect (POSIX + `diff -u`? libgit2's variant?) or pass through whatever the server + produces? Recommendation: pass through, document that the server + should produce unified diff. + +2. **Concurrent operations.** If two clients hit the same `repo_id` + simultaneously (one running `checkout`, one running `diff`), is the + server expected to serialise? Recommendation: server-side serialisation + per `repo_id`. Document the expectation; do not retry on conflict in + the client. + +3. **Long-running operations.** `checkout` against a large tree could + exceed `request_timeout`. Should the protocol support a polling / + async-job mode? Recommendation: defer. Set realistic timeouts; the + working-tree sizes we target (per-session sandboxes) are small. + +4. **Hooks.** The server may have pre-commit / pre-push hooks installed. + Do we surface a "hook output" channel in responses? Recommendation: + include hook stderr in the `checkout` / `stash` response when + non-empty; document that hook failures map to HTTP 422 with a + `HOOK_FAILED` code. + +5. **Schema versioning.** First-cut endpoints are under `/v1/`. When + should we bump to `/v2/`? Recommendation: only on incompatible + request/response shape changes; additive fields stay under `/v1/` + (clients ignore unknown fields). + +6. **Reference RFC.** Should a reference implementation (e.g. a minimal + libgit2-backed gitserver written in Rust) live in this repo, or stay + out-of-tree? Recommendation: out-of-tree. The client and protocol are + sufficient; reference servers belong with operators. + +## 14. Out of Scope (Future RFCs) + +- **Push / fetch from upstreams.** Needed for "agent commits and pushes + changes back to origin" workflows. Will need credential delegation. +- **Sparse / partial checkout.** Useful for huge monorepos; the current + surface assumes the entire repo is materialised on the server. +- **Streaming `log` and `diff`.** Required if response bodies routinely + exceed `max_diff_bytes`. Would motivate revisiting the gRPC decision. +- **Hooks management.** Listing / configuring server-side hooks from the + client. + +## 15. Implementation Notes (shipped in v2.6.x) + +The original draft sketched an 8-step implementation plan; what +actually shipped (Phase 4.2 plus Phase 5.x follow-ups) is: + +1. `RemoteGitBackend` / `RemoteGitBackendConfig` / `RemoteGitConflict` + live in `crates/code/core/src/workspace/remote_git.rs`. No + `remote-git` feature was added — `reqwest` was already a hard + dependency for the LLM clients, so the module compiles + unconditionally. +2. `WorkspaceGit` and `WorkspaceGitStashProvider` implemented in full. + `WorkspaceGitWorktreeProvider` deliberately omitted (see §8). +3. `WorkspaceServices::with_remote_git` is the public attachment + point. The internal helper `with_git_provider` (added in v2.6.x + follow-up) handles the actual struct-literal copy so future fields + on `WorkspaceServices` cannot be silently dropped by the decorator. +4. mTLS is supported in addition to bearer token (`client_cert_pem` + - `client_key_pem`), shipped in a Phase 5.2 follow-up that also + updated §6 above. +5. `diff` is hardened against unbounded responses: HTTP body is + streamed with a hard cap of `max_diff_bytes * 4` (Phase 6.2). The + soft `max_diff_bytes` display truncation continues to apply + post-decode. +6. Test surface: 25+ wiremock-backed unit tests in `remote_git.rs`, + one end-to-end test driving the `git` built-in tool through the + remote backend, plus the workspace-wide conformance suite added + in Phase 6.3. +7. README and CHANGELOG updated; rustls-tls backend selected to match + the AWS SDK. +8. SDK exposure landed in Phase 5.1 (Node + Python). diff --git a/website/docs/v6/en/guide/security.mdx b/website/docs/v6/en/guide/security.mdx new file mode 100644 index 00000000..f1306362 --- /dev/null +++ b/website/docs/v6/en/guide/security.mdx @@ -0,0 +1,202 @@ +--- +title: 'Security' +description: 'Permission policy, HITL, hooks, and verification gates' +--- + +# Security + +A3S Code exposes security controls at session creation time and through +session lifecycle hooks. Treat direct host calls such as `session.tool()` and +`session.bash()` as privileged host operations: the application that calls them +is responsible for deciding whether to expose that power to a user. + +Delegated child runs intersect their local permissions with the parent +permission checker and inherit the parent process sandbox. A child can narrow +its capabilities but cannot replace the host boundary. Child-local approval +behavior applies only to an `Ask` introduced by the child policy; a parent +`Ask`, or a tool-owned escalation after both policies allow, remains under the +parent confirmation provider. Missing providers fail closed before a HITL +request is emitted. Keep high-risk release or publish commands behind explicit +policy. + +## Enforced Execution Boundary + +Permission policy decides whether an operation is allowed, denied, or needs +approval. It does not, by itself, confine a process. Hosts that expose model +Bash should attach a concrete `BashSandbox`; otherwise an approved command runs +through the host workspace runner. + +The Code TUI uses an SRT-backed local process sandbox from a verified user-wide +installation or the support tree shipped in the CLI release. It checks the +exact package graph, registry lock, file types, size bounds, complete normalized +tree digest, Node.js 20.11-or-newer executable, and Core capability handshake. +Official archives, Homebrew, and standalone self-update preserve that payload. +Source and Cargo installs may use a fixed npm development bootstrap with +lifecycle scripts disabled. A global `srt` installation is neither required +nor selected. + +Package verification is necessary but not sufficient for readiness. macOS +requires `sandbox-exec` and `ripgrep`; Linux requires `bubblewrap`, `socat`, +`ripgrep`, and permitted unprivileged user namespaces. Homebrew declares those +dependencies, while direct-archive users provide them. Windows requires its +one-time elevated machine sandbox setup. The TUI runs a bounded command through +the actual OS boundary before attaching it. Failure leaves the sandbox absent, +so Default can request one exact host invocation and Auto denies Bash. + +The adapter fails closed and never silently retries on the host. It denies +network egress, local binding, and Unix sockets; limits writes to the workspace +and a private scratch directory; protects `.git`, `.a3s`, `.agents`, `.codex`, +`.claude`, `.vscode`, `.idea`, and control files; masks common credential +stores; and scrubs ambient secrets. Delegated tasks, Skills, and workflow steps +retain the same handle. + +Because built-in file tools execute in-process, the TUI separately enables +Core's local workspace credential policy. It covers direct and range reads, +writes, edits, patches, and both manifest-backed and fallback grep. Explicit +sensitive paths fail closed, broad grep omits protected candidates, and +source-tree hardlink aliases are denied before mutation. Ordinary +package-store hardlinks remain usable unless they alias a discovered +credential inode. Read-only Git diff regenerates output only for allowed +changed paths, option-like revisions cannot become Git flags, and displayed +remotes omit embedded HTTP credentials and query tokens. + +The release gate exercises the exact generated support tree on macOS and Linux. +It proves normal workspace writes and offline toolchain commands remain usable, +while outside and symlink writes, protected metadata mutations, credential +reads, network egress, local listeners, and Unix sockets remain blocked. + +The terminal execution modes apply this boundary as follows: + +| Mode | Process behavior | Approval behavior | +| ------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| Default | Ordinary Bash runs in the installed sandbox. | Only an explicit host escape, missing sandbox, protected metadata mutation, mutating Git call, or annotated external side effect asks. | +| Plan | Bash and mutations are unavailable. | Boundary crossings are denied, not queued for later execution. | +| Auto | Ordinary Bash runs only in the installed sandbox. | HITL is disabled; any operation that cannot stay inside the boundary is denied. | + +## Threat Model + +The protected assets are host files outside the active workspace, repository +and agent-control metadata, credential stores, host network and listener +capabilities, process lifetime, and the user's authority to approve a specific +operation. Model output, repository content, command output, fetched content, +Skill instructions, and MCP results are untrusted inputs. A malicious +dependency or child process is assumed able to close output pipes early, fork +descendants, create symlinks, inspect its environment, and attempt filesystem, +socket, or network escapes. + +The trusted computing base is the running CLI/Core binary, the verified +release support tree, the exact resolved Node executable, the selected sandbox +provider and operating-system enforcement, and host code that invokes direct +SDK helpers. A configured MCP server or native integration is a separately +trusted extension; missing or unsafe behavior annotations cause confirmation +rather than granting read-only status. + +Trust does not waive process ownership. Each local stdio MCP server leads a +dedicated Unix process group. Closing or dropping its transport stops pipe +tasks, clears pending requests, reaps the leader, and terminates descendants. +The stderr pipe is drained independently so diagnostics cannot stall protocol +traffic. + +This local boundary does not claim kernel-level protection against a compromised +sandbox provider, Node runtime, CLI binary, or operating system. It also does +not turn the privileged host-direct SDK into end-user authorization. Use a +stronger workload provider for hostile multi-tenant code, kernel attack +resistance, or OCI-level isolation. + +## Invocation Ingress + +Every session-owned path enters one scoped invocation kernel: + +| Ingress | Effective origin | Trust handling | +| --------------------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| Model tool call | Agent | Mode policy, permission, tool-owned escalation, and HITL apply. | +| Model-owned `batch`, `program`, workflow, or public custom nesting | Governed nested | Re-enters the same policy for every child. | +| Direct host call to an ordinary custom tool | Host direct at the top level | The selected top-level call is trusted; public nested calls become governed nested calls. | +| Direct host call to built-in `batch`, `program`, or dynamic workflow | Host direct, then trusted host-direct nested | Only host-selected built-in children retain control-plane authority. | +| Skill, Task, worker, or other model sub-run created from host context | Agent in the child | Ambient host-direct authority is removed before model dispatch. | +| Delegated child permission or confirmation | Governed nested/Agent | Child policy intersects the parent; parent `Ask` and tool-owned escalation stay with the parent provider. | +| Tool behavior metadata | Escalation on the current origin | It can require confirmation but cannot weaken `Ask` or `Deny`. | + +Low-level `ToolRegistry` and standalone `ProgramExecutor` APIs are deliberately +ungoverned building blocks for hosts that own the registry. AgentSession and +the TUI do not use those APIs as a fallback. + +## Complete TUI Decision Matrix + +The cells below are the terminal outcomes for model and ordinary governed +nested invocations. “Deny” means no confirmation event is created. “Confirm +once” means one exact invocation ID is pending; cancelling or expiring it +cannot settle another prompt. + +| Mode | Sandbox | Ordinary Bash | Explicit host Bash | Protected metadata mutation | Tool-owned confirmation | Annotated external side effect | +| ------- | ----------- | --------------------------------------------------------- | --------------------------------------------------------- | --------------------------- | ----------------------- | ------------------------------ | +| Default | Ready | Execute in sandbox | Confirm exact call once; execute on host only if approved | Confirm exact mutation once | Confirm once | Confirm once | +| Default | Unavailable | Confirm exact call once; execute on host only if approved | Confirm exact call once; execute on host only if approved | Confirm exact mutation once | Confirm once | Confirm once | +| Plan | Ready | Deny | Deny | Deny | Deny | Deny | +| Plan | Unavailable | Deny | Deny | Deny | Deny | Deny | +| Auto | Ready | Execute in sandbox | Deny | Deny | Deny | Deny | +| Auto | Unavailable | Deny | Deny | Deny | Deny | Deny | + +Origin modifies that matrix only as follows: + +| Origin/call shape | Matrix relationship | +| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Agent or ordinary governed nested | Uses the matrix exactly. | +| Direct host call to a custom tool | The selected top-level call skips model-facing permission/HITL; every public nested call returns to the matrix. | +| Trusted built-in host-direct nested | Skips model-facing permission/HITL because the host supplied the built-in orchestration and its children. Hooks, budget, timeout, cancellation, recursion checks, sandbox selection, and sanitization still apply. | +| Model sub-run born inside any host-direct call | Returns to the Agent row and uses the matrix exactly. | + +Decision precedence is fail-closed: active-Skill restrictions and hard +guardrails first, then hook blocks, origin authority, mode/permission policy, +tool-owned escalation, confirmation availability, and finally execution. +Terminal policies always use `TimeoutAction::Reject`; Core's generic +`AutoApprove` option is not accepted by the TUI. + +Treat the current SRT integration as a local enforcement provider, not as the +stack-wide execution contract. A3S Code owns agent policy and its +`BashSandbox` interface. A3S Runtime owns durable, provider-neutral Task and +Service lifecycle and placement. A3S Box owns OCI and stronger isolation. A +release-owned support payload keeps those boundaries and leaves the existing +capability/version handshake and Core contract unchanged. A3S Observer and A3S +Sentry can add execution evidence and adaptive runtime enforcement as defense +in depth; they do not replace the deterministic per-process sandbox boundary. + +## Permission Policy + +```ts +const session = agent.session('/repo', { + permissionPolicy: { + deny: ['bash(rm -rf*)', 'write(**/.env*)'], + ask: ['bash(git push*)', 'bash(npm publish*)'], + allow: ['read(*)', 'grep(*)', 'glob(*)', 'bash(cargo test*)'], + defaultDecision: 'ask', + enabled: true, + }, +}); +``` + +Avoid permissive defaults for release or production sessions. Make dangerous commands explicit and auditable. + +## Hooks + +Hooks are registered on a session with an event type, matcher, optional config, +and handler: + +```ts +session.registerHook( + 'observe-secret-read', + 'pre_tool_use', + { pathPattern: '**/.env*' }, + { priority: 100 }, + () => ({ action: 'continue' }), +); + +console.log(session.hookCount()); +session.unregisterHook('observe-secret-read'); +``` + +## Verification + +Verification reports and summaries are available from the session and selected +result fields. Release workflows should require tests, package checks, CI +checks, and provider evidence. diff --git a/website/docs/v6/en/guide/sessions.mdx b/website/docs/v6/en/guide/sessions.mdx new file mode 100644 index 00000000..1fe946cd --- /dev/null +++ b/website/docs/v6/en/guide/sessions.mdx @@ -0,0 +1,280 @@ +--- +title: 'Sessions' +description: 'Creating, streaming, resuming, and saving workspace-bound sessions' +--- + +# Sessions + +`Agent` owns configuration and provider state. `Session` binds that agent to one workspace and one conversation lifecycle. + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/repo', { + model: 'provider/model-id', + planningMode: 'enabled', + goalTracking: true, + autoDelegation: { enabled: true, maxTasks: 4 }, + autoParallel: false, +}); +``` + +## Rust Construction + +Rust session construction is async-first because default memory, file-backed +stores, queues, trajectory recording, and MCP discovery can require I/O: + +```rust +let session = agent + .session_builder("/repo") + .options(options) + .build() + .await?; +``` + +`session_async`, `resume_session_async`, `session_for_agent_async`, and +`session_for_worker_async` are direct async alternatives. Node and Python keep +their existing factory names and delegate to this async construction kernel +inside the native binding. + +The synchronous Rust `Agent::session` factory is a strict compatibility path. +It accepts explicitly pre-initialized resources and never starts or blocks an +async runtime. A default/file memory store, file session store, queue, +trajectory recorder, or any host MCP manager supplied in `SessionOptions` +returns `CodeError::AsyncSessionBuildRequired`; session-option MCP capability +discovery is always async. Use the builder instead of catching that error and +silently changing backends. The sync path can only inherit agent-global MCP +tools already cached during agent initialization. + +`planningMode` is explicit: use `'auto'` for the default structured pre-analysis +path, `'enabled'` to force planning, and `'disabled'` to turn planning off for +latency-sensitive calls. The legacy boolean `planning` option remains available +for compatibility. + +Planning emits run-scoped state. Hosts can render that state as a TaskList and +update each item as the runtime records progress instead of trying to infer +progress from text tokens. + +## Single-Flight Operations + +A session admits only one transcript-affecting operation at a time. `send`, +`stream`, their attachment variants, slash commands, and `resumeRun` share the +same fail-fast admission gate. An overlapping call returns +`CodeError::SessionBusy` before reading history or dispatching a command; it is +not queued behind the active operation. + +Wait for the active result, consume the stream to completion, or cancel it +before starting another conversation operation. The runtime retains a stream's +lease until its producer has actually stopped, even if the public stream handle +is dropped or aborted. Direct host tool helpers do not mutate the transcript and +do not claim this lease. + +Node and Python stream iterators wait for that lifecycle cleanup at the terminal +boundary. Once a fully consumed iterator reports completion, an immediate next +conversation operation will not inherit a stale busy state from that stream. + +## Send + +```ts +const result = await session.send( + 'Review this repository and list release blockers', +); + +console.log(result.text); +console.log(result.totalTokens); +console.log(result.verificationStatus); +``` + +## Stream + +```ts +const stream = await session.stream( + 'Run the focused tests and explain failures', +); + +while (true) { + const { value: event, done } = await stream.next(); + if (done) break; + if (!event) continue; + + if (event.text) process.stdout.write(event.text); + if (event.toolName) console.log('tool:', event.toolName); +} +``` + +Every SDK event is an `EventEnvelopeV1` projection with `version === 1`, an open +`type` string, complete `payload`, and optional `metadata`. Convenience fields +such as `text` and `toolName` are derived from that envelope. Keep a default +branch and retain the payload for future event types. + +## Side Questions + +The SDK does not expose a dedicated ephemeral-question helper. Snapshot the +current history and pass it explicitly to `send` or `stream`. Explicit history +is used for that call only; it does not append the answer back into the session +history. + +```ts +const snapshot = session.history(); +const answer = await session.send( + 'What files has this session already inspected?', + snapshot, +); + +console.log(answer.text); +console.log(session.history().length === snapshot.length); +``` + +## Resume + +```ts +import { Agent, FileSessionStore } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.resumeSession('release-review', { + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); +``` + +Set `autoSave: true` or call `await session.save()` when using a session store. +This resumes a saved session snapshot; interrupted run checkpoints use +`session.resumeRun(runId)` instead. See [Persistence](/guide/persistence). + +## Lifecycle and Close + +`session.close()` is a full graceful stop. The first call flips the session +into the **closed** state — further `send`/`stream` calls fast-fail with +`CodeError::SessionClosed` instead of starting a new run — then cancels the +active run, every in-flight delegated subagent task, and all pending +human-in-the-loop confirmations. Subsequent calls are no-ops and never panic. +Check the closed state with `session.isClosed()` (Node) / `session.is_closed()` +(Python). + +```ts +session.close(); +if (session.isClosed()) { + // send/stream now reject with CodeError::SessionClosed +} +``` + +```python +session.close() +if session.is_closed(): + # send/stream now reject with CodeError::SessionClosed + pass +``` + +### Cancellation token + +Every run derives its per-operation cancellation token from a single +session-level parent via `child_token()`, so `close()` cascades to all +in-flight work in one shot. Embedders that need the raw token — for example to +wire it into a host-side `select!` or to abort the session without the +run-store and hook side effects of `close()` — can clone it through +`AgentSession::session_cancel_token()`. + +### Agent-side registry + +The owning `Agent` tracks its live sessions by `Weak` reference (pruned +lazily) so a control plane can drive lifecycle without holding a session +handle: + +- `Agent::list_sessions()` returns the live session IDs (sorted, stable). +- `Agent::close_session(id)` closes one session by ID — the same cleanup as + `AgentSession::close()`, invoked out-of-band. +- `Agent::close()` closes every live session and tears down agent-owned + background resources (it also disconnects the global MCP connections). + After it returns, new `session` / `resumeSession` calls fail fast with + `CodeError::SessionClosed`. +- `Agent::is_closed()` reports whether the agent itself has been closed. + +```ts +const ids = await agent.listSessions(); +await agent.closeSession(ids[0]); +await agent.close(); // closes all remaining sessions + global MCP +console.log(agent.isClosed()); +``` + +```python +ids = agent.list_sessions() +agent.close_session(ids[0]) +agent.close() # closes all remaining sessions + global MCP +print(agent.is_closed()) +``` + +See CHANGELOG `[3.3.0]` — "Session / Agent lifecycle control". + +## Host Identity Labels + +`SessionOptions` carries four opaque identity fields that the host can attach +at session creation. The framework only transports them — it never interprets +or enforces them. They are propagated into `SessionData`, hooks, and traces, +and restored on resume, so the host can drive multi-tenant aggregation, +billing, and distributed tracing: + +| Node (camelCase) | Python (snake_case) | Meaning | +| ----------------- | ------------------- | ------------------------------------------------------------- | +| `tenantId` | `tenant_id` | Multi-tenant label | +| `principal` | `principal` | User / service that triggered the session | +| `agentTemplateId` | `agent_template_id` | Agent template / definition the session was instantiated from | +| `correlationId` | `correlation_id` | Distributed-trace correlation id | + +```ts +const session = agent.session('/repo', { + tenantId: 'tenant-example', + principal: 'principal-example', + agentTemplateId: 'agent-template-example', + correlationId: 'trace-example', +}); +``` + +```python +opts = SessionOptions() +opts.tenant_id = 'tenant-example' +opts.principal = 'principal-example' +opts.agent_template_id = 'agent-template-example' +opts.correlation_id = 'trace-example' +session = agent.session('/repo', opts) +print(session.tenant_id, session.principal) +``` + +See CHANGELOG `[3.3.0]` — "Host-provided identity labels". + +## Run Replay + +Each `send()` or `stream()` creates a run record. Applications can inspect those +records for UI state, audit, replay, cancellation, and tests: + +```ts +const runs = await session.runs(); +const latest = runs.at(-1); + +if (latest) { + console.log(await session.runSnapshot(latest.id)); + console.log(await session.runEvents(latest.id)); +} +``` + +`currentRun()` is for the operation that is current at the time of the call. +During an active `send()` or `stream()`, pass its `id` to `cancelRun(id)` to +request cancellation. When idle, `currentRun()` may return `null` or a retained run +snapshot depending on the preceding control flow, so use `runs()` for completed +history and inspect `status` before treating a snapshot as cancellable: + +```ts +const current = await session.currentRun(); +if (current?.id && current.status === 'running') { + await session.cancelRun(current.id); +} +``` + +## Agent Definitions + +`sessionForAgent()` applies a named agent definition from built-in agents, `.a3s/agents`, or configured `agentDirs`. + +```ts +const session = agent.sessionForAgent('/repo', 'explore', ['./agents'], { + planningMode: 'auto', +}); +``` diff --git a/website/docs/v6/en/guide/skills.mdx b/website/docs/v6/en/guide/skills.mdx new file mode 100644 index 00000000..42487d43 --- /dev/null +++ b/website/docs/v6/en/guide/skills.mdx @@ -0,0 +1,82 @@ +--- +title: 'Skills' +description: 'Prompt-time skills, inline skills, and the Skill tool' +--- + +# Skills + +Skills are reusable instructions that the harness can discover and rank. +File-backed skill directories, inline skills, explicit registries, and +`search_skills` share the same discovery path. A3S Code no longer ships default +embedded skills; `builtinSkills: true` is accepted by the SDKs as a compatibility +no-op. + +## Compatibility Flag + +```ts +const session = agent.session('/repo', { + builtinSkills: true, +}); +``` + +This flag does not add default skills. Put reusable behavior in project skill +files, inline skills, or agent definitions. + +## Skill Files + +Store Markdown files with frontmatter in a skill directory: + +```md +--- +name: release-review +description: Review release blockers and verification evidence +allowed-tools: 'read(*), grep(*), bash(cargo test*)' +--- + +Check package metadata, changelog, release scripts, and CI status. +Return blockers first. +``` + +```ts +const session = agent.session('/repo', { + skillDirs: ['/repo/.a3s/skills'], +}); +``` + +Use `skillDirs` for skill files. Use `agentDirs` for worker/subagent +definitions. + +## Inline Skills + +```ts +const session = agent.session('/repo', { + inlineSkills: [ + { + name: 'strict-release-review', + kind: 'instruction', + content: 'Always separate blockers from nice-to-have improvements.', + }, + ], +}); +``` + +## Skill Tool + +`search_skills` finds relevant skills: + +```ts +await session.tool('search_skills', { + query: 'release blockers', + limit: 5, +}); +``` + +Markdown skill files with `allowed-tools` frontmatter and inline skills are both +discoverable. Skill administration is an SDK or filesystem concern rather than +a model-visible management tool. + +When a skill is invoked through the `Skill` tool, `allowed-tools` is +fail-secure: omitted frontmatter grants no tools to that skill invocation. +Declare the smallest needed allow-list. Ordinary session tool calls are not +restricted by active skills unless `enforceActiveSkillToolRestrictions` is +explicitly enabled for legacy behavior. diff --git a/website/docs/v6/en/guide/tasks.mdx b/website/docs/v6/en/guide/tasks.mdx new file mode 100644 index 00000000..160e5876 --- /dev/null +++ b/website/docs/v6/en/guide/tasks.mdx @@ -0,0 +1,175 @@ +--- +title: 'Tasks' +description: 'Manual and automatic delegation with task, parallel_task, and subagents' +--- + +# Tasks + +The routine multi-agent path is the `task` and `parallel_task` tool pair. They keep child context isolated from the parent conversation and return compact results instead of full transcripts. + +The same delegation core also powers automatic subagent delegation. Enable it +when the runtime should proactively start specialist child agents for +high-confidence work, and disable only automatic parallel fan-out with +`autoParallel: false` when you want serial automatic delegation. + +## Built-in Subagents + +| Agent | Use it for | +| ----------------------------- | --------------------------------------------------------------------------------------- | +| `explore` | Read-only codebase search, file inspection, and structure discovery. | +| `plan` | Read-only implementation plans and architecture analysis. | +| `general` / `general-purpose` | Multi-step implementation work with read/write/command access. | +| `verification` | Focused checks, reproductions, regression validation, and adversarial testing. | +| `review` | Findings-first code review for correctness, regressions, security, and maintainability. | + +You can mention them explicitly, for example `@review`, `@agent-plan`, `use the verification subagent`, or `delegate to general-purpose`. + +## Manual Delegation + +Ask the parent agent to delegate a bounded job: + +```text +Use task to ask an explore agent to inspect the auth module. +Return files inspected, findings, risks, and confidence. +``` + +If the host already knows the task boundary, call the same core tool directly: + +```ts +const task = await session.task({ + agent: 'explore', + description: 'Inspect auth module', + prompt: 'Return files inspected, findings, risks, and confidence.', +}); +if (task.exitCode !== 0) throw new Error(task.output); +console.log(task.output); +``` + +A child agent should return a compact contract: + +- summary +- files inspected or changed +- evidence references +- risks and unknowns +- confidence + +The parent should not ingest the full child transcript. + +## Parallel Delegation + +Use `parallel_task` or `session.tasks(...)` when independent work can run concurrently: + +```text +Run parallel_task with three lanes: +1. inspect provider config parsing +2. inspect Node SDK declarations +3. inspect release scripts + +Merge the results into one release-readiness report. +``` + +```ts +const batch = await session.tasks([ + { + agent: 'explore', + description: 'Inspect config', + prompt: 'Check provider parsing.', + }, + { + agent: 'verification', + description: 'Verify SDK', + prompt: 'Check SDK declarations.', + }, +]); +if (batch.exitCode !== 0) throw new Error(batch.output); +console.log(batch.output); +``` + +`session.task(...)` and `session.tasks(...)` return `ToolResult` values from the +`task` and `parallel_task` tools. Read `output` for the compact child summary +and check `exitCode` before treating the result as successful. `maxParallelTasks` +in session options and `max_parallel_tasks` in ACL bound sibling fan-out. + +## Automatic Delegation + +Automatic delegation is opt-in. The runtime scores the current request against built-in and custom agent descriptions, then launches up to `maxTasks` child runs when confidence is high enough. + +```ts +const session = agent.session('/repo', { + autoDelegation: { enabled: true, minConfidence: 0.72, maxTasks: 4 }, + maxParallelTasks: 8, + autoParallel: false, +}); +``` + +```text +auto_delegation { + enabled = true + auto_parallel = false + min_confidence = 0.72 + max_tasks = 4 +} +``` + +`autoParallel: false` / `auto_parallel = false` is the global kill switch for automatic parallel child-agent fan-out. Manual `parallel_task` and `session.tasks(...)` remain available. + +## Agent Directories + +Load custom agent definitions through `agentDirs`, `agent_dirs`, or the built-in A3S directories: + +```ts +const session = agent.session('/repo', { agentDirs: ['./.a3s/agents'] }); +const loaded = session.registerAgentDir('./more-agents'); +``` + +A3S scans configured `agent_dirs`, project/user `.a3s/agents`, and Claude-compatible `.claude/agents` migration paths. Prefer `.a3s/agents` for new projects. + +Markdown agent files support frontmatter: + +```markdown +--- +name: docs-auditor +description: Use proactively after documentation changes +tools: Read, Grep, Glob +disallowedTools: + - Write + - Bash(rm:*) +--- + +Audit docs for drift, broken examples, and unclear migration notes. +``` + +The `tools` field is an allowlist. `disallowedTools` is a denylist and wins over allowed tools. Model routing fields are intentionally outside this compatibility layer. + +## Worker Agents + +Register disposable worker agents with `workerAgents` or `registerWorkerAgent()`: + +```ts +const session = agent.session('/repo', { + workerAgents: [ + { + name: 'frontend-worker', + description: 'Small verified frontend fixes', + kind: 'implementer', + model: 'provider/model-id', + maxSteps: 24, + confirmationInheritance: 'auto_approve', + }, + ], +}); +``` + +### Confirmation Inheritance + +Control how child runs resolve Ask decisions with `confirmationInheritance`: + +- `'auto_approve'` (default): child runs auto-approve all Ask decisions +- `'deny_on_ask'`: child runs fail immediately when encountering an Ask +- `'inherit_parent'`: child runs inherit the parent's confirmation policy + +Legacy lifecycle control-plane APIs are removed. Applications that need UI state should consume streaming events, run replay, and Node `cancelRun(runId)`. + +## Programmable orchestration + +Everything on this page is model-driven: `task` / `parallel_task`, `session.task(...)` / `session.tasks(...)`, and auto-delegation let the LLM decide when and how to fan out. When the host already knows the shape of the work and wants it to be deterministic and reproducible, express it programmatically instead with `session.parallel(...)`, `session.pipeline(...)`, and `session.parallelResumable(...)`. See [Orchestration](/guide/orchestration) for developer-expressed fan-out, barrier-free pipelines, and resumable/migratable workflows. diff --git a/website/docs/v6/en/guide/teams.mdx b/website/docs/v6/en/guide/teams.mdx new file mode 100644 index 00000000..87d4376b --- /dev/null +++ b/website/docs/v6/en/guide/teams.mdx @@ -0,0 +1,126 @@ +--- +title: 'Teams' +description: 'Building teams with agent definitions, task tools, and automatic delegation' +--- + +# Teams + +Teams are a harness pattern built from named agent definitions plus the `task` and `parallel_task` delegation core. A3S Code does not expose a separate team runner API; the parent session stays responsible for synthesis, policy, and final verification. + +## Recommended Shape + +1. Put durable roles in `.a3s/agents` or configure additional `agentDirs`. +2. Keep each role focused: description, prompt, allowed tools, denied tools. +3. Enable `autoDelegation` when the runtime should select high-confidence subagents. +4. Use `session.task(...)` or `session.tasks(...)` when the host already knows the lanes. +5. Merge child summaries, evidence references, and risks in the parent. + +```ts +const session = agent.session('/repo', { + agentDirs: ['./.a3s/agents'], + planningMode: 'auto', + autoDelegation: { enabled: true, maxTasks: 4 }, + maxParallelTasks: 8, + autoParallel: false, +}); + +await session.send(` +Build a release-readiness team: +- explorer: find risky changed areas +- tester: identify missing verification +- security: review side-effect paths + +Use independent subagents where useful and return a single prioritized report. +`); +``` + +## Custom Agent Files + +Markdown agent files use Claude-compatible frontmatter, with A3S-native placement under `.a3s/agents`: + +```markdown +--- +name: release-reviewer +description: Use proactively after release or CI changes +tools: Read, Grep, Glob, Bash(cargo test*) +disallowedTools: + - Write + - Bash(git push*) +--- + +Review release blockers first, then risks, then follow-up work. +``` + +A3S also reads `.claude/agents` as a migration source. Prefer `.a3s/agents` for new projects. + +## Built-in Team Roles + +Use these without creating files: + +- `explore`: read-only repository exploration +- `plan`: read-only implementation planning +- `general` / `general-purpose`: multi-step implementation +- `verification`: checks, repros, and regression validation +- `review`: findings-first code review + +## Manual Lanes + +SDK callers can call direct helpers when the host already knows the lanes: + +```ts +const result = await session.tasks([ + { + agent: 'explore', + description: 'Changed files', + prompt: 'Find risky changed files.', + }, + { + agent: 'verification', + description: 'Test gaps', + prompt: 'Find missing verification.', + }, + { + agent: 'review', + description: 'Regression review', + prompt: 'Review correctness risks.', + }, +]); + +if (result.exitCode !== 0) throw new Error(result.output); +console.log(result.output); +``` + +`session.tasks(...)` is the host-driven wrapper around `parallel_task`; it +returns a `ToolResult`, not a `StepOutcome[]`. Use `session.parallel(...)` when +you need one structured outcome per lane. + +When the team's lane structure is fixed and should be reproducible and resumable rather than model-chosen, use the programmable combinators in [Orchestration](/guide/orchestration) (`session.parallel` / `session.pipeline` / `session.parallelResumable`). + +## Worker Agents + +Register disposable worker agents with `workerAgents` or `registerWorkerAgent()` when the role is constructed by the host instead of stored on disk: + +```ts +const session = agent.session('/repo', { + workerAgents: [ + { + name: 'frontend-worker', + description: 'Small verified frontend fixes', + kind: 'implementer', + model: 'provider/model-id', + maxSteps: 24, + confirmationInheritance: 'auto_approve', + }, + ], +}); +``` + +Control how child runs resolve Ask decisions with `confirmationInheritance`: + +- `'auto_approve'` (default): child runs auto-approve all Ask decisions +- `'deny_on_ask'`: child runs fail immediately when encountering an Ask +- `'inherit_parent'`: child runs inherit the parent's confirmation policy + +## Runtime State + +Applications should observe streaming events and run replay snapshots, then cancel active Node work with `cancelRun(runId)` when needed. diff --git a/website/docs/v6/en/guide/telemetry.mdx b/website/docs/v6/en/guide/telemetry.mdx new file mode 100644 index 00000000..e631ac0e --- /dev/null +++ b/website/docs/v6/en/guide/telemetry.mdx @@ -0,0 +1,43 @@ +--- +title: 'Telemetry' +description: 'Trace events, verification summaries, and runtime observability' +--- + +# Telemetry + +A3S Code exposes runtime evidence through events, traces, verification reports, and result metadata. + +## Result Metadata + +```ts +const result = await session.send('Run release checks'); + +console.log(result.promptTokens); +console.log(result.completionTokens); +console.log(result.totalTokens); +console.log(result.toolCallsCount); +console.log(result.verificationStatus); +console.log(result.verificationSummaryText); +``` + +## Trace And Verification + +```ts +const trace = session.traceEvents(); +const reports = session.verificationReports(); +const summary = session.verificationSummary(); +const text = session.verificationSummaryText(); +``` + +## Streaming Events + +Streaming returns versioned `AgentEvent` envelopes. `version`, `type`, +`payload`, and optional `metadata` are the stable lossless fields; text, tool +names, tool output, errors, token totals, and verification summaries are +convenience projections. Unknown future `type` values and their payloads remain +available to hosts. + +## Logging + +For product telemetry, prefer structured trace events and verification reports +over scraping console output. diff --git a/website/docs/v6/en/guide/tools.mdx b/website/docs/v6/en/guide/tools.mdx new file mode 100644 index 00000000..5f08d492 --- /dev/null +++ b/website/docs/v6/en/guide/tools.mdx @@ -0,0 +1,303 @@ +--- +title: 'Tools' +description: 'Tool selection, direct tool calls, and verification evidence' +--- + +# Tools + +A3S Code keeps a registry of available tools. `toolNames()` returns the current +session tool surface. That surface is assembled from the workspace capability +set plus session-level integrations, so a non-local workspace can intentionally +hide tools it cannot service. + +## Tool Surface + +| Layer | Tools | Registration rule | +| ------------------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Workspace-gated builtins | `read`, `write`, `edit`, `patch`, `grep`, `glob`, `ls`, `bash`, `git` | Registered only when `WorkspaceServices` advertises the required capability. For example, a browser or S3 workspace can expose file tools while hiding `bash` or local git. | +| Runtime builtins | `web_fetch`, `web_search`, `batch`, `program` | Registered by the core tool executor. `batch` and `program` receive the current scoped invoker, so their usable inner tools depend on the session surface and keep the caller's governance scope. | +| Session bootstrap tools | `task`, `parallel_task`, `generate_object`, `search_skills`, `Skill` | Added while building an `AgentSession`. Delegation tools disappear when manual delegation is disabled; `generate_object` requires the configured LLM client; skill tools use the effective skill registry. | +| TUI workflow tools | `dynamic_workflow` and, after `/login`, optional host runtime tools | Registered by the `a3s code` host. `dynamic_workflow` powers `ultracode` and DeepResearch with A3S Flow replay; host runtime tools are login-gated integrations. | +| Dynamic integrations | `mcp____` and host-registered tools | Added from MCP managers or host code after discovery. | + +Use `toolNames()` / `toolDefinitions()` in tests or application diagnostics when +a workflow depends on a specific tool being visible. Tool visibility is not a +security grant. Model-selected tool calls inside `send`, `run`, and `stream` +pass through active-skill restrictions, permission policy, confirmation, hooks, +budget, queue/timeouts, cancellation, recursive-call protection, output +sanitization, artifact limits, and workspace path checks. Direct SDK calls such +as `session.tool(...)` are host control-plane calls with a different explicit +policy, described below. +Use `activeTools()` for a different question: which tool calls are currently +running in an active operation. + +## One Invocation Kernel + +The runtime attaches an origin to every invocation: + +| Origin | Examples | Permission / confirmation policy | +| -------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Agent | A model emits a tool call during `send` or `stream` | Full model-facing policy and HITL apply. | +| Governed nested | Inner calls from a model-owned `batch`, `program`, workflow, or public custom `InvocationRuntime` | Re-enters the model-facing policy and inherits the invocation stack, cancellation, budget, hooks, and sandbox. Ambient host-direct context cannot change this origin. | +| Host direct | `session.tool(...)`, typed read/write/git helpers, `session.program(...)`, and direct task helpers | Trusted control plane: model-facing permission/HITL are skipped because the host selected the call. | +| Trusted host-direct nested | A built-in `batch`, `program`, or dynamic workflow that the host called directly invokes a host-selected child | Retains trusted control-plane authority for that built-in nested operation. Third-party tools cannot construct this origin through the public API. | + +Model sub-runs created by a host-direct Skill, Task, or custom tool start again +with the Agent origin. A public custom tool can make nested calls only through +`InvocationRuntime`, which always creates the governed nested origin. This +prevents one direct call to arbitrary extension code from becoming a reusable +authority token. + +All origins use the same invocation kernel. Pre-hooks can block them; +budget checks run before side effects; queue/timeout, cancellation, recursive +invocation protection, post-hooks, and security-provider output sanitization +remain active. A nested call cannot fall back to a raw registry while a scoped +invoker is installed. + +The host-direct policy is not an end-user authorization system. The embedding +application must authenticate and authorize a user before translating their +request into a direct SDK helper. Closing the session cancels an in-flight +host-direct tool through the session cancellation scope. + +## Bounded Tool Contracts + +Governed agent, nested, and session calls validate arguments against each +tool's cached JSON Schema before confirmation or side effects. Tools also +publish per-invocation scheduling capabilities: read-only, idempotent, +resumable, cancellation-safe, pagination support, maximum parallelism, and +output kind. + +`read`, `ls`, `glob`, Git log/list/diff, and `web_fetch` return explicit +continuation cursors or offsets. Shell capture is byte-bounded across both +streams, retains the beginning and end with exact accounting, and still +exposes stdout and stderr separately to a sandbox host. A command deadline +includes both output draining and `child.wait()`, so closing both pipes cannot +evade it. Timeout and cancellation terminate the complete Unix process group. +Large file changes replace full before/after event payloads with bounded +previews and unified diff text plus hashes, sizes, and artifact references. + +`batch` accepts at most 32 calls and applies at most 16-way concurrency. It +fans out only when every child declares safe read-only, idempotent behavior; +mutating and unknown tools are serialized. A partial batch identifies failed +indices while treating the orchestration as completed, so callers retry only +failed items. `parallel_task` has the same 32-task bound and settles cancelled +children before publishing terminal state. + +`web_search` reports `complete`, `partial`, or `failed` in metadata. Empty +results with engine errors are failures rather than successful empty searches. +Timeout, cancellation, invalid-argument, partial-failure, and rate-limit errors +carry structured error kinds. + +## Structured Output with `generate_object` + +The `generate_object` tool asks the configured LLM for a JSON object, validates +the response against a JSON Schema, and returns the validated object only on a +zero-exit result. It works in two ways: + +1. **Agent-driven**: The LLM sees `generate_object` in its tool list and calls it autonomously when structured output is needed. +2. **Direct call**: Your application calls `session.tool('generate_object', ...)` to bypass model-driven tool selection. The tool itself still calls the configured LLM. + +```ts +const result = await session.tool('generate_object', { + schema: { + type: 'object', + required: ['sentiment', 'confidence'], + properties: { + sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] }, + confidence: { type: 'number', minimum: 0, maximum: 1 }, + }, + }, + prompt: 'Classify: "This product is amazing!"', + schema_name: 'sentiment', + mode: 'tool', + max_repair_attempts: 2, // retries if schema validation fails +}); + +if (result.exitCode !== 0) { + throw new Error(result.output); +} + +const { object } = JSON.parse(result.output); +// object = { sentiment: "positive", confidence: 0.95 } +``` + +### Parameters + +| Parameter | Type | Required | Description | +| --------------------- | ------- | -------- | ------------------------------------------------------------------------ | +| `schema` | object | yes | JSON Schema used to validate the output | +| `prompt` | string | yes | What to generate or extract | +| `schema_name` | string | no | Short name (default: "result") | +| `schema_description` | string | no | Description for the synthetic tool | +| `system` | string | no | Optional system prompt | +| `mode` | string | no | `"auto"` / `"strict"` / `"json"` / `"tool"` / `"prompt"` (default: auto) | +| `max_repair_attempts` | integer | no | 0–5 (default: 2) | +| `timeout_ms` | integer | no | Independent generation deadline, 1,000–600,000 ms (default: 120,000) | + +### Modes + +- **tool**: Injects a synthetic tool whose parameters are the schema. This is the current cross-provider default. +- **prompt**: Appends schema instructions to the prompt. It is useful as a prompt-only fallback, but depends more on model compliance. +- **auto**: Currently resolves to `tool`. +- **strict** / **json**: Accepted by the API, but currently resolve to `tool` because provider-level `response_format` wiring is not enabled in this runtime path. + +### Streaming + +When called through `session.stream()`, partial objects are emitted as +`tool_output_delta` events. Snapshots are rate-limited to one event per 100 ms; +objects over the event budget emit byte accounting instead of duplicating the +full value in the stream: + +```ts +const stream = await session.stream('Extract all invoices...'); + +while (true) { + const { value: ev, done } = await stream.next(); + if (done) break; + if (!ev) continue; + + if (ev.type === 'tool_output_delta' && ev.toolName === 'generate_object') { + const { object_partial } = JSON.parse(ev.text); + renderProgress(object_partial); + } +} +``` + +### Repair Loop + +If the LLM output fails schema validation, the tool automatically retries by +feeding the validation errors back to the model. This handles edge cases like +missing required fields or wrong enum values without application-level retry logic. + +## Direct Tool Calls + +SDK callers can call deterministic tools directly: + +```ts +const files = await session.glob('src/**/*.rs'); +const hits = await session.grep('PermissionPolicy'); +const status = await session.git('status'); +const output = await session.bash('cargo test -p a3s-code-core'); +const raw = await session.tool('read', { file_path: 'README.md' }); +const schemas = session.toolDefinitions(); +const hitLines = hits.split('\n').filter(Boolean).length; + +console.log(files.length, hitLines, output.length, schemas.length); +console.log(status.output); +console.log(raw.output); +``` + +Direct calls execute inside the session workspace and should be treated as +privileged host operations. They do not claim the session's single-flight +conversation lease because they do not update transcript history. + +For delegated child work, use SDK helpers over the same core tools: + +```ts +await session.task({ + agent: 'explore', + description: 'Find auth files', + prompt: 'Inspect auth-related files and return a compact evidence list.', +}); + +await session.tasks([ + { agent: 'explore', description: 'Find tests', prompt: 'Locate auth tests.' }, + { + agent: 'verification', + description: 'Check risk', + prompt: 'Review auth edge cases.', + }, +]); +``` + +Automatic subagent delegation also uses these core tools. `autoParallel: false` +disables only automatic parallel fan-out; it does not remove `parallel_task` or +`session.tasks(...)`. + +## Programmatic Tool Calling + +PTC is the next step beyond a single direct call. The `program` tool runs a sandboxed JavaScript script in an embedded QuickJS VM. The script defines `async function run(ctx, inputs)` and replaces repeated model-tool turns with one bounded program. + +Instead of spending LLM turns on: + +```text +grep -> read -> grep -> read -> summarize +``` + +the model can ask `program` to run a script: + +```js +// search-auth.js +export default async function run(ctx, inputs) { + const hits = await ctx.grep(inputs.query, { glob: '*.rs' }); + const files = await ctx.glob('crates/**/*.rs'); + const snippets = []; + + for (const file of files.slice(0, 20)) { + const content = await ctx.readFile(file); + if (content.includes(inputs.query)) { + snippets.push({ file, preview: content.slice(0, 1200) }); + } + } + + return { + summary: `Found ${snippets.length} candidate files for ${inputs.query}`, + evidence: snippets, + rawSearch: hits, + }; +} +``` + +Run it through the SDK helper with either inline `source` or a workspace-relative `.js` or `.mjs` file path: + +```js +await session.program({ + path: 'scripts/ptc/search-auth.js', + inputs: { query: 'PermissionPolicy' }, + allowedTools: ['grep', 'glob', 'read'], + limits: { + timeoutMs: 30000, + maxToolCalls: 30, + maxOutputBytes: 65536, + }, +}); +``` + +`session.program(...)` is equivalent to `session.tool('program', { type: 'script', language: 'javascript', ... })` but uses SDK-native naming. When `allowedTools` / `allowed_tools` is omitted, the script can call every registered tool except `program`. Provide an allow-list when a workflow should run with a smaller capability surface. + +The QuickJS VM receives no filesystem, network, subprocess, or environment permissions. The only useful capabilities are the `ctx` methods wired back to A3S Code tools. PTC returns a readable `ToolResult.output` and structured data in `ToolResult.metadataJson`. Keep large raw output out of the prompt; summarize findings, evidence refs, risks, and suggested next actions. + +In `a3s code`, PTC is also used by `DynamicWorkflowRuntime`. Recursive +`program`, `dynamic_workflow`, and `parallel_task` calls are kept out of the +default TUI PTC allow-list. When a dynamic workflow needs local parallel +subagents, it schedules a Flow step named `parallel_task`; the TUI host runs +the native `parallel_task` implementation outside QuickJS. After `/login`, +dynamic workflow PTC steps may call `ctx.tool("runtime", ...)` when a host +runtime tool is registered in the session. + +## Tool Results + +`ToolResult` includes `name`, `output`, `exitCode`, and optional `metadataJson`. +Direct `session.tool(...)`, `session.program(...)`, `session.git(...)`, +`session.writeFile(...)`, `session.ls(...)`, `session.editFile(...)`, and +`session.patchFile(...)` calls return `ToolResult`. The typed read/search/shell +helpers return simpler values: `readFile`, `grep`, and `bash` return strings, +while `glob` returns a string array. Long outputs should be summarized before +they are fed back to the model. + +## Verification + +Use verification commands to turn "done" into evidence: + +```ts +const report = await session.verifyCommands('release readiness', [ + { + id: 'unit', + kind: 'test', + description: 'Run core tests', + command: 'cargo test -p a3s-code-core', + required: true, + timeoutMs: 120000, + }, +]); +``` diff --git a/website/docs/v6/en/guide/tui.mdx b/website/docs/v6/en/guide/tui.mdx new file mode 100644 index 00000000..995328cf --- /dev/null +++ b/website/docs/v6/en/guide/tui.mdx @@ -0,0 +1,618 @@ +--- +title: 'A3S Code TUI' +description: 'Install, configure, and operate the a3s code terminal workspace' +--- + +# A3S Code TUI + +`a3s code` is the interactive terminal workspace for A3S Code. It is shipped by +the [`a3s` CLI](https://github.com/A3S-Lab/Cli), drives the Rust +`a3s-code-core` runtime, and renders the runtime event stream with +[`a3s-tui`](https://github.com/A3S-Lab/TUI). + +Use the TUI when you want the ready coding-agent product in a terminal. Use +`a3s-code-core` when you are embedding the same runtime in another Rust host, +runner, IDE bridge, or controlled product surface. + +## Surface Map + +| Surface | Repository | Responsibility | +| --------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `a3s-code-core` | [A3S-Lab/Code](https://github.com/A3S-Lab/Code) | Rust runtime crate for sessions, tools, memory, permissions, delegation, persistence, verification, and event replay. | +| `a3s code` | [A3S-Lab/Cli](https://github.com/A3S-Lab/Cli) | Terminal coding-agent application that drives `a3s-code-core` sessions. | +| A3S Use | [A3S-Lab/Use](https://github.com/A3S-Lab/Use) | Independently released Browser, native Office, built-in OCR, optional Office compatibility, and signed external application capabilities projected into Code through standard MCP and Skills. | +| `a3s-tui` | [A3S-Lab/TUI](https://github.com/A3S-Lab/TUI) | Terminal UI framework used by the CLI. It is the renderer and component library, not the agent runtime. | +| A3S Flow | [A3S-Lab/Flow](https://github.com/A3S-Lab/Flow) | Durable workflow engine used by `DynamicWorkflowRuntime` for replayable per-turn dynamic workflows. | +| A3S monorepo | [A3S-Lab/a3s](https://github.com/A3S-Lab/a3s) | Product docs, release orchestration, submodule pins, and related crates. | + +## Install And Start + +Install the CLI, then run `a3s code` from the workspace the agent should +inspect: + +```bash +brew install A3S-Lab/tap/a3s + +# or from crates.io +cargo install a3s + +# or from the CLI repository +cargo install --git https://github.com/A3S-Lab/Cli +``` + +```bash +a3s code +a3s code resume +a3s code resume +a3s code update +``` + +The top-level `a3s update` command reaches the same updater as TUI `/update`. + +## First Launch And Config + +The TUI discovers config in this order: + +1. `A3S_CONFIG_FILE` +2. `.a3s/config.acl` while walking upward from the current directory +3. `~/.a3s/config.acl` + +On first launch, when no config exists, the CLI creates a starter +`~/.a3s/config.acl` and opens it in the built-in editor. Use `/config` later to +edit the active ACL file from inside the TUI. + +Project-local config can set models, providers, an optional A3S OS endpoint +with `os = "https://..."`, `flow_dir`, `agent_dir`, `mcp_dir`, `skill_dir`, +storage, memory, delegation, and asset paths. Keep real keys, private provider +URLs, tenant identifiers, and user-specific paths out of committed examples. +Resolve credentials through environment variables: + +```text +default_model = "provider/model-id" +max_parallel_tasks = 8 +os = env("A3S_OS_URL") + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "model-id" { + tool_call = true + limit = { + context = 128000 + output = 4096 + } + } +} +``` + +## Capability Overview + +`a3s code` combines the coding chat loop, file and config editing, durable +context, memory, local asset development, optional host integrations, runtime +fan-out, trusted runtime views, and engineered automation loops. + +| Area | What the TUI provides | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Coding loop | Chat with the agent, stream reasoning/text/tool events, approve or deny gated tools, switch `/auto`, run direct shell turns with `!`, set a persistent `/goal`, clear context, and fork sessions. | +| Workspace UI | `/ide` opens a file tree and editor, while `/config` edits the active config in the same surface. Both use terminal-safe type-aware file/folder sigils, semantic icon colors, icon-bearing breadcrumbs, and a ruled line-number gutter. `Ctrl+T` opens the complete semantic transcript, and file edits render bounded diffs through shared TUI components. | +| Models | `/model` switches configured ACL providers and signed-in account-backed model tabs when available. | +| Effort | `/effort` changes thinking budget, tool-round budget, continuation count, and rigor guidance from `low` through `max` and `ultracode`. | +| Tools and safety | File, search, shell, git, web, structured-output, MCP, PTC `program`, `task`, and `parallel_task` tools all pass through workspace boundaries, permission policy, HITL approval, timeouts, hooks, and traces. | +| Application capabilities | A3S Use is prepared on first TUI use when policy allows, then projects built-in Browser, native Office, OCR, optional OfficeCLI compatibility, and installed signed external domains into a dedicated restricted `use` worker. | +| Context and memory | The footer tracks context fill and auto-compaction. `/ctx` searches past sessions, `/ctx ` attaches a transcript window, `/ctx save ` promotes it to memory, `/sleep` consolidates the day, and `/memory` browses durable memories as an event/entity graph. | +| Dynamic workflows | `ultracode` and `?` DeepResearch can use `DynamicWorkflowRuntime`, a local A3S Flow-backed runtime that records workflow and step history while sandboxed PTC scripts perform tool work. | +| Parallel work | Local fan-out uses native host-side `parallel_task`. Dynamic workflows schedule a Flow step named `parallel_task` when they need local parallel subagents; QuickJS/PTC scripts do not call `parallel_task` directly. | +| Optional runtime tools | A3S OS can register runtime tools after `/login`; local tools and `parallel_task` remain available without an account. | +| Deep research | Prefix a prompt with `?` to start the standalone evidence-first engine. Exact-query bootstrap and bounded semantic planning run concurrently, fetched evidence is published before optional synthesis, and generic fallback never depends on a topic or named entity. | +| Asset development | `/agent`, `/mcp`, `/skill`, and `/okf` enter local development modes with an active asset, review commands, clone/draft flows, and publish/deploy/status surfaces. | +| Workflow assets | `/flow` selects or drafts workflow DAG files for local review and optional host publication. | +| Knowledge | `/kb` manages a local personal knowledge vault. `/okf` manages shareable OKF knowledge-package assets. | +| Engineered loops | `/loop init`, `/loop run`, `/loop audit`, and `/loop logs` manage durable maker/checker loops under `.a3s/loops` with reports, budgets, state files, and optional runtime/view evidence. | +| Operations | `/help` opens the command guide, `/theme` changes syntax themes, `/plugin` and `/reload` refresh skills/plugins, `Open view` reopens the latest validated local report or trusted runtime view when shown, and `/update` upgrades and restarts the CLI. | + +DeepResearch writes progressively publishable artifacts to +`.a3s/research/artifacts//report.md` and +`.a3s/research/artifacts//index.html`. The independent +`a3s-deep-research` crate owns orchestration, evidence admission, typed +claim-graph compilation, quality gates, and rendering; CLI, TUI, and A3S Web +share one typed runner and supply only product adapters. + +The request language is pinned across planning, admission, and publication. +Comprehensive publication requires report-wide volume and source diversity plus +a multi-step analytical chain in every resolved material dimension: multi-source +facts, comparison, mechanism or trade-off analysis, cross-source synthesis, and +a supported implication or applicability boundary. Repeated openings, +near-duplicate claims, and source-by-source summaries do not satisfy that gate. +A closed narrative plan selects natural headings and paragraph groups without +changing evidence; rendering uses continuous prose and keeps traceability in a +collapsed disclosure. + +The self-contained HTML follows A3S Web's design tokens with a sticky left +action menu, centered editable report, sticky right table of contents, save and +print actions, and a responsive stacked layout. Each run also records a bounded +`.a3s/research/runs//journal-v2.jsonl` projection without absolute +artifact paths. Complete material coverage publishes `synthesized`; supported +incomplete work may publish depth-gated `qualified`; failed synthesis preserves +`source_backed`; and an empty safe-source set produces `no_evidence`. + +## Browser, Office, And OCR Through A3S Use + +A3S Use is an independently released first-use component. Before terminal +takeover, `a3s code` reuses a healthy install or installs the verified release +when networking and automatic setup are allowed. `--offline`, +`A3S_OFFLINE=1`, and `A3S_NO_AUTO_INSTALL=1` remain strict no-mutation +boundaries. Setup failure is non-fatal and remains visible through `/use`. +Runtimes and model assets can also be prepared explicitly: + +```bash +a3s install use --source release +a3s install use/browser + +# Optional OfficeCLI compatibility provider. Native Office is built into Use. +a3s install use/office + +# Install or repair the pinned local PP-OCRv6 models. +a3s install use/ocr +a3s use ocr doctor --json +``` + +When the parent Use binary is ready, Code consumes its versioned +capability registry and watches both generation and content revision. Provider +changes and extension enable, disable, install, or upgrade events then hot-plug +their MCP and verified `SKILL.md` surfaces into the current session. If +first-use setup was disabled or failed, install the parent explicitly and +restart Code once; later capability changes do not require a restart. + +Use `/use` or `/use status` to inspect the binary path/version, registry +convergence, provider readiness, MCP connection/tool count, and verified/loaded +Skills. `/use repair` prints non-destructive repair guidance only. It never +runs an installer or changes extension state. + +The primary coding model intentionally does not receive raw +`mcp__use_*` definitions. It sees a dedicated `use` worker through `task` and +delegates application work to that worker. The worker can see only +`mcp__use_*`; it cannot use shell, workspace, unrelated MCP, or recursive +delegation tools. Managed Skills provide domain guidance but cannot widen this +permission boundary. Ask naturally, for example, “Use Browser to inspect this +page,” “Use Office to update this workbook,” or “Use OCR on this scan.” If a +capability is unavailable, the worker reports the typed failure instead of +falling back to another tool. The default Browser surface includes doctor and a +bounded installer; missing managed Browser files can therefore be prepared +inside the Use worker only after parent TUI confirmation. + +The projected routes are intentionally distinct: + +| Capability | Code surface | Provider behavior | +| ------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Browser | `mcp__use_browser__*` | Uses a discovered browser or requests the managed provider through a mutation that requires parent HITL. Closed-world session inspection can run without an extra prompt; navigation, network/open-world reads, input, clicks, and submits require HITL. | +| Native Office | `mcp__use_office__*` | Uses the bounded native Office package kernel and its revision/conflict checks. Reads are closed-world; mutations and destructive saves require HITL. | +| Built-in OCR | `mcp__use_ocr__*` | Runs the pinned `PP-OCRv6_small` detection and recognition models locally through ONNX Runtime. Doctor and extraction are closed-world read-only tools; source bytes never leave the device. | + +MCP behavior metadata is escalation-only. An operation is frictionless only +when it declares `readOnlyHint=true`, `openWorldHint=false`, and is not +destructive. Missing annotations, open-world access, mutation, destruction, or +submit risk asks the parent TUI; a parent denial remains authoritative. Office +mutations are never retried automatically. In particular, +`use.office.outcome_unknown` means the change may already have been applied, so +the worker preserves the evidence and stops. + +Structured MCP output is not flattened away: Code retains output schemas, +`structuredContent`, images, text/blob resources, protocol metadata, and +SHA-256-addressed bounded artifacts for the normal transcript/tool-result path. + +## Browser, Office, And OCR Through A3S Use + +A3S Use is an independently released first-use component. Before terminal +takeover, `a3s code` reuses a healthy installation or installs the verified +release when networking and automatic setup are allowed. `--offline`, +`A3S_OFFLINE=1`, and `A3S_NO_AUTO_INSTALL=1` remain strict zero-network, +zero-receipt boundaries. Setup failure is non-fatal; install the parent +explicitly and restart Code once if first-use preparation was disabled or +failed. + +When Use is ready, Code consumes its versioned registry and gives the initial +MCP projection a separate bounded window. Browser, native Office, OCR, and +installed extension routes are exposed only through a dedicated `use` worker. +The primary model never sees raw `mcp__use_*` tools, and the worker cannot use +the workspace, shell, unrelated MCP servers, or recursive delegation. Managed +Skills provide domain guidance without widening that boundary. + +## Interaction Model + +The main screen is an event-driven transcript. User messages, model text, +reasoning deltas, tool starts, streamed tool output, approvals, subagent +progress, plans, memory events, and final summaries arrive as structured +runtime events and are rendered incrementally. + +Assistant Markdown is source-backed and newline-gated. Stable rows are paced +through an adaptive commit queue, deep or stale queues catch up automatically, +and active tables remain in a replaceable tail until completion. Tail updates +reuse the wrapped transcript prefix; terminal resize still reflows both regions +from raw source without truncating headings, code, URLs, graphemes, or cells. + +| Input | Behavior | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Normal prompt | Sends a coding-agent turn. If a turn is busy, the input is queued. | +| `/queue` | Opens the pending follow-up queue with stable selection, Send now, remove, and explicitly confirmed clear actions. | +| `/history` | Fuzzy-searches prompts from the current session and restores the selected text. | +| `/copy` / `/copy transcript` | Copies the latest assistant source Markdown or the complete semantic session. | +| `/export [path]` | Atomically creates a no-clobber Markdown session file inside the workspace. | +| `/tasks` | Opens live delegated-task control with search, progress/output inspection, refresh, and confirmed cancellation. | +| `/permissions` | Searches exact session/project grants, opens canonical arguments, and revokes only after a second matching confirmation. | +| `! ` | Runs a direct shell turn through the same workspace output surface. | +| `? ` | Starts exact-query bootstrap and bounded semantic planning, stages a source-backed report, then optionally compiles a typed claim graph into synthesized or qualified output. | +| `@` | Attaches a workspace file through the file picker path. | +| `/` | Opens the command palette and slash command registry. | +| `Shift+Enter` | Inserts a newline in the input. | +| `Ctrl+O` | Sends the current draft now by cancelling and settling the active turn before consuming it. | +| `Shift+Tab` | Cycles default, plan, and auto modes. | +| `PgUp` / `PgDn` | Scrolls the transcript or active full-screen panel. | +| `Shift+End` | Jumps to the latest transcript output. | +| `Ctrl+T` | Opens the complete live semantic session transcript, including user and assistant messages, plans, every tool lifecycle and full tool output, subagent state, and the current streaming tail. | +| `Ctrl+R` | Opens prompt-history search; repeated presses cycle matching prompts. | +| `Ctrl+B` | Opens or closes delegated-task control without interrupting the parent turn. | +| `Esc` | Interrupts the running turn or closes the active panel. | +| `Ctrl+C` twice | Quits after session persistence runs. | + +The UI keeps long-running work observable. The transcript can show streamed +model text, tool input/output, progress deltas, approvals, runtime view +buttons, dynamic-workflow artifacts, subagent activity, queue entries, memory +events, and context-fill warnings. `Ctrl+T` opens the complete live semantic +transcript, including the current Markdown tail and full tool output. The +`/tasks` panel reads the current session's authoritative task snapshots while +the parent turn keeps streaming. It keeps every running task plus bounded +recent history, refreshes every second, preserves selection across reordering, +searches task metadata, progress, and output, opens full details, and requires +a second matching cancellation action. The standalone `a3s top` command shows +local process activity outside the TUI. + +`/permissions` remains available while the parent turn streams. It separates +ephemeral session grants from project grants stored in +`.a3s/permissions.acl`, preserves selection while filtering, and opens the +authoritative canonical arguments. `X` or Delete must be repeated on the same +row before revocation. Both scopes update the live checker immediately; project +removal then performs an atomic `a3s-acl` rewrite and restores the grant if +persistence fails. Revocation affects future checks only and does not cancel +tools already running. + +Pending follow-ups use Lane priority and FIFO metadata, while each row retains +its submission-time execution mode, attachments, and Plan state. `/queue` +selects rows by stable sequence: Up/Down or the wheel moves selection, Enter or +`S` sends the selected row now, Delete or `D` removes one row, and `C` opens a +separate clear confirmation. Esc closes the modal without changing the +composer draft. During a live turn, Enter on an empty composer sends the queue +head now. + +Prompt recall remains available through Up/Down. For longer sessions, +`/history` or `Ctrl+R` opens a fuzzy-searchable catalog capped at 100 matches. +Results rank by relevance and recency, repeated prompts retain distinct +positions, repeated Ctrl+R cycles matches, and Enter or Tab restores the +selected prompt. Esc closes the panel without changing the existing draft. + +`/copy` selects the current live assistant tail when one exists, then falls +back to the latest committed assistant response. `/copy transcript` uses the +same stable semantic Markdown projection as `/export`: user and assistant +messages, visible tool lifecycle/output, and visible delegated-task results. +Private reasoning, transient UI notices, terminal-width-dependent rows, and +hidden duplicate cells are excluded. Native clipboard delivery is reported +only when verified; otherwise the TUI describes the OSC 52 request and its +64,000-byte UTF-8 payload bound. + +`/export` generates a unique session-and-time filename in the workspace root. +An optional path is interpreted relative to the workspace, may target an +existing directory, and is created atomically with private permissions. Parent +traversal, escaping symlinks, missing parent directories, and existing targets +are rejected instead of overwriting or writing outside the workspace. + +## Sessions And Safety + +Core session snapshots auto-save under +`/.a3s/tui/sessions/v1/sessions`; TUI-owned per-session state is +stored under `/.a3s/tui/session-state/v1`. Exiting prints the complete +`a3s code resume ` command and highlights it when color output is +enabled. `a3s code resume` without an id resumes the newest saved session in the +current workspace. Resume restores the selected model and credential source, +effort profile, execution mode (`default`, `plan`, or `auto`), and syntax theme. +`/fork` copies the current transcript into a new session id while keeping the +original. `/fork worktree` also creates an `a3s/fork-` branch in a sibling +`.a3s-worktrees` directory, transfers tracked and untracked workspace content +through a binary patch, and copies the complete session into that isolated +workspace. It excludes TUI persistence from the patch, never changes the real +Git index, and prints the exact command for opening the fork. + +Ordinary user turns retain bounded pre/post Git tree checkpoints. `/rewind` +forks the pre-turn conversation and reverses the last file patch only after a +whole-patch conflict check succeeds. A later edit to a touched file causes a +full refusal instead of an overwrite. `HEAD`, the real Git index, and the +original session remain unchanged. In a non-Git workspace, conversation rewind +still works and reports that file rewind was unavailable. `/clear` starts a +fresh conversation. + +If exit interrupts a durable `/goal`, the goal is saved as paused. The resumed +TUI presents `Resume goal` and `Leave paused`: resume continues the next goal +iteration without changing the restored execution mode, while leave enters the +session with the goal paused so `/goal resume` can continue it later. + +The TUI owns human-in-the-loop approval, but approval is a sandbox-boundary +event rather than a tool-category event: + +| Mode | Quiet execution | Boundary crossing | +| ------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Default | Workspace file mutations, governed orchestration, and ordinary Bash inside the installed local process sandbox. | Explicit host Bash escalation, Bash when no process sandbox is available, protected control metadata, mutating Git operations, and annotated external side effects enter HITL. | +| Plan | Read-only workspace and web discovery only. | Mutations, Bash, and escalation requests are denied. A completed plan is reviewed before implementation starts as a separate Default turn. | +| Auto | The same bounded workspace mutations and process sandbox as Default. | Auto never opens HITL. Missing-sandbox Bash, host escalation, protected control metadata, and hard policy denials fail closed. | + +Shift+Tab cycles Default, Plan, and Auto. A running or queued turn keeps the +mode captured when it was submitted. Permission and confirmation routing are +snapshotted at run admission; delegated, parallel, Skill, and background +descendants keep that snapshot after the composer advances to another mode. +Tool execution timeouts and confirmation timeouts are tracked separately, so +waiting for a human does not consume the command runtime budget. + +`/permissions` makes remembered grants inspectable and revocable. Session and +project scopes remain distinct, exact arguments are available through Enter, +and a second matching `X` or Delete is required. Project changes use an atomic +`a3s-acl` rewrite; revocation affects future checks only. + +All local filesystem work stays under active workspace services and A3S Code +permission policy. Local chat, file edits, subagents, MCP, memory, asset +drafting, and `DynamicWorkflowRuntime` work without `/login`. Account-backed +runtime tools, hosted asset publishing, trusted view links, and hosted activity +panels are available only when a host explicitly configures and registers them. + +## Tool Runtime + +A3S Code TUI exposes tools through the session registry, not by letting the +model run arbitrary host APIs. Each tool call carries a name, JSON arguments, +streamed output, timeout policy, permission decision, and traceable event id. + +### Local process sandbox + +Before terminal takeover, the TUI reuses a verified user-wide managed SRT +installation or the exact support tree carried by an official CLI archive. The +release tree is accepted only after its package graph, file types, size bounds, +and complete normalized digest match the CLI. It requires Node.js 20.11 or +newer, performs the Core capability handshake, and does not select a global +`srt` installation. + +There is no silent host fallback. Offline mode and +`A3S_NO_AUTO_INSTALL=1` may use verified state or the release payload without +mutation. Source and Cargo installs without that payload may use a fixed npm +development bootstrap only when automatic first-use setup is allowed. If no +verified boundary is available, Default asks before running an exact Bash +invocation on the host and Auto denies Bash. With the managed runtime, routine +commands inherit an OS-enforced boundary that: + +- denies outbound networking, local listeners, and Unix sockets; +- limits writes to the workspace and a private per-run scratch directory; +- keeps repository, A3S, agent, editor, and tool control metadata read-only; +- blocks reads from common credential stores and removes ambient secrets from + the command environment; +- denies existing `.env*` files at every governed source-tree depth and masks + pre-existing multi-link source files for both reads and writes; +- preserves bounded streaming output, timeout, cancellation, and process-group + cleanup. + +The TUI also enables Core's local workspace credential policy for built-in +file operations. Direct and range reads, writes, edits, patches, and both +manifest-backed and fallback grep enforce the same sensitive-path and +source-hardlink rules. Explicit sensitive targets fail closed and broad grep +filters them without exposing matching content. Ordinary package-store +hardlinks remain readable unless they alias a discovered credential inode. +Read-only Git diff is regenerated only for allowed changed paths; +option-like revisions cannot become Git flags, and displayed remotes omit +embedded HTTP credentials and query tokens. + +Delegated tasks, Skills, and dynamic workflow steps inherit the same sandbox, +permission checker, and parent confirmation boundary. Child-local `Ask` +decisions retain the worker's configured confirmation behavior, but a parent +`Ask` or tool-owned escalation remains governed by the parent provider. A +child-local allow rule or automatic confirmation provider cannot weaken the TUI +boundary. + +The CLI release, Homebrew formula, and standalone self-update preserve the same +verified support tree. npm is a development fallback, not the production +release supply path. + +This local process sandbox is the low-latency boundary for routine repository +commands. A3S Code owns permission and escalation policy; the sandbox provider +owns OS enforcement. A3S Runtime owns provider-neutral Task and Service +lifecycle and placement, not per-command approval. Dependency-heavy, untrusted, +OCI, or stronger-isolation workloads belong on A3S Box or a provider selected +through A3S Runtime. + +| Tool family | TUI behavior | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Workspace tools | `read`, `write`, `edit`, `patch`, `ls`, `glob`, `grep`, `bash`, `git`, `web_fetch`, and `web_search` run through workspace services, path boundaries, timeout handling, sandbox enforcement, and confirmation policy. | +| Structured output | `generate_object` uses `Generating/Generated object` cards while keeping schema-shaped JSON in the same bounded event stream as normal tools. | +| MCP tools | Configured MCP servers are registered as `mcp____` names and use the same approval and output rendering path. | +| PTC scripts | The `program` tool runs sandboxed JavaScript-compatible scripts with a host-provided `ctx` object. Recursive `program`, `dynamic_workflow`, and `parallel_task` calls are kept out of the default PTC allow-list. | +| Delegation | `task` launches one child agent. `parallel_task` launches multiple child agents on the native host runtime, preserves input order, emits subagent progress events, and respects `max_parallel_tasks`. | +| Dynamic workflow | `dynamic_workflow` is registered in the TUI because `ultracode` and `?` DeepResearch use it. It records project-local A3S Flow history under `.a3s/workflow` and can schedule host steps such as `parallel_task`. | +| Optional runtime | Host-provided runtime tools are registered only after `/login`. Once present, normal model turns and dynamic workflow PTC steps can call them for hosted batch execution. | +| Dynamic tools | Agent-directory and host-registered tools without a dedicated renderer use bounded `Calling/Called tool(args)` fallback cards. | + +## Effort Profiles + +`/effort` rebuilds the active session with a different depth profile. The +design scales work on three axes: + +- Thinking budget for providers that expose extended thinking. +- Tool-round budget and continuation count for all providers. +- Model-agnostic prompt guidance for rigor, verification, and decomposition. + +| Level | Thinking budget | Tool rounds | Continuations | Parallel tasks | Intended behavior | +| ----------- | --------------: | ----------: | ------------: | -------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `low` | 2,048 | 240 | 4 | 4 | Fast, minimal changes with narrow verification. | +| `medium` | 8,192 | 800 | 8 | 8 | Balanced default behavior without extra depth steering. | +| `high` | 16,384 | 1,200 | 12 | 12 | More deliberate planning, relevant tests, and self-review. | +| `xhigh` | 32,768 | 1,800 | 16 | 16 | Compare alternatives, probe edge cases, and verify thoroughly. | +| `max` | 65,536 | 2,400 | 24 | 24 | Maximum rigor for correctness, adversarial checks, and completeness. | +| `ultracode` | 65,536 | 3,200 | 32 | 32 | Message-gated dynamic workflow mode. Trivial turns stay direct; complex turns may use `dynamic_workflow`, A3S Flow replay, native `parallel_task`, and signed-in `runtime`. | + +All effort levels keep local `task` and `parallel_task` available, with the TUI +session limiting explicit sibling fan-out through `max_parallel_tasks`. +Runtime-driven automatic delegation is disabled for `low` through `max`; signed-in +Codex models still receive the corresponding native `reasoning.effort` value. +`ultracode` enables automatic delegation, planning, goal tracking, and +dynamic-workflow guidance, but the pre-analysis gate still decides whether a +turn actually needs planning or fan-out. Synthesis-only continuations cannot +start another delegation wave. + +## Dynamic Workflows Vs `/flow` + +A3S Code has two workflow concepts and they are intentionally different: + +| Concept | Surface | Purpose | +| ------------------------ | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `DynamicWorkflowRuntime` | Model-visible `dynamic_workflow` tool, used by `ultracode` and `?` DeepResearch | Per-turn dynamic orchestration. A sandboxed JavaScript PTC function returns A3S Flow commands such as `complete`, `fail`, `schedule_step`, or `schedule_steps`; A3S Flow records replayable workflow and step history. | +| Workflow assets | `/flow`, `/flow publish`, `/flow run`, `/flow deploy`, `/flow open`, `/flow logs`, `/flow status` | Durable workflow asset lifecycle. Local DAG JSON files can be reviewed locally and, when a host integration is configured, published with runtime-binding metadata. | + +Dynamic workflow scripts are runtime artifacts, not a separate TypeScript SDK. +They run inside the existing `program` QuickJS sandbox and may call only the +tools that the host allows through `ctx`. + +```javascript +export default async function run(ctx, inputs) { + if (inputs.kind === 'workflow') { + return { + type: 'schedule_steps', + steps: [ + { + step_id: 'inspect', + step_name: 'inspect_workspace', + input: { query: inputs.input.query }, + }, + { + step_id: 'fanout', + step_name: 'parallel_task', + input: { + tasks: [ + { + task_id: 'tests', + agent: 'explore', + description: 'Find test coverage', + prompt: 'Inspect relevant tests and coverage gaps.', + }, + { + task_id: 'risk', + agent: 'review', + description: 'Review risk', + prompt: 'Review the approach for regressions.', + }, + ], + }, + }, + ], + }; + } + + if (inputs.step_name === 'inspect_workspace') { + const hits = await ctx.grep(inputs.input.query, { glob: '*.rs' }); + return { hits }; + } + + return { ok: true }; +} +``` + +PTC is hosted by QuickJS, so `parallel_task` stays native to the host. When a +workflow needs parallel local subagents, it schedules a Flow step named +`parallel_task`; the TUI host executes the native implementation outside +QuickJS. If A3S OS registers a runtime tool after `/login`, dynamic +workflow PTC steps may also call `ctx.tool("runtime", ...)`. + +## Optional A3S OS Runtime And Views + +Add an A3S OS endpoint to `config.acl`, then sign in: + +```text +os = "https://os.example.com" +``` + +```text +/login +``` + +Signed-out behavior is intentionally useful but local: chat, file editing, +workspace tools, local MCP, local asset drafting, memory, `/ctx`, `/kb`, +`task`, `parallel_task`, `dynamic_workflow`, DeepResearch with validated local +reports, and local loops keep working. + +Signed-in behavior adds A3S OS asset publication, runtime tools, trusted view +links, and asset activity panels without changing the local-first TUI contract. + +| Capability | Signed out | Signed in after `/login` | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | +| Coding chat and workspace tools | Available with local permission checks and HITL approval. | Available with the same local safety path. | +| Context, memory, and local knowledge | `/ctx`, `/memory`, `/sleep`, and `/kb` use local stores. | Local stores remain available; hosted reports may also return trusted views. | +| Dynamic workflows | `DynamicWorkflowRuntime` can run local Flow-backed orchestration and host-side `parallel_task` fallback. | Normal workflow PTC steps may also call registered runtime tools; DeepResearch fan-out remains local for now. | +| Asset authoring | `/agent`, `/mcp`, `/skill`, `/flow `, and `/okf` can draft and review local assets. | Publish, deploy, run, open, logs, status, list, and activity commands can use A3S OS. | +| Runtime views | Validated local DeepResearch HTML is served through a loopback-only viewer and becomes an inline `Open view` action; OS `.view` and `viewUrl` responses are unavailable. | Local report views remain available, and OS `.view` and `viewUrl` responses also become inline `Open view` actions. | +| Runtime activity | The standalone `a3s top` command observes local processes. | Asset `activity` commands can inspect hosted jobs, runs, invocations, indexing, and workflow activity. | + +## Command Reference + +These commands are available outside the asset-specific flows: + +| Command | Capability | +| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/help` | Open the full command guide with slash commands, command forms, input modes, keys, panels, and resume help. | +| `/model` | Switch among configured ACL models and signed-in account-backed model tabs when available. | +| `/effort` | Change the active effort profile from `low` to `ultracode`. | +| `/init` | Analyze the workspace and generate an `AGENTS.md` instruction file. | +| `/config` | Edit the active ACL config in the built-in editor. | +| `/queue` | Inspect pending follow-ups, send the exact selected row now, remove one row, or explicitly confirm clearing all pending rows. | +| `/history` | Fuzzy-search prompts from the current session and restore a selected prompt without changing the current draft on cancel. | +| `/copy` / `/copy transcript` | Copy the latest assistant source Markdown or the complete semantic session with explicit native/OSC 52 delivery feedback. | +| `/export [path]` | Atomically create a private, no-clobber Markdown session snapshot inside the workspace. | +| `/tasks` | Inspect running and recent delegated tasks, search progress/output, open full details, refresh, or safely cancel a running task. | +| `/permissions` | Search exact session/project grants, inspect canonical arguments, and revoke with a second matching confirmation; project changes atomically update `.a3s/permissions.acl`. | +| `/use` / `/use status` / `/use repair` | Inspect live A3S Use capabilities or print explicit, non-mutating repair guidance. | +| `/login` / `/logout` | Sign in or out of the configured account integration; login can register host capabilities and runtime tools. | +| `Open view` | Reopen the latest validated local report or captured trusted runtime view when the inline action is shown. | +| `/ide` | Open the workspace file browser and editor. | +| `/memory` | Browse durable memory as an event/entity graph with tiers, aliases, relations, conflicts, and forget candidates. | +| `/ctx ` | Search past ctx-indexed sessions. | +| `/ctx ` | Attach a previous search result to the next message. | +| `/ctx save ` | Promote a previous session hit into durable memory. | +| `/sleep` | Consolidate the day's work into memory. | +| `/kb` | Manage the local personal knowledge base. | +| `/goal ` | Set a persistent goal for the current session or active asset mode. | +| `/goal resume` | Continue a durable goal that was left paused during session resume. | +| `/compact` | Summarize and shrink the active conversation context. | +| `/clear` | Start a fresh conversation in the current session surface. | +| `/fork` / `/fork session` | Branch the current transcript into a new session id. | +| `/fork worktree` | Create an isolated branch/worktree and copy the current workspace plus complete session into it. | +| `/rewind` | Fork the conversation before the last completed user turn and reverse its file patch only when conflict checks pass. | +| `/relay` | Open the session/background-work dashboard. Press `/` to filter the bounded per-source catalog by task, status, model, session id, or source path; `Space` toggles a task peek. Stable identities preserve a still-present selected session across manual `R` refreshes and automatic 15-second refreshes. | +| `/auto` | Switch future turns into non-interactive Auto mode; hard denials fail directly without opening HITL. | +| `/plugin` / `/reload` | Manage and hot-reload skills/plugins. | +| `/theme` | Cycle syntax highlighting themes. | +| `/update` | Upgrade the CLI and restart back into the saved session. | +| `/exit` | Quit `a3s code` after session persistence runs. | + +Asset command families include `/agent`, `/mcp`, `/skill`, `/flow`, `/okf`, +and `/loop`. Their publish, deploy, run, open, logs, status, list, activity, +clone, review, and local development forms are scoped to the selected asset +family. + +## Headless Smoke Mode + +Set `A3S_CODE_TUI_SMOKE=1` to exercise the same `AgentSession::stream()` +integration without taking over the terminal. This is useful for release smoke +checks of the configured model/session path. + +```bash +A3S_CODE_TUI_SMOKE=1 a3s code +``` + +## Related Pages + +1. [A3S CLI Code TUI](https://a3s-lab.github.io/a3s/docs/cli/code-tui) +2. [Commands](/guide/commands) +3. [Tools](/guide/tools) +4. [Memory](/guide/memory) +5. [Sessions](/guide/sessions) +6. [Security](/guide/security) diff --git a/website/docs/v6/en/guide/verification.mdx b/website/docs/v6/en/guide/verification.mdx new file mode 100644 index 00000000..40f57e10 --- /dev/null +++ b/website/docs/v6/en/guide/verification.mdx @@ -0,0 +1,175 @@ +--- +title: 'Verification' +description: "Prove a turn is done with verification commands and reports instead of trusting the model's claim" +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Verification + +The harness treats "done" as something that must be **proven**, not merely +claimed. When the model says a task is complete, that assertion is worth nothing +on its own. Verification turns the claim into evidence: you declare commands +that _must_ succeed, the runtime executes them, and the result carries a report +you can inspect, gate on, or surface to a user. + +Verification is session-scoped. The Rust core runs each command, records its +exit status and output, and rolls every report up into a single summary that +travels alongside the turn result. + +## Running Verification Commands + +A verification command is a small, named check: an `id`, a `kind`, a +human-readable `description`, and the `command` to run. Mark a check `required` +when a failure should be treated as a hard failure rather than a warning. + + + + +```ts +const report = await session.verifyCommands('release-readiness', [ + { + id: 'build', + kind: 'build', + description: 'Project compiles', + command: 'cargo build --all-features', + required: true, + timeoutMs: 120000, + }, + { + id: 'tests', + kind: 'test', + description: 'Unit tests pass', + command: 'cargo test', + required: true, + }, +]); + +console.log(report); +``` + + + + +```python +report = session.verify_commands('release-readiness', [ + { + "id": "build", + "kind": "build", + "description": "Project compiles", + "command": "cargo build --all-features", + "required": True, + "timeout_ms": 120000, + }, + { + "id": "tests", + "kind": "test", + "description": "Unit tests pass", + "command": "cargo test", + "required": True, + }, +]) + +print(report) +``` + + + + +The `subject` (here `release-readiness`) labels the batch so multiple +verification passes within one session stay distinct in the reports. + +## Reading The Post-Turn Summary + +Every turn's `send()` result also carries read-only verification fields, so you +can gate on the outcome without issuing a separate verification call. Use these +to decide whether the turn actually accomplished what it claimed. + + + + +```ts +const result = await session.send('Apply the fix and run the checks'); + +console.log(result.verificationStatus); +console.log(result.pendingVerificationCount); +console.log(result.failedVerificationCount); +console.log(result.verificationReportCount); +console.log(result.verificationSummaryText); + +if (result.failedVerificationCount > 0) { + throw new Error('Turn reported done but verification failed'); +} +``` + + + + +```python +result = session.send('Apply the fix and run the checks') + +print(result.verification_status) +print(result.pending_verification_count) +print(result.failed_verification_count) +print(result.verification_report_count) +print(result.verification_summary_text) + +if result.failed_verification_count > 0: + raise RuntimeError('Turn reported done but verification failed') +``` + + + + +## Inspecting Reports And Summaries + +Beyond the per-turn fields, the session exposes the full set of reports, a +structured summary, the available presets, and a human-readable digest. The +digest is the quickest way to show a person _why_ a turn passed or failed. + + + + +```ts +import { formatVerificationSummary } from '@a3s-lab/code'; + +const reports = session.verificationReports(); +const summary = session.verificationSummary(); +const presets = session.verificationPresets(); + +// Either the session helper or the standalone formatter yields readable text. +console.log(session.verificationSummaryText()); +console.log(formatVerificationSummary(summary)); +``` + + + + +```python +reports = session.verification_reports() +summary = session.verification_summary() +presets = session.verification_presets() + +# The session helper returns a ready-to-print human-readable digest. +print(session.verification_summary_text()) +``` + + + + +`verificationPresets()` returns workspace-aware check templates inferred from +files such as `Cargo.toml`, `package.json`, `pyproject.toml`, and `go.mod`. +Treat them as starting points: review the commands, timeouts, and required +flags for the project before gating releases or user-visible automation. + +## Why This Matters + +Without verification, an agent run ends on the model's word. With it, the run +ends on observable evidence: a build that compiled, a test suite that passed, a +linter that stayed quiet. The summary text gives you the audit trail; the +counts on the result let you fail closed in automation. + +## Related + +- [Telemetry](/guide/telemetry) — inspect trace events and verification reports as runtime evidence. +- [Limits](/guide/limits) — bound how much work a turn can do before verification runs. diff --git a/website/docs/v6/en/guide/workspace-backends.mdx b/website/docs/v6/en/guide/workspace-backends.mdx new file mode 100644 index 00000000..58090ffd --- /dev/null +++ b/website/docs/v6/en/guide/workspace-backends.mdx @@ -0,0 +1,233 @@ +--- +title: 'Workspace Backends' +description: 'Run A3S Code sessions against local files, S3-compatible object storage, and remote git services.' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Workspace Backends + +Workspace backends decide where built-in workspace tools read and write files. +The default is the local filesystem rooted at the session workspace. The SDKs +also expose typed backends for local files, S3-compatible object storage, and +an optional HTTP/JSON remote git provider. + +Use this surface when the host owns workspace placement: local development, +browser or container workspaces, object-storage workspaces, or managed sessions. + +## Capability Matrix + +| Backend | File tools | Search tools | Shell and local git | +| ---------------------------------- | -------------------------------------- | ------------------------------------------------------------- | ----------------------------------- | +| Default local workspace | `read`, `write`, `edit`, `patch`, `ls` | `grep`, `glob` | `bash`, `git` | +| `LocalWorkspaceBackend` | Same as default local workspace | Same as default local workspace | Same as default local workspace | +| `S3WorkspaceBackend` | `read`, `write`, `edit`, `patch`, `ls` | Optional degraded `grep` / `glob` when `searchEnabled` is set | Not registered | +| `S3WorkspaceBackend` + `remoteGit` | S3 file tools | Optional degraded S3 search | `git` through remote git, no `bash` | + +Object storage cannot service local processes. Do not promise shell execution on +an S3 workspace unless the host provides a separate sandbox through MCP or +A3S Box. + +## Local Backend + +The explicit local backend is useful when host code wants one option surface for +both local and remote sessions. + + + + +```ts +import { Agent, LocalWorkspaceBackend } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/repo', { + workspaceBackend: new LocalWorkspaceBackend('/repo'), +}); +``` + + + + +```python +from a3s_code import Agent, LocalWorkspaceBackend, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.workspace_backend = LocalWorkspaceBackend("/repo") +session = agent.session("/repo", opts) +``` + + + + +## S3 Backend + +`S3WorkspaceBackend` points the built-in file tools at any S3-compatible service, +including AWS S3, MinIO, RustFS, Cloudflare R2, and Backblaze B2. + + + + +```ts +import { Agent, S3WorkspaceBackend } from '@a3s-lab/code'; + +const backend = new S3WorkspaceBackend({ + endpoint: process.env.WORKSPACE_S3_ENDPOINT, + region: process.env.WORKSPACE_S3_REGION ?? 'us-east-1', + accessKeyId: process.env.S3_ACCESS_KEY_ID!, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, + bucket: process.env.WORKSPACE_S3_BUCKET!, + prefix: 'sessions/example', + forcePathStyle: true, + searchEnabled: false, +}); + +const agent = await Agent.create('agent.acl'); +const session = agent.session('s3://workspace-bucket/sessions/example', { + workspaceBackend: backend, +}); +``` + + + + +```python +import os + +from a3s_code import Agent, S3WorkspaceBackend, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.workspace_backend = S3WorkspaceBackend( + bucket=os.environ["WORKSPACE_S3_BUCKET"], + prefix="sessions/example", + access_key_id=os.environ["S3_ACCESS_KEY_ID"], + secret_access_key=os.environ["S3_SECRET_ACCESS_KEY"], + endpoint=os.environ.get("WORKSPACE_S3_ENDPOINT"), + region=os.environ.get("WORKSPACE_S3_REGION", "us-east-1"), + force_path_style=True, + search_enabled=False, +) +session = agent.session("s3://workspace-bucket/sessions/example", opts) +``` + + + + +S3 search is intentionally opt-in. When enabled, `grep` / `glob` degrade to +object listing and bounded downloads. Configure `maxObjectsScanned`, +`maxGrepBytesPerObject`, and `searchConcurrency` when the bucket can be large or +the endpoint rate-limits. + +### S3 Options + +| Node.js option | Python option | Required | Purpose | +| ----------------------- | --------------------------- | -------- | ------------------------------------------------------------------------------- | +| `bucket` | `bucket` | yes | S3 bucket that stores the workspace objects. | +| `prefix` | `prefix` | yes | Logical workspace root inside the bucket; use `""` for the bucket root. | +| `accessKeyId` | `access_key_id` | yes | Access key id, normally read from the host environment. | +| `secretAccessKey` | `secret_access_key` | yes | Secret access key, normally read from the host environment or a secret manager. | +| `endpoint` | `endpoint` | no | Custom S3-compatible endpoint; omit for AWS S3 defaults. | +| `region` | `region` | no | Region, defaulting to `us-east-1` when omitted. | +| `sessionToken` | `session_token` | no | STS session token when temporary credentials are used. | +| `forcePathStyle` | `force_path_style` | no | Set `true` for MinIO, RustFS, and most non-AWS endpoints. | +| `maxReadBytes` | `max_read_bytes` | no | Per-read size ceiling; defaults to 10 MiB. | +| `searchEnabled` | `search_enabled` | no | Enables degraded S3 `grep` / `glob`; defaults to false. | +| `maxObjectsScanned` | `max_objects_scanned` | no | Per-search object scan cap; used only when search is enabled. | +| `maxGrepBytesPerObject` | `max_grep_bytes_per_object` | no | Per-object download cap for `grep`; used only when search is enabled. | +| `searchConcurrency` | `search_concurrency` | no | Concurrent object downloads during `grep`; used only when search is enabled. | + +## Remote Git + +`remoteGit` attaches an HTTP/JSON git provider on top of `workspaceBackend`. It is +designed for non-local workspaces where the built-in `git` tool cannot use a +local `.git` directory. + +`remoteGit` requires `workspaceBackend`; passing it alone is rejected. + + + + +```ts +import { Agent, S3WorkspaceBackend } from '@a3s-lab/code'; + +const backend = new S3WorkspaceBackend({ + endpoint: process.env.WORKSPACE_S3_ENDPOINT, + region: process.env.WORKSPACE_S3_REGION ?? 'us-east-1', + accessKeyId: process.env.S3_ACCESS_KEY_ID!, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, + bucket: process.env.WORKSPACE_S3_BUCKET!, + prefix: 'sessions/example', + forcePathStyle: true, +}); + +const agent = await Agent.create('agent.acl'); +const session = agent.session('s3://workspace-bucket/sessions/example', { + workspaceBackend: backend, + remoteGit: { + baseUrl: process.env.REMOTE_GIT_BASE_URL!, + repoId: 'sessions/example', + bearerToken: process.env.REMOTE_GIT_TOKEN, + }, +}); +``` + + + + +```python +import os + +from a3s_code import ( + Agent, + RemoteGitBackendConfig, + S3WorkspaceBackend, + SessionOptions, +) + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.workspace_backend = S3WorkspaceBackend( + bucket=os.environ["WORKSPACE_S3_BUCKET"], + prefix="sessions/example", + access_key_id=os.environ["S3_ACCESS_KEY_ID"], + secret_access_key=os.environ["S3_SECRET_ACCESS_KEY"], + endpoint=os.environ.get("WORKSPACE_S3_ENDPOINT"), + region=os.environ.get("WORKSPACE_S3_REGION", "us-east-1"), + force_path_style=True, +) +opts.remote_git = RemoteGitBackendConfig( + base_url=os.environ["REMOTE_GIT_BASE_URL"], + repo_id="sessions/example", + bearer_token=os.environ["REMOTE_GIT_TOKEN"], +) +session = agent.session("s3://workspace-bucket/sessions/example", opts) +``` + + + + +Keep remote git credentials out of `agent.acl` and agent directories. Inject them +from the host environment or secret manager. + +### Remote Git Options + +| Node.js option | Python option | Required | Purpose | +| ------------------ | -------------------- | ---------- | -------------------------------------------------------------------------------------- | +| `baseUrl` | `base_url` | yes | Remote git service base URL, without a trailing slash. | +| `repoId` | `repo_id` | yes | Opaque repository id negotiated with the remote git service. | +| `bearerToken` | `bearer_token` | production | Bearer credential for the remote git service; omit only in trusted development setups. | +| `clientCertPem` | `client_cert_pem` | no | mTLS client certificate path; must be paired with the client key. | +| `clientKeyPem` | `client_key_pem` | no | mTLS client key path; must be paired with the certificate. | +| `requestTimeoutMs` | `request_timeout_ms` | no | Per-call HTTP timeout in milliseconds; defaults to 30000. | +| `maxDiffBytes` | `max_diff_bytes` | no | Client-side cap on `diff` response bytes; defaults to 1 MiB. | +| `maxLogEntries` | `max_log_entries` | no | Client-side cap on `log` entries; defaults to 200. | + +## Choosing A Backend + +- Use the default local workspace for normal developer machines and CI checkouts. +- Use `LocalWorkspaceBackend` when your host always passes a typed backend object. +- Use `S3WorkspaceBackend` when workspace state must live in object storage. +- Add `remoteGit` when a non-local workspace still needs the built-in `git` tool. +- Use MCP or A3S Box for shell-like execution that cannot run inside the + selected workspace backend. diff --git a/website/docs/v6/en/index.mdx b/website/docs/v6/en/index.mdx new file mode 100644 index 00000000..6020b30c --- /dev/null +++ b/website/docs/v6/en/index.mdx @@ -0,0 +1,7 @@ +--- +pageType: home +title: A3S Code +description: A governed coding-agent runtime with explicit tools, policy, events, and durable evidence. +sidebar: false +outline: false +--- diff --git a/website/docs/v6/zh/_meta.json b/website/docs/v6/zh/_meta.json new file mode 100644 index 00000000..aa467abc --- /dev/null +++ b/website/docs/v6/zh/_meta.json @@ -0,0 +1,12 @@ +[ + { + "type": "dir", + "name": "guide", + "label": "文档" + }, + { + "type": "dir", + "name": "api", + "label": "API" + } +] diff --git a/website/docs/v6/zh/_nav.json b/website/docs/v6/zh/_nav.json new file mode 100644 index 00000000..095c548a --- /dev/null +++ b/website/docs/v6/zh/_nav.json @@ -0,0 +1,42 @@ +[ + { + "text": "文档", + "link": "/guide/", + "activeMatch": "^/guide/" + }, + { + "text": "示例", + "link": "/guide/examples/", + "activeMatch": "^/guide/examples/" + }, + { + "text": "API", + "link": "/api/", + "activeMatch": "^/api/" + }, + { + "text": "资源", + "items": [ + { + "text": "GitHub", + "link": "https://github.com/A3S-Lab/Code" + }, + { + "text": "更新日志", + "link": "https://github.com/A3S-Lab/Code/blob/main/CHANGELOG.md" + }, + { + "text": "Rust API", + "link": "https://docs.rs/a3s-code-core" + }, + { + "text": "npm", + "link": "https://www.npmjs.com/package/@a3s-lab/code" + }, + { + "text": "PyPI", + "link": "https://pypi.org/project/a3s-code/" + } + ] + } +] diff --git a/website/docs/v6/zh/api/index.mdx b/website/docs/v6/zh/api/index.mdx new file mode 100644 index 00000000..be63d87d --- /dev/null +++ b/website/docs/v6/zh/api/index.mdx @@ -0,0 +1,42 @@ +--- +title: API 与包 +description: A3S Code 的 Rust、Node.js 与 Python API 入口,以及跨语言运行时契约。 +--- + +# API 与包 + +A3S Code 的四个产品表面共享同一个运行时内核,但各自承担不同的集成职责。 +版本号与发布状态请以对应 Registry 和 +[GitHub Releases](https://github.com/A3S-Lab/Code/releases) 为准。 + +| 表面 | 包或命令 | API 入口 | 适用场景 | +| -------- | --------------- | -------------------------------------------------- | ----------------------------- | +| Terminal | `a3s code` | [A3S CLI](https://github.com/A3S-Lab/CLI) | 直接运行交互式编码产品 | +| Rust | `a3s-code-core` | [docs.rs](https://docs.rs/a3s-code-core) | 完整 Runtime API 与扩展 Trait | +| Node.js | `@a3s-lab/code` | [npm](https://www.npmjs.com/package/@a3s-lab/code) | N-API 原生绑定与异步事件流 | +| Python | `a3s-code` | [PyPI](https://pypi.org/project/a3s-code/) | PyO3 原生包的同步和异步 API | + +## 安装 + +```bash +# Rust +cargo add a3s-code-core + +# Node.js +npm install @a3s-lab/code + +# Python +python -m pip install a3s-code +``` + +## 跨语言契约 + +跨语言产品边界由版本化事件、持久化 Snapshot 和明确的生命周期方法组成。 +先阅读 [API 契约](/guide/api-contract),再根据需要进入 +[会话](/guide/sessions)、[工具](/guide/tools)、 +[Workspace 后端](/guide/workspace-backends) 和 +[持久化](/guide/persistence)。 + +可替换的宿主扩展边界包括 `LlmClient`、`ContextProvider`、`MemoryStore`、 +`SessionStore`、Workspace 服务、工具、权限、确认、Hook、安全提供者、MCP +Transport 和 Graph Store。 diff --git a/website/docs/v6/zh/guide/_meta.json b/website/docs/v6/zh/guide/_meta.json new file mode 100644 index 00000000..ebb47e89 --- /dev/null +++ b/website/docs/v6/zh/guide/_meta.json @@ -0,0 +1,72 @@ +[ + "index", + "tui", + { + "type": "dir", + "name": "examples", + "label": "示例", + "collapsible": true, + "collapsed": true + }, + { + "type": "section-header", + "label": "文件系统优先" + }, + "filesystem-first", + "convention-over-configuration", + "agents-md", + "filesystem-instructions", + "filesystem-config", + "agent-dir", + "filesystem-agents", + "filesystem-skills", + "filesystem-tools", + "filesystem-schedules", + { + "type": "section-header", + "label": "运行时核心" + }, + "api-contract", + "sessions", + "commands", + "tools", + "verification", + "tasks", + "teams", + "orchestration", + "skills", + { + "type": "section-header", + "label": "治理工程" + }, + "security", + "hooks", + "limits", + "isolation", + { + "type": "section-header", + "label": "基础设施" + }, + "architecture", + "lane-queue", + "workspace-backends", + "multi-machine", + "cluster-extension-points", + { + "type": "section-header", + "label": "扩展" + }, + "providers", + "mcp", + "context", + "memory", + "persistence", + "telemetry", + { + "type": "dir", + "name": "rfcs", + "label": "RFC", + "collapsible": true, + "collapsed": true + } +] diff --git a/website/docs/v6/zh/guide/agent-dir.mdx b/website/docs/v6/zh/guide/agent-dir.mdx new file mode 100644 index 00000000..8e235249 --- /dev/null +++ b/website/docs/v6/zh/guide/agent-dir.mdx @@ -0,0 +1,106 @@ +--- +title: 'Agent 目录' +description: 'AgentDir 目录结构、加载映射、serve 守护进程和持久化边界。' +--- + +# Agent 目录 + +AgentDir 是按约定定义长期 Agent 的单一目录。一个文件夹承载主 Agent 的角色、运行配置、私有 skills、目录级 tools 和周期性 schedules。`AgentDir::load` 读取该目录并合成已有的 A3S Code 配置对象;它不引入新的 runtime,也不引入新的 prompt 系统。 + +这是一个有意为之的设计选择:`instructions.md` 作为 prompt **slot** 注入,而不是作为 system-prompt 覆盖。harness 始终保持 `BOUNDARIES`、response-format 契约、tool 可见性、安全门以及 verification 的权威性。一次调度运行始终是一个完整的 harness turn(`AgentSession::send`),绝不是裸的模型调用。 + +该特性位于 Rust 侧,由 `serve` 这个 Cargo feature 控制。从不启用 `serve` 的纯库嵌入方不会为它付出额外代价。 + +## 目录结构 + +```text +my-agent/ +├── instructions.md (required) Role and guidelines. Injected as a prompt slot. +├── agent.acl (optional) Model, providers, queue, and CodeConfig. +├── skills/ (optional) Private *.md skills. +├── schedules/ (optional) Cron jobs: frontmatter + body prompt. +└── tools/ (optional) Tool specs: kind: mcp or kind: script. +``` + +只有 `instructions.md` 是必需的。其余一切都是可选的,缺失的子目录只是不贡献对应能力。 + +`AgentDir::load` 将该目录映射到已有对象上: + +| Path | Becomes | Notes | +| ----------------- | ------------------------ | -------------------------------------------------------------------------------------------------- | +| `instructions.md` | `SystemPromptSlots.role` | 主 Agent 的角色 slot,详见 [instructions.md](/guide/filesystem-instructions)。 | +| `agent.acl` | `CodeConfig` | 模型、provider、队列和目录发现配置,详见 [agent.acl](/guide/filesystem-config)。 | +| `skills/` | `skill_dirs` | AgentDir 私有技能,详见 [skills/ 技能目录](/guide/filesystem-skills)。 | +| `schedules/*.md` | `Vec` | 每个文件一条周期性 turn,详见 [schedules/ 调度目录](/guide/filesystem-schedules)。 | +| `tools/*.md` | `Vec` | 每个文件一个 `kind: mcp` 或 `kind: script` 工具,详见 [tools/ 工具目录](/guide/filesystem-tools)。 | + +## Serve 守护进程 + +`serve_agent_dir` 将调度加载到各自独立的 session 中,并运行它们的 cron 循环,直到某个 cancellation token 触发。每次触发都会把该调度的 prompt 经由 `AgentSession::send` 路由。 + +```rust +use a3s_code_core::config::AgentDir; +use a3s_code_core::serve::serve_agent_dir; +use a3s_code_core::Agent; +use tokio_util::sync::CancellationToken; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let agent_dir = AgentDir::load("./my-agent")?; + let agent = Agent::from_config(agent_dir.config.clone()).await?; + let cancel = CancellationToken::new(); + + serve_agent_dir(&agent, &agent_dir, "./workspace", None, cancel).await?; + Ok(()) +} +``` + +在 `Cargo.toml` 中启用该 feature: + +```toml +[dependencies] +a3s-code-core = { version = "...", features = ["serve"] } +``` + +## 覆盖 Session 选项 + +`serve_agent_dir` 的第四个参数是一个可选的 `SessionOptions`,它会合并进每一个调度 session,用来固定 model、LLM client、session store 或 prompt slots。守护进程始终为每个 schedule 分配稳定的 `session_id`:`schedule:`;extra options 里的 `session_id` 会被有意忽略,避免多个 schedule 写入同一个 store id。若未提供 `prompt_slots`,则使用 AgentDir 的 `instructions.md` slot。 + +```rust +use a3s_code_core::SessionOptions; + +let extra = SessionOptions::new(); // set model, session_store, etc. +serve_agent_dir(&agent, &agent_dir, "./workspace", Some(extra), cancel).await?; +``` + +优雅取消会让进行中的 turn 完成其循环迭代后再停止;一旦每个 job 循环都已退出,守护进程返回 `Ok(())`。一个没有任何已启用调度的 AgentDir 会立即返回。 + +## 持久性 + +默认情况下,每次守护进程启动都会全新启动每个调度 session。在 `SessionOptions` 覆盖中传入一个 `SessionStore`,守护进程就会恢复那些 `schedule:` session 已存在于该 store 中的调度,还原其累积的对话历史。 + +```rust +use a3s_code_core::SessionOptions; + +let extra = SessionOptions::new().with_file_session_store("./sessions"); +serve_agent_dir(&agent, &agent_dir, "./workspace", Some(extra), cancel).await?; +``` + +恢复只还原历史。当前的 `instructions.md`、`skills/` 和 `tools/` 会在每次启动时重新应用,因此即便是被恢复的 session,编辑 AgentDir 也会在下一次重启时生效。 + +## 状态 + +| Area | State | +| ----------------------------------------- | -------------------------------------------------- | +| `instructions.md`、`agent.acl`、`skills/` | 已加载并使用。 | +| `schedules/` + serve 守护进程 | 已实现(`serve` feature)。 | +| 启动时重新水合 | 已实现,配合 `SessionStore` 可在重启后恢复上下文。 | +| `tools/`(`kind: mcp`) | 已实现,声明式 MCP server 注册进每个调度 session。 | +| `tools/`(`kind: script`) | 已实现,基于 `program` 路径的沙箱化 QuickJS tool。 | + +## 备注 + +- `instructions.md` 是一个角色 slot,因此 harness 的边界、响应契约以及 verification 保持权威。 +- AgentDir 是主 Agent 的目录;它不同于 `agent_dirs` / `registerAgentDir`,后者扫描 [agents/ 角色目录](/guide/filesystem-agents) 以获取 worker/subagent 定义。 +- secret 应放在环境变量或宿主密钥系统中,并从 `agent.acl` 引用,不要内联写进目录里。 +- `tools/` 由 serve 守护进程按调度 session 安装。普通交互式 session 应优先使用 direct tools、MCP 或 SDK 注册路径。 diff --git a/website/docs/v6/zh/guide/agents-md.mdx b/website/docs/v6/zh/guide/agents-md.mdx new file mode 100644 index 00000000..4584d2a3 --- /dev/null +++ b/website/docs/v6/zh/guide/agents-md.mdx @@ -0,0 +1,57 @@ +--- +title: 'AGENTS.md' +description: '作为一等上下文的项目指令' +--- + +# AGENTS.md + +`AGENTS.md` 是 workspace 级项目指令文件。它让项目规则随仓库一起版本化,避免每次 prompt 都重复说明构建命令、代码风格、安全边界和发布流程。 + +在文件系统优先架构里,`AGENTS.md` 负责“这个项目如何工作”;AgentDir 的 `instructions.md` 负责“这个长期 Agent 是谁”。两者都会进入上下文组合流程,但都不能覆盖 harness 的权限门、响应契约或验证要求。 + +```md +# Project Instructions + +- Core 变更运行 `cargo test -p a3s-code-core`。 +- 不要提交 `.a3s/config.acl` 里的真实密钥。 +- 搜索优先使用 `rg`。 +- 发布检查必须包含包元数据、CI 和 provider 验证。 +``` + +## 适合内容 + +- 构建、测试、lint、格式化和发布命令。 +- 目录职责、模块边界和代码风格。 +- 安全规则,例如 secrets、权限、外部副作用和数据处理要求。 +- 验证政策,例如哪些变更必须跑哪些检查。 +- 项目专用术语和常见工作流。 + +## 不适合内容 + +- 密钥、token、私有凭据或临时个人路径。 +- 某个 worker agent 的角色说明;放进 `.a3s/agents/`。 +- 某个长期 Agent 的身份和默认输出风格;放进 `instructions.md`。 +- 可复用 checklist;放进 `.a3s/skills/`。 + +## 嵌套规则 + +当前运行时只会自动读取 workspace 根目录的 `AGENTS.md`。嵌套 `AGENTS.md` +仍然可以作为仓库文档或宿主约定存在,但 A3S Code 不会自动沿目录树遍历它们, +除非宿主自己读取并注入这些内容。 + +只有当子目录规则确实不同,才添加嵌套 `AGENTS.md`。例如桌面端、API 端或某个 SDK +的构建工具链不同,可以在对应目录下放局部规则。不要为了重复根部说明而复制文件; +重复会让长期 Agent 难以判断哪份规则才是最新。 + +## 与其他约定的关系 + +```text +repo/ +├── AGENTS.md # 项目级长期指令 +├── agent.acl # 运行配置 +└── .a3s/ + ├── agents/ # worker/subagent 定义 + └── skills/ # 可复用技能 +``` + +`AGENTS.md` 应该告诉 Agent 项目事实和工作边界;`agent.acl` 告诉运行时如何连接模型和目录;`agents/` 与 `skills/` 提供可被发现的角色和流程。 diff --git a/website/docs/v6/zh/guide/api-contract.mdx b/website/docs/v6/zh/guide/api-contract.mdx new file mode 100644 index 00000000..ccef424e --- /dev/null +++ b/website/docs/v6/zh/guide/api-contract.mdx @@ -0,0 +1,717 @@ +--- +title: 'API 契约' +description: '本地集成检查覆盖的 SDK 机制' +--- + +# API 契约 + +本页只记录 `crates/code/scripts/docs_api_contract_smoke.mjs` 覆盖过的 +A3S Code Node SDK 行为。该脚本会启动一个临时的 +OpenAI-compatible 本地测试服务,创建真实 SDK session,调用 native binding, +并断言返回值。不需要 docs build。 + +在 `crates/code` 下运行: + +```bash +node scripts/docs_api_contract_smoke.mjs +``` + +## Agent + +已验证入口: + +```ts +const agent = await Agent.create(aclSource); +await agent.refreshMcpTools(); + +const session = agent.session(workspace, options); +const named = agent.sessionForAgent(workspace, 'explore', [], options); +``` + +`Agent.create()` 接受 ACL source text 或 `.acl` 文件路径。JSON config 不在 +已验证契约内。集成检查覆盖 `apiKey`/`baseUrl` 和 +`api_key`/`base_url` 两组 provider alias。`sessionForAgent()` 已用内置 +`explore` agent 验证。 + +Node factory 在 JavaScript surface 保持同步命名,但 native implementation 会把 +resource resolution 委托给 core 的异步 session construction path。Rust embedding +应使用 `SessionBuilder::build().await` 或异步 factory;同步 Rust compatibility +factory 要求显式传入预初始化 memory store。 + +集成检查也覆盖会创建文件型 session store 的 ACL 字段: + +```text +storage_backend = "file" +sessions_dir = "/tmp/a3s-doc-stores/acl-storage" +``` + +本契约不覆盖 `storage_url`。它不是本地文件型 session persistence 路径; +ACL 中使用 `sessions_dir`,或在 SDK options 中传 `sessionStore`。 + +## Session Options + +已验证的 option 形状: + +```ts +const session = agent.session(workspace, { + model: 'openai/docs-alt', + builtinSkills: true, + planningMode: 'disabled', + memoryStore: new FileMemoryStore(memoryDir), + sessionStore: new FileSessionStore(sessionDir), + sessionId: 'docs-contract', + autoSave: true, + securityProvider: new DefaultSecurityProvider(), + skillDirs: [path.join(workspace, 'skills')], + inlineSkills: [ + { + name: 'strict-release-review', + kind: 'instruction', + content: 'Always separate blockers from nice-to-have improvements.', + }, + ], + maxToolRounds: 24, + maxParseRetries: 3, + toolTimeoutMs: 120000, + circuitBreakerThreshold: 4, + autoCompact: true, + autoCompactThreshold: 0.75, + continuationEnabled: true, + maxContinuationTurns: 3, + maxExecutionTimeMs: 300000, // 5 分钟超时 + confirmationPolicy: { + enabled: true, + defaultTimeoutMs: 60000, + timeoutAction: 'reject', + }, +}); +``` + +`model` 是 per-session override。检查已覆盖使用 +`model: 'openai/docs-alt'` 创建的 session 会把 `docs-alt` 发送给本地 +provider。 + +基础 session accessor 已验证: + +```ts +console.log(session.sessionId); +console.log(session.workspace); +console.log(session.initWarning); +console.log(session.history()); +console.log(session.cancel()); +``` + +`workspace` 返回 SDK canonical workspace path。 + +`planningMode` 使用显式三态:`'auto'`、`'enabled'`、`'disabled'`。 + +检查也覆盖 session 创建时接受这个 `permissionPolicy` 形状: + +```ts +agent.session(workspace, { + permissionPolicy: { + deny: ['write(**/.env*)', 'bash(rm -rf*)'], + ask: ['bash(git push*)', 'bash(npm publish*)'], + allow: ['read(*)', 'grep(*)', 'glob(*)', 'bash(npm run build*)'], + defaultDecision: 'ask', + enabled: true, + }, +}); +``` + +Prompt slot options 是字符串: + +```ts +agent.session(workspace, { + role: 'release-readiness reviewer', + guidelines: + 'Find blockers before improvements. Require command evidence for done claims.', + responseStyle: 'concise, findings first', + goalTracking: true, +}); +``` + +## Result Shape + +`session.send()` 返回的 `AgentResult` 字段在 result 对象自身: + +```ts +const result = await session.send('Return a short answer'); + +console.log(result.text); +console.log(result.toolCallsCount); +console.log(result.promptTokens); +console.log(result.completionTokens); +console.log(result.totalTokens); +console.log(result.verificationStatus); +console.log(result.pendingVerificationCount); +console.log(result.failedVerificationCount); +console.log(result.verificationReportCount); +console.log(result.verificationSummaryJson); +console.log(result.verificationSummaryText); +``` + +trace events 和 verification reports 是 session API,不是 `AgentResult` 字段。 + +## Streaming + +`session.stream()` 返回 `EventStream`。已验证的读取方式是 `.next()`: + +```ts +const stream = await session.stream('Stream one sentence'); + +while (true) { + const { value: event, done } = await stream.next(); + if (done) break; + if (!event) continue; + if (event.text) process.stdout.write(event.text); +} +``` + +Smoke check 会在循环结束后立即发起下一次 `send()`。因此这里验证的不只是 event +delivery,还包括 stream single-flight admission lease 已释放;不需要人为增加重试 +延时。 + +每个 SDK 事件都是 envelope v1 投影:`version === 1`、开放的 `type` 字符串、 +完整的 `payload` 和可选 `metadata`。消费者必须为未来事件类型保留默认分支。 +Node 提供 `payloadJson` / `metadataJson` 字符串视图;Python 提供 +`payload_json` / `metadata_json`,并保留 `event_type` 作为 `type` 的别名。 + +不要依赖 `for await`,除非你安装的 SDK 版本已经单独验证支持 async +iteration。 + +## Direct Tools + +> 完整指南:[工具](/guide/tools)。 + +已验证的宿主侧直接工具调用: + +```ts +await session.readFile('README.md'); +await session.glob('src/*.rs'); +await session.grep('PermissionPolicy'); +await session.bash('printf docs-bash'); +await session.tool('read', { file_path: 'README.md' }); +await session.git('status'); +await session.git('diff'); +await session.git( + 'log', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + 5, +); +await session.tool('search_skills', { query: 'release blockers', limit: 5 }); + +session.toolNames(); +session.toolDefinitions(); +session.registerAgentDir(path.join(workspace, 'agents')); +``` + +已验证的 `toolNames()` 集合包含 `read`、`write`、`edit`、`patch`、`grep`、 +`glob`、`ls`、`bash`、`task`、`parallel_task`、`search_skills`、`Skill`、 +`program`、`git`、`batch`、`web_fetch` 和 `web_search`。 + +直接工具调用是宿主侧特权能力。把它暴露给最终用户之前,应在宿主应用内做权限判断。 + +## AGENTS.md + +脚本会在 workspace 写入一个 `AGENTS.md`,并断言其中的 instruction token +出现在本地 provider request body 中: + +```md +# Project Instructions + +Always mention docs-contract-agents-md-token when asked for project instructions. +``` + +项目指令应保持可操作,并且不要包含 secret。 + +## Programmatic Tool Calling + +`session.program()` 在内嵌 QuickJS runtime 中运行有边界的 JavaScript: + +```ts +const result = await session.program({ + source: ` + export default async function run(ctx, inputs) { + const text = await ctx.readFile('README.md'); + const hits = await ctx.grep(inputs.q, { glob: '*.md' }); + return { summary: 'ok', hasHits: text.includes(inputs.q) && hits.includes(inputs.q) }; + } + `, + inputs: { q: 'planningMode' }, + allowedTools: ['read', 'grep'], + limits: { timeoutMs: 30000, maxToolCalls: 12, maxOutputBytes: 65536 }, +}); + +const meta = JSON.parse(result.metadataJson); +console.log(meta.script_result); +console.log(meta.program.tool_calls); +``` + +已验证的 `ctx` helper:`readFile`、`read`、`grep`、`glob`、`ls`、`bash`、 +`git` 和通用 `tool(name, args)`。`allowedTools` 限制脚本能调用的已注册 +tool。`program` 不会出现在它自己的默认 tool set 里。 + +## Verification + +> 完整指南:[验证](/guide/verification)。 + +验证信息是 session 级能力: + +```ts +const report = await session.verifyCommands('docs api check', [ + { + id: 'echo', + kind: 'command', + description: 'echo works', + command: 'printf verify', + required: true, + }, +]); + +console.log(report.subject); +console.log(session.verificationReports()); +console.log(session.verificationSummary()); +console.log(session.verificationSummaryText()); +console.log(session.verificationPresets()); +console.log(formatVerificationSummary(session.verificationSummary())); +``` + +## Memory + +> 完整指南:[记忆](/guide/memory)。 + +Node memory 已用 `FileMemoryStore` 验证: + +```ts +const session = agent.session(workspace, { + memoryStore: new FileMemoryStore(memoryDir), +}); + +console.log(session.hasMemory); +await session.rememberSuccess('docs memory success', ['grep'], 'remembered'); +await session.rememberFailure('docs memory failure', 'expected failure', [ + 'bash', +]); +await session.memoryRecent(10); +await session.recallSimilar('docs memory', 5); +await session.recallByTags(['grep'], 10); +``` + +当前 Node SDK 已验证的 recent-memory 方法是 `memoryRecent()`。`recallRecent()` +不存在于当前 Node SDK surface。 + +## Skills + +> 完整指南:[Skills](/guide/skills)。 + +文件型和 inline skills 已通过 `search_skills` 验证: + +```ts +const session = agent.session(workspace, { + skillDirs: [path.join(workspace, 'skills')], + inlineSkills: [ + { + name: 'strict-release-review', + kind: 'instruction', + content: 'Always separate blockers from nice-to-have improvements.', + }, + ], +}); + +await session.tool('search_skills', { query: 'release blockers', limit: 5 }); +await session.tool('search_skills', { + query: 'strict release review', + limit: 5, +}); +``` + +skill-file 检查使用带 YAML frontmatter 的 Markdown,并覆盖了 +`allowed-tools` key。 + +## Side Questions + +> 完整指南:[会话](/guide/sessions)。 + +当前 SDK surface 没有专用的临时提问 helper。需要不改变 session transcript +时,使用显式 history: + +```ts +const snapshot = session.history(); +const side = await session.send('What is this test?', snapshot); + +console.log(side.text); +console.log(session.history().length === snapshot.length); +``` + +## Runs And Cancellation + +> 完整指南:[会话](/guide/sessions)。 + +每次 `send()` 或 `stream()` 都会记录可回放的 run state: + +```ts +const runs = await session.runs(); +const latest = runs.at(-1); + +if (latest) { + console.log(await session.runSnapshot(latest.id)); + console.log(await session.runEvents(latest.id)); +} + +const current = await session.currentRun(); +if (current?.id && current.status === 'running') { + await session.cancelRun(current.id); +} + +console.log(session.traceEvents()); +``` + +`currentRun()` 用于读取当前操作。空闲时,它可能返回 `null`,也可能 +因前序控制流保留一个 snapshot。已完成历史请使用 `runs()`。 + +同一个 session 的 transcript-affecting operation 使用 single-flight。重叠的 send、 +stream、attachment call、slash command 或 `resumeRun` 会立即返回 `SessionBusy`, +不会排队。即使公开 handle 被丢弃,stream 也会持有 admission,直到 producer 停止。 + +## Persistence + +> 完整指南:[持久化](/guide/persistence)与[会话](/guide/sessions)。 + +文件型 session persistence 已验证稳定 `sessionId`、`autoSave`、显式 +`save()` 和 `resumeSession()`: + +```ts +const session = agent.session(workspace, { + sessionStore: new FileSessionStore(sessionDir), + sessionId: 'docs-contract', + autoSave: true, +}); + +await session.save(); + +const resumed = agent.resumeSession('docs-contract', { + sessionStore: new FileSessionStore(sessionDir), +}); +console.log(resumed.history()); +``` + +Core persistence 会把 conversation、artifact、trace、run record、verification +report 与 subagent task snapshot 一起提交为一个有版本的 `SessionSnapshotV1`。 +File/memory store 原子发布这个 aggregate。Legacy fragmented record 仍可加载; +自定义 store 必须显式实现 aggregate save。 + +Node 进程需要及时释放 session 级后台资源时,调用 `session.close()`。`close()` 是完整的优雅停止入口:把 `session.isClosed` 翻成 `true`(之后 `send` / `stream` 会以 `Session closed` 错误立即返回),fire session 级 `CancellationToken` 让所有 in-flight run、委派子代理任务和 HITL 待确认全部中止。重复调用 `close()` 是 no-op。 + +控制面只持有 session ID 时,可以从 Agent 侧触发同样的清理: + +```ts +await agent.listSessions(); // ['session-a', 'session-b'] +await agent.closeSession('session-a'); // 若原本是 open,返回 true +await agent.close(); // 关闭所有活 session + 断开全局 MCP +``` + +`agent.close()` 之后,再调 `agent.session(...)` / `agent.resumeSession(...)` 会立即抛 `Session closed`。幂等。建议在进程退出 handler 中调用,保证没有 session 级 worker 比 agent 活得更久。 + +## Delegation + +> 完整指南:[任务](/guide/tasks)与[编排](/guide/orchestration)。 + +已验证核心委派工具的直接 helper: + +```ts +await session.task({ + agent: 'general', + description: 'docs delegated check', + prompt: 'Return a short response.', + maxSteps: 1, +}); + +await session.tasks([ + { + agent: 'general', + description: 'one', + prompt: 'Return one response.', + maxSteps: 1, + }, + { + agent: 'general', + description: 'two', + prompt: 'Return another response.', + maxSteps: 1, + }, +]); +``` + +它们返回来自 `task` 和 `parallel_task` 的 `ToolResult`。 + +## Hooks + +> 完整指南:[Hooks](/guide/hooks)。 + +已验证的 hook 管理面: + +```ts +session.registerHook( + 'docs-observer', + 'pre_tool_use', + { tool: 'bash' }, + { priority: 1, timeoutMs: 1000 }, + () => ({ action: 'continue' }), +); + +console.log(session.hookCount()); +session.unregisterHook('docs-observer'); +``` + +把 hook 行为作为生产关卡前,需要对你依赖的具体 event path 做集成测试。 + +## Slash Commands + +> 完整指南:[命令](/guide/commands)。 + +自定义 slash command 通过 `session.send()` 触发: + +```ts +session.registerCommand( + 'docs_status', + 'Return docs command status', + (args, ctx) => { + return `status args=${args}; session=${ctx.sessionId}; workspace=${ctx.workspace}`; + }, +); + +console.log(session.listCommands()); +const result = await session.send('/docs_status check'); +console.log(result.text); +``` + +## Lane Queue + +> 完整指南:[Lane 队列](/guide/lane-queue)。 + +Queue infrastructure 是显式 opt-in: + +```ts +const queued = agent.session(workspace, { + queueConfig: { enableDlq: true, enableMetrics: true }, +}); + +console.log(queued.hasQueue()); +await queued.setLaneHandler('execute', { mode: 'external', timeoutMs: 1000 }); +await queued.pendingExternalTasks(); +await queued.completeExternalTask('missing', { + success: true, + result: { ok: true }, +}); +await queued.queueStats(); +await queued.queueMetrics(); +await queued.deadLetters(); +``` + +没有传入 `queueConfig` 的普通 session 不会启用 queue。 + +## MCP + +> 完整指南:[MCP](/guide/mcp)。闲置断开见[集群扩展点](/guide/cluster-extension-points)。 + +集成检查覆盖一个真实 stdio MCP server: + +```ts +const count = await session.addMcp({ + name: 'echo', + transport: { + type: 'stdio', + command: process.execPath, + args: ['tools/mcp_echo_server.mjs', 'example-value'], + }, + timeoutMs: 30000, +}); + +console.log(count); +console.log(await session.mcpStatus()); +console.log( + session.toolNames().filter((name) => name.startsWith('mcp__echo__')), +); + +await session.tool('mcp__echo__echo', { message: 'docs mcp ok' }); +await session.removeMcpServer('echo'); +``` + +server 注册出的 tool 名称格式是 `mcp____`。 +`addMcpServer(...)` 和 `addMcpServerConfig(...)` 仍是兼容别名;新示例使用更紧凑的 object-shaped `addMcp(...)` API。 + +Live add/remove 只作用于当前 session 私有 manager。Agent-global 和 host-supplied +manager 是继承的只读 capability source,因此一个 session 不能修改 sibling 或 +global MCP 配置。 + +## 集群级扩展点 + +> 完整指南:[集群扩展点](/guide/cluster-extension-points)(身份标签、预算守卫、集群事件、确定性 ID/回放、loop checkpoint、保留上限)。 + +这些契约让集群控制面在**不 fork 框架**的前提下接入多租户、成本管控和容错运行。框架定义"决策点"和"结构化事件",**策略实现由 host 提供**。 + +### 身份标签 + +`SessionOptions` 上四个可选 slot,会透传到 hooks / traces / `SessionData`,框架本身不解释: + +```ts +const session = agent.session(workspace, { + tenantId: 'tenant-example', + principal: 'principal-example', + agentTemplateId: 'agent-template-example', + correlationId: 'trace-example', + sessionStore: new FileSessionStore('./sessions'), +}); +session.tenantId; // -> 'tenant-example' +session.correlationId; // -> 'trace-example' +``` + +resume 时 `apply_persisted_runtime_options` 会从持久化快照里还原标签;但**调用方在 resume_session 时传的 opts 优先**,可以借此 relabel。 + +### 预算 / 成本守卫 + +`BudgetGuard` 会在每个属于 run 的 provider call 前检查,并在成功响应后记录 usage; +每个 governed tool call(包括 nested 与 trusted host-direct call)也会在执行前检查。 +`Deny` 返回 `CodeError::BudgetExhausted { resource, reason }`;`SoftLimit` 发射 `AgentEvent::BudgetThresholdHit { kind: "soft", .. }` 后继续执行。 + +目前仅 Rust 层接入(Node/Python wrapper 后续补): + +```rust +let guard: Arc = /* host-supplied impl */; +let opts = SessionOptions::new().with_budget_guard(guard); +``` + +### 集群事件词汇 + +`AgentEvent`(`#[non_exhaustive]`)新增三类平台级事件,host 通过 `HookExecutor` 注入: + +- `BudgetThresholdHit { resource, kind, consumed, limit, message? }` +- `PassivationRequested { reason, deadline_ms? }` +- `PeerInvocation { from_session_id, from_tenant_id?, correlation_id? }` + +session 内部 hook 可统一订阅,不必关心 host 用什么传输发过来。 + +### 确定性 ID / 时钟 + +`HostEnv { id_generator, clock }` 替换默认的 `uuid::Uuid::new_v4()` + 墙上时钟。Replay 工具传入 `SequentialIdGenerator` + `FixedClock` 即可在另一台机器上 bit-identical 重放一个 run。 + +### Loop checkpoint + run 恢复 + +配置了 `SessionStore` 后,agent loop **每次 tool round 结束**会持久化一个 `LoopCheckpoint`(按 `run_id` 索引)。任何拥有同一个 store 的节点都能从最近的边界 rehydrate: + +```ts +// Node — host 探测到 A 节点死掉;在 B 节点上: +const session = agentB.session(workspace, { + sessionStore: new FileSessionStore('./sessions'), + sessionId: 'session-from-node-a', +}); +const result = await session.resumeRun('run-id-from-node-a'); +``` + +```python +# Python 等价 +opts = SessionOptions() +opts.session_store = FileSessionStore('./sessions') +opts.session_id = 'session-from-node-a' +session = agent_b.session(workspace, opts) +result = session.resume_run('run-id-from-node-a') +``` + +resume 出来的会**分配一个全新的 run id** — 框架不假装旧 run 还在继续,新旧 run 的关系是 host 的元数据。两个可区分的错误路径方便 host 端调度分支: + +- `"resume_run requires a session_store"` — host 应该回退到新建 session。 +- `"no loop checkpoint found for run 'X'"` — host 可以稍等重试(checkpoint 写入竞态),或当 run 已丢失。 + +**边界策略**:checkpoint 只在 tool round **之间**取,不在工具执行中途取。进程在工具执行中途死掉时,这一轮的工作会丢失,LLM 从前一个边界重新思考。这是用"重试成本"换"正确性" — 把非幂等工具(write、bash)在边界两侧重跑比让 LLM 重想要糟得多。 + +### 长跑 session 的保留上限 + +`SessionRetentionLimits` 让 host 给四种 in-memory 存储设上限:run 记录、每 run 的事件、trace 事件、**终态的** subagent 任务快照。每个字段都是可选的(`None` 保持原本无上限的默认 — 短 session 没问题,小时/天级的就漏内存)。FIFO 严格按插入序丢;**Running 状态的** subagent 任务永不被丢。 + +```rust +use a3s_code_core::retention::SessionRetentionLimits; + +let limits = SessionRetentionLimits::new() + .with_max_runs(100) + .with_max_events_per_run(5_000) + .with_max_trace_events(10_000) + .with_max_terminal_subagent_tasks(1_000); + +let opts = SessionOptions::new().with_retention_limits(limits); +``` + +上限建议跟 host 自己 Prometheus / 观测系统的内存预算保持一致。Node 暴露为 `retentionLimits`;Python 暴露为 `opts.retention_limits`。 + +### MCP 闲置断开 + +`Agent::disconnect_idle_mcp(threshold_ms)` 扫描所有已连接的 MCP server,把"最后活跃时间"早于 `now - threshold_ms` 的全部断开。注册的配置**保留** — 后续 tool 调用会按需重连。返回被断开的 server 名称列表。 + +```ts +// Node — 周期回收闲置 MCP 子进程 +setInterval(async () => { + const dropped = await agent.disconnectIdleMcp(5 * 60 * 1000); // 5min + if (dropped.length) { + console.log('reaped idle MCP servers:', dropped); + } +}, 60_000); +``` + +```python +# Python — 等价 +dropped = agent.disconnect_idle_mcp(5 * 60 * 1000) +``` + +每次 `connect` 和成功的 `call_tool` 都会刷新活跃时间。Host 走旁路通道路由 tool 时,可以手动 `McpManager.touch(name)` 把 server 保温。 + +### BudgetGuard 的 SDK 桥接 + +两个 SDK 共用同一个决策返回形状: + +| 返回值 | 效果 | +| -------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `None` / `null` / `{decision:'allow'}` | 静默放行 | +| `{decision:'soft', resource, consumed, limit, message?}` | 发射 `BudgetThresholdHit('soft')` 事件,继续执行 | +| `{decision:'deny', resource, reason}` | 中止调用,Python 抛 `RuntimeError("Budget exhausted...")`/Node reject 同样的错误 | + +guard 对象上缺失的方法 = 宽松默认(Allow / no-op);callback 抛错 → fallback 到 Allow,异常的 guard 不会拖垮 live session。 + +```python +# Python — 通过 SessionOptions 在 session 构造前挂上 +class MyGuard: + def check_before_llm(self, session_id, estimated_tokens): + return {"decision": "deny", "resource": "llm_tokens", "reason": "cap"} + def record_after_llm(self, session_id, usage): + track(session_id, usage["total_tokens"]) + +opts = SessionOptions() +opts.budget_guard = MyGuard() +session = agent.session(workspace, opts) +``` + +```ts +// Node — session 构造后通过 setBudgetGuard 挂上。 +// JsFunction 不能塞进值类型的 SessionOptions,所以 guard 在 Session 上注册, +// 下一次 send/stream 生效。 +session.setBudgetGuard({ + checkBeforeLlm: (ctx) => { + if (overBudget(ctx.sessionId)) { + return { decision: 'deny', resource: 'llm_tokens', reason: 'cap' }; + } + return null; + }, + recordAfterLlm: (ctx) => { + track(ctx.sessionId, ctx.usage.totalTokens); + }, +}); +``` + +Node 回调接收单个 `ctx` 对象,且**绝不能 throw**;请用 `try/catch` 包裹并返回显式决策。 +卡住或无法解析的 `check*` 回调会 fail-closed 为 `deny`。Python 回调使用位置参数,异常会被捕获并视为 Allow。 + +Node 用 `setBudgetGuard(null)` 清除;Python 把 `opts.budget_guard` 设回 `None` 后重建 session。 diff --git a/website/docs/v6/zh/guide/architecture.mdx b/website/docs/v6/zh/guide/architecture.mdx new file mode 100644 index 00000000..2efed953 --- /dev/null +++ b/website/docs/v6/zh/guide/architecture.mdx @@ -0,0 +1,156 @@ +--- +title: '架构' +description: 'Session 构建、run 作用域、受控调用、事件协议与持久化' +--- + +# 架构 + +A3S Code 把配置解析、逐 run 执行、稳定 wire contract 与持久化分成清晰边界。 +TUI 和 SDK 使用同一个运行时内核,不存在另一套执行路径。 + +```text +CodeConfig + SessionOptions + -> 校验并异步解析资源 + -> ResolvedSessionConfig + -> AgentSession + -> single-flight run admission + -> InvocationContext + ├─ LLM invoker -> provider calls + ├─ tool invoker -> model、nested、delegated 与 host-direct calls + └─ events -> EventEnvelopeV1 -> Rust / Node / Python consumers + -> SessionSnapshotV1 -> atomic store generation +``` + +## 异步优先的 Session 构建 + +`SessionOptions` 是公开的配置 patch,不是半初始化的运行时状态。异步构建路径会把 +它与 `CodeConfig` 合并,校验冲突选项,初始化异步资源,再生成唯一的内部 +`ResolvedSessionConfig`。后续 session assembly 只消费这个已解析值,不会让不同 +层各自重复决定同一个配置。 + +Rust 宿主应优先使用: + +```rust +let session = agent + .session_builder("/repo") + .options(options) + .build() + .await?; +``` + +`Agent::session_async`、`resume_session_async`、`session_for_agent_async` 与 +`session_for_worker_async` 使用同一个构建内核。文件型 memory/session store、 +queue、trajectory recorder 与 session MCP discovery 都在异步阶段初始化;失败时 +返回带具体 resource 的 `SessionConfiguration` 或 `SessionInitialization` 类型错误。 + +同步 `Agent::session` 只为已经显式传入预初始化 memory store、并初始化好其他全部 +资源的宿主保留兼容。它不会启动或阻塞 Tokio runtime。任何需要异步工作的配置都会返回 +`CodeError::AsyncSessionBuildRequired`;运行时不会悄悄换成更容易构建的 backend。 +通过 `SessionOptions::with_mcp` 传入的 manager 始终需要异步 capability discovery; +同步路径只能继承 agent 启动时已经缓存的 global MCP tools。 + +## 对话状态 Single-Flight + +对话历史通过 admission 串行化,而不是依赖乐观锁。同一个 session 同时只能有一个 +会影响 transcript 的操作,包括 `send`、`stream`、两种 attachment variant、 +slash command 与 `resume_run`。重叠调用会在读取历史或派发命令前立即返回 +`CodeError::SessionBusy`。 + +流式调用会一直持有 admission lease,直到 stream runtime 真正结束。丢弃或中止 +公开 handle 不会在原 producer 仍写入 event/history 时短暂放行第二个操作。直接 +宿主工具调用属于控制面操作,不占用 conversation lease。 + +## Invocation Context + +每个已准入 run 都创建一个不可变的 `InvocationContext`,其中包含: + +- run ID 与 session ID +- run cancellation token +- event sender +- governance snapshot,包括当前 budget guard + +它是 provider 与 tool 工作的单一事实来源,也会把同一个 cancellation token 和 +session identity 安装进 `ToolContext`。因此取消可以传递到 queued tools、嵌套 +`batch`/`program`、委派任务、planning、structured-output repair、compaction 和 +其他属于该 run 的 helper call。 + +## LLM 调用边界 + +属于 run 的 provider 工作统一经过 scoped LLM invoker。它在每次 provider call +前检查预算与取消,在成功响应后记录 usage;streaming path 会代理 terminal usage, +并合并 caller cancellation 与 run cancellation。普通 turn、planning、structured +output/repair、compaction 以及 memory/helper path 都使用这个边界,不再各自维护 +预算逻辑。 + +硬预算拒绝会作为错误返回,绝不会转换成不受治理的 fallback。soft limit 会发出 +`budget_threshold_hit` 事件,然后继续当前调用。 + +## Tool 调用边界 + +Tool invoker 是 model-selected、nested、programmatic、delegated 与 direct host +call 的统一治理内核。对模型发起的工作,它会执行 active-skill restriction、 +permission policy、pre/post hook、budget check、human confirmation、queue/timeout、 +cancellation、递归调用保护与 output sanitization。`batch` 和 `program` 接收的是 +scoped invoker,而不是 raw registry,因此内部调用不能绕过这些检查。 + +直接 SDK helper 使用显式的 `HostDirectPolicy::TrustedControlPlane` origin。宿主已经是 +选择该操作的权威,因此跳过面向模型的 permission/HITL 决策;但 pre-hook 仍可 +阻止调用,budget、queue/timeout、cancellation、递归保护、post-hook 与 output +sanitization 仍然生效。应用在把这条特权路径暴露给终端用户前,必须自行完成授权。 + +## 稳定事件协议 + +`AgentEvent` 是 Rust 内部运行时 enum。跨语言 contract 是无损 envelope: + +```json +{ + "version": 1, + "type": "tool_end", + "payload": {}, + "metadata": {} +} +``` + +v1 事件目录与 Rust exhaustive mapping 共享一个事实源,因此新增 runtime variant +却没有 canonical wire name 会直接导致编译失败。Node 与 Python 使用统一投影生成 +`text`、`toolName`、`tool_name` 等 convenience field。`type` 保持开放字符串: +未来未知类型仍完整保留 payload 与 metadata,不会被压成 `unknown` sentinel。 + +## 原子 Session 持久化 + +`SessionSnapshotV1` 表示一个有版本的完整持久化 generation,包含 conversation、 +artifact、trace event、run record、verification report 与 delegated-task snapshot。 +`session.save()` 会物化这个 aggregate,并且只调用一次 +`SessionStore::save_snapshot`。 + +File store 先写入并同步临时文件,再用 atomic replacement 发布一个完整 JSON +envelope;memory store 在同一把锁下替换整个 aggregate entry。两者都会声明 atomic +snapshot capability。历史 bare `SessionData` 与 fragment directory 仍可读取以便 +迁移,但新保存不会发布碎片化 generation。自定义 store 必须显式实现 aggregate +save;默认方法返回错误,不会把 partial/no-op write 当成成功。 + +## MCP 所有权与隔离 + +MCP manager 的所有权是显式的: + +1. Agent-global manager 持有全局配置加载的 server。 +2. Session option 中由宿主传入的 manager 是继承的只读 capability source。 +3. 每个 session 都新建一个私有 live manager。 + +Capability 按以上顺序组装,因此 session-local tool 只能在当前 session 内 shadow +继承工具。实时 `add_mcp_server` / `remove_mcp_server` 只改变私有 manager;移除本地 +shadow 后会重新显露继承能力。Sibling session 不能互相修改;delegated child 会 +继承调用同一批工具所需的有序 manager source,但不会取得它们的所有权。 + +## Programmatic Tool Calling + +`program` 工具在内嵌 QuickJS VM 中运行 JavaScript,只暴露受控 `ctx` 对象。VM +没有直接文件系统、网络、子进程或环境变量访问权;有用能力都通过 scoped tool +invoker 回到 A3S Code。下一步需要判断时使用普通模型工具调用;动作序列已经确定、 +只需让模型理解结果时,使用有边界的 program。 + +## 扩展点 + +通过 typed session options、skills、agent definitions、hooks、MCP servers、 +memory/session stores、security providers、queue configuration 与 workspace services +扩展运行时。优先使用显式 policy 和可回放证据,不要引入平行执行路径。 diff --git a/website/docs/v6/zh/guide/cluster-extension-points.mdx b/website/docs/v6/zh/guide/cluster-extension-points.mdx new file mode 100644 index 00000000..75064bb1 --- /dev/null +++ b/website/docs/v6/zh/guide/cluster-extension-points.mdx @@ -0,0 +1,192 @@ +--- +title: '集群扩展点' +description: '集群宿主用来在多节点上运行长时会话、且无需分叉框架的接缝。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 集群扩展点 + +集群宿主平台在多个节点上运行长时运行的智能体会话。框架本身并不附带调度器或放置引擎,而是暴露一小组接缝:它定义决策点、发出结构化事件,并把策略交给宿主来提供。下文的所有内容都是你从框架外部接入的——你永远不需要分叉框架。 + +本页会明确区分哪些接缝在两个 SDK 中都可用(以 Node.js + Python 代码展示),哪些当前只在 Rust 核心中配置(没有 SDK 选项时以文字说明)。 + +## 身份标签 + +每个会话都可以携带四个不透明的身份标签。框架从不解释它们——它会把它们传播到钩子、追踪和 `SessionData`,并在恢复时还原它们。宿主正是借此把一个会话归属到租户、主体、智能体模板以及更广的关联链。 + +请将身份标签与 `sessionStore` / `session_store` 搭配使用,使标签在进程重启后依然保留。恢复时,**由调用方提供的选项优先生效**,因此你可以在节点之间迁移会话时为其重新打标签。 + + + + +```ts +const session = agent.session('/path/to/project', { + tenantId: 'tenant-example', + principal: 'principal-example', + agentTemplateId: 'agent-template-example', + correlationId: 'trace-example', +}); + +// Getters return string | null +console.log(session.tenantId); // 'tenant-example' +console.log(session.principal); // 'principal-example' +console.log(session.agentTemplateId); // 'agent-template-example' +console.log(session.correlationId); // 'trace-example' +``` + + + + +```python +opts = SessionOptions() +opts.tenant_id = 'tenant-example' +opts.principal = 'principal-example' +opts.agent_template_id = 'agent-template-example' +opts.correlation_id = 'trace-example' +session = agent.session('/path/to/project', opts) + +# Getters are properties, return str | None +print(session.tenant_id) # 'tenant-example' +print(session.principal) # 'principal-example' +print(session.agent_template_id) # 'agent-template-example' +print(session.correlation_id) # 'trace-example' +``` + + + + +## 预算 / 成本守卫 + +预算守卫让宿主针对成本或令牌预算对每一次 LLM 调用进行把关。框架会在每次 LLM 请求*之前*调用你的守卫,并在请求返回*之后*再次调用。守卫是你自己拥有的策略;框架只负责执行你返回的决策。 + + + + +```ts +session.setBudgetGuard({ + checkBeforeLlm(ctx) { + if (overLimit(ctx.sessionId, ctx.estimatedTokens)) { + return { + decision: 'deny', + resource: 'tokens', + reason: 'monthly cap reached', + }; + } + return { decision: 'allow' }; + }, + recordAfterLlm(ctx) { + meter(ctx.sessionId, ctx.usage); + }, +}); + +// Clear the guard +session.setBudgetGuard(null); +``` + +Node 回调接收单个 `ctx` 对象,且**绝不能抛出异常**。请用 `try/catch` 包裹逻辑并返回显式决策。卡住或无法解析的 `check*` 回调会 fail-closed 为 `deny`。 + + + + +```python +class MyGuard: + def check_before_llm(self, session_id, estimated_tokens): + if over_limit(session_id, estimated_tokens): + return {'decision': 'deny', 'resource': 'tokens', 'reason': 'monthly cap reached'} + return {'decision': 'allow'} + + def record_after_llm(self, session_id, usage): + meter(session_id, usage) + +opts = SessionOptions() +opts.budget_guard = MyGuard() +session = agent.session('/path/to/project', opts) + +# To clear: set opts.budget_guard = None and re-create the session. +``` + + + + +两个 SDK 的决策结构完全一致: + +| 返回值 | 效果 | +| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `None` / `null` / `{ decision: 'allow' }` | 继续执行 LLM 调用。 | +| `{ decision: 'soft', resource, consumed, limit, message? }` | 发出 `BudgetThresholdHit`(kind 为 `soft`)并继续执行。 | +| `{ decision: 'deny', resource, reason }` | 中止 LLM 调用。Python 抛出 `RuntimeError("Budget exhausted...")`;Node 以 `"Budget exhausted..."` 拒绝(reject)。 | + +这种健壮性是刻意为之,但两个 SDK 的失败语义不同:**缺失的守卫方法**会被当作宽松默认值处理。Python 回调出错会回退为 Allow。Node 回调不能 throw;超时或无法解析的 `check*` 返回会 fail-closed 为 `deny`。 + +## 集群事件词汇 + +宿主通过其钩子执行器,将集群级别的决策作为结构化的 `AgentEvent` 变体发出。会话内的钩子以统一方式订阅它们——与它们观察其他任何事件的方式相同——因此在宿主处编写的策略会原样呈现给智能体自身的钩子,无需特殊处理。 + +集群词汇如下: + +- **`BudgetThresholdHit { resource, kind, consumed, limit, message? }`** —— 预算守卫返回了 `soft` 决策(或宿主越过了它自己跟踪的某个阈值)。`kind` 用于区分软性警告与更硬性的限制。 +- **`PassivationRequested { reason, deadline_ms? }`** —— 宿主请求会话进入一个安全、可持久化的状态,以便将其从当前节点驱逐。`deadline_ms` 若存在,则表示强制驱逐前的宽限窗口。 +- **`PeerInvocation { from_session_id, from_tenant_id?, correlation_id? }`** —— 另一个会话调用了本会话。这些标签让接收方能够把调用归属回其源租户和关联链。 + +这些事件通过你的会话内钩子已经在使用的、经过验证的同一套钩子 API 来观察——Node 中为 `session.registerHook`,Python 中为 `session.register_hook`(参见[钩子](/guide/hooks))。请将上述三个变体视为已记录在案的契约;宿主负责通过其钩子执行器发出它们。 + +## 确定性 ID 与时间(重放) + +希望在另一节点上对某次运行进行**逐位一致重放**的集群,必须消除常规运行中两处不确定性的来源:随机 ID 和挂钟时间。Rust 核心将二者建模在一个 `HostEnv { id_generator, clock }` 之后。默认实现把 UUID 生成器与系统时钟配对;重放工具会换入 `SequentialIdGenerator` 和 `FixedClock`,使得对相同输入的重新执行在任意节点上都产生相同的 ID 和时间戳,从而产生相同的输出。 + +这是当前**在 Rust 核心中配置的**。它尚未暴露在 JS/Python 选项面上,因此没有对应的 Node/Python 代码——SDK 接入可能随后跟进。 + +## 循环检查点与运行恢复 + +配置了 `sessionStore` / `session_store` 后,智能体循环会在**每一轮工具调用完成之后**持久化一个检查点,以运行 id 作为键。任何共享同一存储的节点都可以重新水合该运行并继续它。 + + + + +```ts +import { FileSessionStore } from '@a3s-lab/code'; + +const session = agent.session(workspace, { + sessionStore: new FileSessionStore('./.a3s/sessions'), + sessionId: 'session-from-node-a', +}); + +const result = await session.resumeRun('run-id-from-node-a'); +``` + + + + +```python +from a3s_code import FileSessionStore + +opts = SessionOptions() +opts.session_store = FileSessionStore('./.a3s/sessions') +opts.session_id = 'session-from-node-a' +session = agent.session(workspace, opts) + +result = session.resume_run('run-id-from-node-a') +``` + + + + +系统会为恢复的工作分配一个**新的运行 id**——存储中的原始运行保持不变。有两条错误路径值得处理: + +- **`resume_run requires a session_store`** —— 未配置存储;回退到一个全新会话。 +- **`no loop checkpoint found for run 'X'`** —— 该运行从未到达其第一个检查点,或已被清理;稍后重试,或将该运行视为丢失。 + +由于检查点只在**工具轮次之间、绝不在工具执行中途**生成,恢复的运行永远不会重放一个执行到一半的工具。存储细节参见[持久化](/guide/persistence)。 + +## 长时运行会话的保留上限 + +运行数小时或数天的会话会在四个内存存储中累积状态:运行记录、每次运行的事件缓冲区、追踪事件,以及终态子智能体任务快照。若不加限制,它们会随会话寿命增长——对短寿命会话无妨,对长寿命会话则是真实的泄漏。 + +`SessionRetentionLimits` 为这四个存储分别设置上限。每个上限都是可选的:`None` 表示无上限的默认值。驱逐采用严格的 **FIFO**,并且**正在运行的子智能体任务永不被丢弃**——只有终态(已完成/已失败)快照会被驱逐。 + +Node 使用 `retentionLimits`,Python 使用 `opts.retention_limits`。Rust 宿主使用 `SessionOptions::with_retention_limits(...)`。字段名和示例见[限制](/guide/limits)。 + +--- + +**另见:** [多机部署](/guide/multi-machine) · [持久化](/guide/persistence) · [限制](/guide/limits) · [钩子](/guide/hooks) diff --git a/website/docs/v6/zh/guide/commands.mdx b/website/docs/v6/zh/guide/commands.mdx new file mode 100644 index 00000000..f0fc4206 --- /dev/null +++ b/website/docs/v6/zh/guide/commands.mdx @@ -0,0 +1,59 @@ +--- +title: '命令' +description: '命令表面与推荐控制流' +--- + +# 命令 + +A3S Code 主要是 SDK 驱动。CLI 和 UI 通常把命令映射到 session API,而不是依赖 +一个庞大的公开命令协议。 + +本页说明 SDK command registry。`a3s code` 终端应用中的内置命令见 +[A3S Code TUI](/guide/tui)。 + +这两个 surface 刻意不同: + +| Surface | 所有者 | 用途 | +| ------------------------- | -------- | -------------------------------------------------------------------------------------------------- | +| `a3s code` slash commands | CLI/TUI | `/model`、`/flow`、`/memory`、`/kb`、`/update`、`/exit` 等终端控制。 | +| SDK command registry | 宿主应用 | 用 `session.registerCommand(...)` 注册、再通过 `session.send("/name args")` 触发的产品自定义命令。 | + +SDK command 会在 LLM 看到输入之前执行。handler 接收原始参数字符串和 session +元数据,并返回展示文本。命令适合薄薄的控制面动作;workflow、工具、验证和持久化 +应直接使用普通 SDK 方法。 + +| 用户动作 | Session API | +| ---------------------- | ----------------------------------------------------------------- | +| 发送 prompt | `session.send(prompt)` | +| 流式 prompt | `session.stream(prompt)` | +| 临时问题 | 创建隔离 session,或用显式隔离的 history 调用 `send` / `stream`。 | +| 直接工具 | `session.tool(name, args)` | +| 历史 | `session.history()` | +| 保存 | `session.save()` | +| 恢复 | `agent.resumeSession(id, options)` | +| 工具列表 | `session.toolNames()` / `session.toolDefinitions()` | +| 验证 | `session.verifyCommands(subject, commands)` | +| 回放 run 状态 | `session.runs()` / `session.runEvents(runId)` | +| 检查或取消 current run | `session.currentRun()` / `session.cancelRun(runId)` | + +如果产品提供 slash command,请让它们薄薄地映射到这些 API。注册 handler、列出 +可用命令,并通过 `session.send()` 触发: + +```ts +session.registerCommand( + 'docs_status', + 'Return docs command status', + (args, ctx) => { + return `status args=${args}; session=${ctx.sessionId}; workspace=${ctx.workspace}`; + }, +); + +console.log(session.listCommands()); + +const result = await session.send('/docs_status check'); +console.log(result.text); +``` + +command handler 不应隐藏长时间运行的工作。如果一个命令需要运行工具、验证输出、 +fan out 到 subagent 或调度周期性自动化,让 handler 返回简短确认,再由明确的宿主 +代码执行这些工作。这样取消、审计和权限边界仍然可见。 diff --git a/website/docs/v6/zh/guide/context.mdx b/website/docs/v6/zh/guide/context.mdx new file mode 100644 index 00000000..fa7f1021 --- /dev/null +++ b/website/docs/v6/zh/guide/context.mdx @@ -0,0 +1,32 @@ +--- +title: '上下文' +description: '可靠编程智能体的预算化上下文组装' +--- + +# 上下文 + +A3S Code 把上下文当作有预算的资源。 + +上下文来源包括用户 prompt、历史、AGENTS.md、skills、memory、文件搜索、直接工具结果、MCP、委派子运行摘要和 trace event。AGENTS.md 注入、skills discovery、memory API、直接工具结果、委派 helper 和 `traceEvents()` 属于 Node SDK 文档表面;MCP context 行为应针对你的 live integration 单独验证。 + +```text +sources -> ContextItem -> rank -> dedupe -> budget -> render +``` + +长 grep 输出、日志和委派子运行 transcript 不应直接塞进 prompt;应保存在 prompt 之外,再总结成适合 prompt 的证据。紧凑运行证据可通过 `session.traceEvents()` 获取。 + +## 上下文压缩 + +长会话可启用自动压缩: + +```ts +const session = agent.session('/repo', { + autoCompact: true, + autoCompactThreshold: 0.75, + maxContextTokens: 128_000, +}); +``` + +未设置 `maxContextTokens` 时,Core 会优先使用当前模型声明的上下文窗口。每次模型请求前,Core 会统计 system prompt、会话消息、工具调用与结果以及暴露给模型的工具 schema;达到阈值后,它会限制过大的工具输出、总结较早且边界安全的消息、保留近期消息,并继续当前任务。压缩摘要还会参与后续压缩,因此长会话可以持续滚动压缩;这不会扩大模型单次请求的物理上下文窗口。压缩成功时,`context_compacted` 事件会携带累计摘要,使用外部历史的宿主可以把同一压缩代持久化到后续轮次。 + +Python SDK 中对应的字段是 `max_context_tokens`。 diff --git a/website/docs/v6/zh/guide/convention-over-configuration.mdx b/website/docs/v6/zh/guide/convention-over-configuration.mdx new file mode 100644 index 00000000..f1b163bc --- /dev/null +++ b/website/docs/v6/zh/guide/convention-over-configuration.mdx @@ -0,0 +1,226 @@ +--- +title: '约定大于配置' +description: '用文件系统优先 Agent、工具、连接、子 Agent、调度、持久化和 HITL 构建长期工作单元。' +--- + +# 约定大于配置 + +A3S Code 的约定大于配置能力不是一个单独的 runner,而是一套长期 Agent +组合:[文件系统优先](/guide/filesystem-first)约定、会话运行时、工具权限、 +多 Agent 委派、可恢复编排、HITL、run replay 和调度守护进程。它让一个 Agent +从“能聊天的 SDK 会话”变成“有角色、有工具、有团队、有状态、有接管点的工作单元”。 + +如果你在评估目录优先的 agent framework,可以把 A3S Code 看成更底层、更可嵌入的运行时。 +目录约定、工具、连接、子 Agent、调度、持久化和观测都存在,但不会强制绑定到某个前端渠道 +或部署平台;宿主可以把它接入托管 session、开放平台 API、MCP、A3S Box 或自己的任务系统。 + +## 能力映射 + +| Agent framework 概念 | A3S Code 对应能力 | 说明 | +| --------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| Agent 是一个目录 | [Agent 目录](/guide/agent-dir) | `instructions.md`、`agent.acl`、`skills/`、`tools/`、`schedules/` 合成一个可运行 Agent。 | +| Markdown instructions | `instructions.md`、`AGENTS.md`、prompt slots | 指令作为 slot 注入,不能覆盖 harness 的边界、响应契约和安全门。 | +| Markdown skills | [Skills](/guide/skills) | 文件型 skill、inline skill 和显式 registry 使用同一套 discovery 语义。A3S Code 不再内置默认 skills。 | +| TypeScript tools | `program`、direct tools、MCP tools、AgentDir script tools | A3S Code 更关注工具注册、权限门、typed errors 和验证证据;脚本工具运行在 QuickJS 受限环境。 | +| Sandbox | `program` 沙箱、权限策略、HITL、A3S Box | A3S Code 控制工具权限和脚本沙箱;需要进程、文件系统、网络级隔离时接 A3S Box。 | +| Channels | 托管 session、开放平台 WebSocket/SSE、宿主自有 UI | A3S Code 不把渠道写死,宿主通过 streaming events 和 run replay 接到任意前端。 | +| Connections | MCP、provider 配置、宿主注入凭据 | 连接由宿主配置和授权,不建议把 token 写进 Agent 目录。 | +| Subagents | [任务](/guide/tasks)、[团队](/guide/teams)、`workerAgents` | 支持手动 `task`、并行 `parallel_task`、自动委派和动态 worker agents。 | +| Schedules | AgentDir `schedules/` + `serve_agent_dir` | 每个 schedule 有独立 session id,可配合 session store 在重启后恢复上下文。 | +| Durable execution | `SessionStore`、run replay、`parallelResumable` | 会话历史、run 事件和可恢复工作流能落盘;失败 step 可在恢复时重试。 | +| Human-in-the-loop | [安全](/guide/security)、确认继承、tool confirmation | 高风险工具可请求确认;子运行可配置自动批准、遇 Ask 失败或继承父级策略。 | +| Evaluations | [验证](/guide/verification)、报告、回归证据 | A3S Code 当前把“评测”产品化为验证命令、证据摘要、run replay 与发布门禁,而不是单独 eval suite。 | + +## 最小工作形态 + +交互式团队 Agent 可以从一个普通 repo 开始: + +```text +repo/ +├── agent.acl +├── AGENTS.md +├── .a3s/ +│ ├── agents/ +│ │ ├── release-reviewer.md +│ │ ├── security-reviewer.md +│ │ └── verification-runner.md +│ └── skills/ +│ └── release-readiness.md +└── src/ +``` + +`agent.acl` 负责模型、provider、并行度和自动委派: + +```text +default_model = "provider/model-id" +max_parallel_tasks = 4 +auto_parallel = false + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "model-id" { + tool_call = true + limit = { + context = 128000 + output = 4096 + } + } +} + +agent_dirs = ["./.a3s/agents"] + +auto_delegation { + enabled = true + min_confidence = 0.72 + max_tasks = 4 + auto_parallel = false +} +``` + +宿主启动一个 session,并把 skills、agent 目录、持久化和委派策略接进去: + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +const session = agent.session('/repo', { + skillDirs: ['./.a3s/skills'], + agentDirs: ['./.a3s/agents'], + autoDelegation: { enabled: true, minConfidence: 0.72, maxTasks: 4 }, + maxParallelTasks: 8, + autoParallel: false, + autoSave: true, +}); + +const result = await session.send(` +准备一次发布就绪检查: +1. 让探索角色找出高风险变更。 +2. 让安全角色检查权限、secret、外部副作用。 +3. 让验证角色给出必须跑的回归命令。 +4. 合并成一个按阻塞程度排序的报告。 +`); + +console.log(result.text); +console.log(result.verificationSummaryText); +``` + +## Agent 目录形态 + +需要长期运行、调度或目录级工具时,使用 filesystem-first agent: + +```text +release-agent/ +├── instructions.md +├── agent.acl +├── skills/ +│ └── release-readiness.md +├── schedules/ +│ └── daily.md +└── tools/ + ├── github.md + └── search-auth.md +``` + +`instructions.md` 是角色 slot: + +```md +You are a release-readiness agent for this repository. +Always separate blockers from follow-up work. +Never invent versions or CI status. Read evidence from the workspace. +``` + +`schedules/daily.md` 是周期性 turn: + +```md +--- +cron: '0 9 * * *' +name: daily-release-check +enabled: true +--- + +Summarize merged changes since the last run, inspect release risks, +and report only blockers plus required verification. +``` + +`serve_agent_dir` 会为每个 schedule 创建独立 session。传入 `SessionStore` 后,重启只重新加载当前目录配置和工具,历史上下文从 store 恢复。 + +## 工具与连接 + +A3S Code 的工具面分三层: + +| 层 | 用法 | 适合场景 | +| --------------------- | -------------------------------------------------------- | --------------------------------------------------------- | +| 内置与 direct tools | `session.tool(...)`、文件、shell、git、`generate_object` | 宿主明确知道要跑什么,或需要确定性调用。 | +| MCP | MCP server | 连接 GitHub、Linear、内部系统、远程宿主工具或跨进程能力。 | +| AgentDir script tools | `tools/*.md` + QuickJS `program` | 把一段受限脚本包装成模型可见工具。 | + +工具不是“文件存在就无限可用”。可见性、权限门、HITL、allow-list 和 sandbox 都由 harness 或 AgentDir loader 统一控制。高权限工具应只给可信目录,secret 应通过环境变量和宿主连接注入。 + +## 子 Agent 与团队 + +`subagents/` 在 A3S Code 中拆成三种入口: + +| 入口 | 谁决定分工 | 适合场景 | +| ------------------------------------------------------------------------------------ | ---------- | ------------------------------------------ | +| `task` / `parallel_task` | 父 agent | 模型自己判断要不要委派。 | +| `session.task(...)` / `session.tasks(...)` | 宿主代码 | 宿主已经知道 lane,但仍想复用 Agent 能力。 | +| `session.parallel(...)` / `session.pipeline(...)` / `session.parallelResumable(...)` | 宿主代码 | 需要可复现、可测试、可恢复的固定工作流。 | + +自动委派依赖 agent 描述和置信度评分。它适合“用户只描述目标,由运行时挑选 specialist”的场景;固定发布流程、批量审查、迁移任务更适合用编排显式表达。 + +## 可观测与接管 + +长期 Agent 必须能被观察和中止。A3S Code 的核心观察面包括: + +- `stream()` 输出增量事件。 +- `runs()`、`runSnapshot()`、`runEvents()` 查看当前和历史 run。 +- `toolNames()` / `toolDefinitions()` 查看可见工具表面。 +- `activeTools()` 查看当前正在运行的工具调用快照。 +- `cancelRun(runId)` 中止正在执行的回合。 +- `traceEvents()` 读取压缩、委派、工具和验证证据。 +- `verificationSummaryText` 给发布或审核流程使用。 + +宿主平台可以把这些事件转成 WebSocket/SSE、审计记录、调试面板和工作流节点状态。 + +## 与 A3S Box 的关系 + +A3S Code 负责智能体循环、工具、委派、状态和验证。A3S Box 负责更强的运行隔离:MicroVM、OCI workload、网络和 TEE。需要“模型能跑 shell,但进程必须隔离”的场景,应把 A3S Code 的工具执行放进 A3S Box 或由宿主通过 MCP 暴露隔离后的能力。 + +典型组合: + +```text +A3S Code session + -> permission policy and HITL + -> MCP tool adapter + -> A3S Box isolated workload + -> typed result and verification evidence +``` + +## 什么时候用 + +适合: + +- 发布巡检、依赖升级、代码库维护等长期工程 Agent。 +- 能拆成 explore / review / verify / implement 多角色协作的任务。 +- 需要自动委派,但仍要保留权限门、审计和验证证据的工作。 +- 需要 cron 调度和可恢复上下文的周期性报告。 +- 需要接入托管工作流、宿主 session 或外部协作渠道的 Agent。 + +不适合: + +- 只需要一次确定性 API 调用的工具型资产,直接用 tool contract 更简单。 +- 需要完整 GUI 自动化但没有结构化工具接口的任务,应优先提供 MCP 工具或浏览器工具,再交给 A3S Code 调度。 +- 需要强 OS 级隔离却只启用本地 shell 的任务,应接入 A3S Box。 + +## 阅读顺序 + +1. [文件系统优先](/guide/filesystem-first) +2. [Agent 目录](/guide/agent-dir) +3. [agents/ 角色目录](/guide/filesystem-agents) +4. [tools/ 工具目录](/guide/filesystem-tools) +5. [schedules/ 调度目录](/guide/filesystem-schedules) +6. [任务](/guide/tasks) +7. [团队](/guide/teams) diff --git a/website/docs/v6/zh/guide/examples/_meta.json b/website/docs/v6/zh/guide/examples/_meta.json new file mode 100644 index 00000000..6814f12c --- /dev/null +++ b/website/docs/v6/zh/guide/examples/_meta.json @@ -0,0 +1,22 @@ +[ + "index", + "quick-start", + "streaming", + "direct-tools", + "structured-output", + "batch", + "planning", + "orchestration", + "external-tasks", + "lane-queue", + "memory", + "auto-compact", + "prompt-slots", + "ripgrep-context", + "model-switching", + "skills", + "skill-tool", + "hooks", + "security", + "git-worktree" +] diff --git a/website/docs/v6/zh/guide/examples/auto-compact.mdx b/website/docs/v6/zh/guide/examples/auto-compact.mdx new file mode 100644 index 00000000..b8dcebd4 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/auto-compact.mdx @@ -0,0 +1,81 @@ +--- +title: '自动压缩' +description: '让 runtime 自动将长 session 保持在上下文预算内' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 自动压缩 + +A3S Code 可以替你将长对话保持在模型的上下文预算内。启用 `autoCompact` 后, +runtime 会持续监测上下文用量;一旦超过 `autoCompactThreshold`,较早的轮次就会被 +压缩成一份持续更新的摘要,让 agent 在大量步骤中始终保持连贯,而无需你手动管理 +token。续写则处理另一个方向:当单条回复因长度被截断时,runtime 会自动继续生成, +拼出完整回复。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +const session = agent.session('/repo', { + // Compact older turns once context fills past the threshold. + autoCompact: true, + autoCompactThreshold: 0.75, + // Auto-continue a single response that the model truncates by length. + continuationEnabled: true, + maxContinuationTurns: 3, +}); + +// Run a long, multi-step task. The runtime compacts older turns as needed; +// you never touch the token math. +for (let i = 0; i < 50; i++) { + await session.send(`Step ${i}: continue refactoring the parser`); +} + +// Inspect what the session is currently carrying. +console.log('history turns:', session.history().length); +console.log('recent memory:', await session.memoryRecent(5)); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") + +opts = SessionOptions() +# Compact older turns once context fills past the threshold. +opts.auto_compact = True +opts.auto_compact_threshold = 0.75 +# Auto-continue a single response that the model truncates by length. +opts.continuation_enabled = True +opts.max_continuation_turns = 3 +session = agent.session("/repo", opts) + +# Run a long, multi-step task. The runtime compacts older turns as needed; +# you never touch the token math. +for i in range(50): + session.send(f"Step {i}: continue refactoring the parser") + +# Inspect what the session is currently carrying. +print("history turns:", len(session.history())) +print("recent memory:", session.memory_recent(5)) + +session.close() +``` + + + + +长 session 若希望由 runtime 替你管理 context pressure,就使用自动压缩。 +`autoCompactThreshold` / `auto_compact_threshold` 是触发压缩的上下文窗口占比 +(0.0–1.0,默认 0.8);调低它可以更早触发压缩。用 `history()`(同步)和 +`memoryRecent` / `memory_recent` 查看实时 session。 diff --git a/website/docs/v6/zh/guide/examples/batch.mdx b/website/docs/v6/zh/guide/examples/batch.mdx new file mode 100644 index 00000000..df3eb0a4 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/batch.mdx @@ -0,0 +1,83 @@ +--- +title: 'Batch' +description: '通过组合 SDK 的确定性辅助方法来批量执行确定性操作' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Batch + +SDK 中并没有 `batch()` 方法。当宿主工作流或某个 agent 回合已经有一组清晰、相互独立、 +确定性的步骤时,你可以直接用会话提供的确定性辅助方法(`readFile`、`grep`、`glob`、`ls`、 +`git`)把它们组合起来,并自行汇总结果。这样"批处理"就完全在你的掌控之中:无需任何模型调用, +执行顺序明确,且除非你主动调用,否则不会执行任何破坏性操作。 + +下面的示例读取包元数据、changelog 和 release script,然后在不编辑任何文件的前提下报告版本不一致之处。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/path/to/project'); + +// Node helpers are async — group independent reads with Promise.all. +const [pkg, changelog, releaseScript] = await Promise.all([ + session.readFile('package.json'), + session.readFile('CHANGELOG.md'), + session.readFile('scripts/release.sh'), +]); + +const pkgVersion = JSON.parse(pkg).version; +const mismatches = []; +if (!changelog.includes(pkgVersion)) + mismatches.push(`CHANGELOG.md is missing ${pkgVersion}`); +if (!releaseScript.includes(pkgVersion)) + mismatches.push(`release.sh is missing ${pkgVersion}`); + +console.log( + mismatches.length ? mismatches.join('\n') : 'All files agree on the version.', +); + +session.close(); +``` + + + + +```python +import json +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session("/path/to/project", SessionOptions()) + +# Python helpers are synchronous — call them in sequence, no await. +pkg = session.read_file("package.json") +changelog = session.read_file("CHANGELOG.md") +release_script = session.read_file("scripts/release.sh") + +pkg_version = json.loads(pkg)["version"] +mismatches = [] +if pkg_version not in changelog: + mismatches.append(f"CHANGELOG.md is missing {pkg_version}") +if pkg_version not in release_script: + mismatches.append(f"release.sh is missing {pkg_version}") + +print("\n".join(mismatches) if mismatches else "All files agree on the version.") + +session.close() +``` + + + + +不要把破坏性操作混入这些分组读取之中。任何破坏性宿主工作流(写入、`git` 提交、`bash`) +都应放在显式的应用确认或你的自动化关卡之后,并通过 `permissionPolicy` / +`permission_policy` 进行约束,避免出现意料之外的步骤被静默执行。 + +如果这些步骤并非相互独立——每一步都依赖上一步的结果,并且你希望由 agent 来驱动它们—— +请改用 [`session.pipeline(...)`](/guide/examples/orchestration),它会分阶段运行任务, +每个阶段都能接收到上一阶段的输出。 diff --git a/website/docs/v6/zh/guide/examples/direct-tools.mdx b/website/docs/v6/zh/guide/examples/direct-tools.mdx new file mode 100644 index 00000000..7502ffa7 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/direct-tools.mdx @@ -0,0 +1,147 @@ +--- +title: '直接工具' +description: '不消耗 LLM 回合即可运行确定性宿主工具' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 直接工具 + +`session.tool(name, args)`(以及 `glob`、`grep`、`readFile` 等类型化辅助方法)会直接运行宿主工具,循环中不发起任何模型调用。它们适用于测试、迁移以及宿主驱动的工作流——你需要确定性结果,而不是一次智能体回合。 + +直接调用是宿主控制面调用。调用前请先执行你的产品授权逻辑;`permissionPolicy` +管控的是 agent turn 内由模型选择的工具调用,不是你的应用代码是否可以调用 SDK helper。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.'); + +const allowedDirectTools = new Set(['glob', 'grep', 'read', 'generate_object']); +function assertDirectToolAllowed(name: string) { + if (!allowedDirectTools.has(name)) { + throw new Error(`direct tool not allowed here: ${name}`); + } +} + +// Glob: list files by pattern +assertDirectToolAllowed('glob'); +const files = await session.glob('**/*.ts'); +console.log(`glob found ${files.length} TypeScript files`); + +// Grep: search file contents +assertDirectToolAllowed('grep'); +const matches = await session.grep('Agent.create'); +const matchCount = matches.split('\n').filter(Boolean).length; +console.log(`grep found ${matchCount} matching lines`); + +// Read a file +assertDirectToolAllowed('read'); +const readme = await session.readFile('README.md'); +console.log(`README is ${readme.length} bytes`); + +// Direct tool call by name +assertDirectToolAllowed('read'); +const raw = await session.tool('read', { file_path: 'package.json' }); +console.log(`package.json via tool(): ${raw.output.length} bytes`); + +// Inspect available tool schemas +const schemas = session.toolDefinitions(); +console.log(`session exposes ${schemas.length} tools`); + +// Structured output: generate a schema-validated JSON object +assertDirectToolAllowed('generate_object'); +const structured = await session.tool('generate_object', { + schema: { + type: 'object', + required: ['count', 'language'], + properties: { + count: { type: 'integer' }, + language: { type: 'string' }, + }, + }, + prompt: 'How many TypeScript files are in this project?', + schema_name: 'file_stats', +}); +if (structured.exitCode !== 0) { + throw new Error(structured.output); +} +console.log('structured output:', JSON.parse(structured.output).object); + +session.close(); +``` + + + + +```python +import json + +from a3s_code import Agent + +agent = Agent.create("agent.acl") +session = agent.session('.') + +ALLOWED_DIRECT_TOOLS = {'glob', 'grep', 'read', 'generate_object'} + + +def assert_direct_tool_allowed(name: str) -> None: + if name not in ALLOWED_DIRECT_TOOLS: + raise RuntimeError(f'direct tool not allowed here: {name}') + +# Glob: list files by pattern +assert_direct_tool_allowed('glob') +files = session.glob('**/*.py') +print(f'glob found {len(files)} Python files') + +# Grep: search file contents +assert_direct_tool_allowed('grep') +matches = session.grep('Agent.create') +match_count = len([line for line in matches.splitlines() if line]) +print(f'grep found {match_count} matching lines') + +# Read a file +assert_direct_tool_allowed('read') +readme = session.read_file('README.md') +print(f'README is {len(readme)} bytes') + +# Direct tool call by name +assert_direct_tool_allowed('read') +raw = session.tool('read', {'file_path': 'pyproject.toml'}) +print(f'pyproject.toml via tool(): {len(raw.output)} bytes') + +# Inspect available tool schemas +schemas = session.tool_definitions() +print(f'session exposes {len(schemas)} tools') + +# Structured output: generate a schema-validated JSON object +assert_direct_tool_allowed('generate_object') +structured = session.tool('generate_object', { + 'schema': { + 'type': 'object', + 'required': ['count', 'language'], + 'properties': { + 'count': {'type': 'integer'}, + 'language': {'type': 'string'}, + }, + }, + 'prompt': 'How many Python files are in this project?', + 'schema_name': 'file_stats', +}) +if structured.exit_code != 0: + raise RuntimeError(structured.output) +print('structured output:', json.loads(structured.output)['object']) + +session.close() +``` + + + + +直接工具在 session 工作区下执行,应视为宿主侧的特权操作。大多数调用(`read`、`glob`、`grep`)是纯确定性的;`generate_object` 是个例外——它仍会调用模型来填充经过 schema 校验的 JSON 对象,但由你显式驱动,而非通过自由形式的智能体回合。 + +可运行版本位于 `crates/code/sdk/node/examples/basic/test_generate_object.ts`(Python:`crates/code/sdk/python/examples/test_generate_object.py`)。 diff --git a/website/docs/v6/zh/guide/examples/external-tasks.mdx b/website/docs/v6/zh/guide/examples/external-tasks.mdx new file mode 100644 index 00000000..f707348a --- /dev/null +++ b/website/docs/v6/zh/guide/examples/external-tasks.mdx @@ -0,0 +1,118 @@ +--- +title: '外部任务' +description: '在 agent 进程之外完成 agent 排队的工作,并把结构化证据报告回去' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 外部任务 + +有些工作无法在 agent 进程内运行:它属于独立的 worker、CI runner,或位于另一个系统中的 +人工处理者。当某个 lane 被路由到外部处理器时,该 lane 上的工具会被**排队**而不是被执行—— +它们会作为外部任务等待。你的宿主代码会取出待处理队列、以任意方式完成工作,并通过 +`completeExternalTask` 把结果报告回去。仅当外部 worker 确实是你架构的一部分时,才使用此模式。 + +外部任务由 [lane 队列](/guide/examples/lane-queue) 产生:你必须先注册至少一个 +`external`(或 `hybrid`)lane 处理器,否则每个任务都会在进程内运行,也就没有什么可取出的。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session(process.cwd()); + +// Route a lane to an external worker so its tools are queued, not executed. +await session.setLaneHandler('execute', { + mode: 'external', + timeoutMs: 300000, +}); + +// Drain the tasks waiting for the host to fulfill. +const pending = await session.pendingExternalTasks(); + +for (const task of pending) { + console.log( + `pending: ${task.task_id} on ${task.lane} (${task.command_type})`, + ); + + try { + // ...the host does the real work here (run CI, call a service, ask a human)... + const ok = await session.completeExternalTask(task.task_id, { + success: true, + result: { + summary: 'worker completed the test run', + command: 'npm run build', + exitCode: 0, + }, + }); + console.log('completed:', ok); + } catch (err) { + await session.completeExternalTask(task.task_id, { + success: false, + error: String(err), + }); + } +} + +session.close(); +``` + + + + +```python +import os +from a3s_code import Agent + +agent = Agent.create("agent.acl") +session = agent.session(os.getcwd()) + +# Route a lane to an external worker so its tools are queued, not executed. +session.set_lane_handler("execute", "external", 300000) + +# Drain the tasks waiting for the host to fulfill. +pending = session.pending_external_tasks() + +for task in pending: + print(f"pending: {task['task_id']} on {task['lane']} ({task['command_type']})") + + try: + # ...the host does the real work here (run CI, call a service, ask a human)... + ok = session.complete_external_task( + task["task_id"], + success=True, + result={ + "summary": "worker completed the test run", + "command": "npm run build", + "exit_code": 0, + }, + ) + print("completed:", ok) + except Exception as err: + session.complete_external_task( + task["task_id"], + success=False, + error=str(err), + ) + +session.close() +``` + + + + +说明: + +- 每个待处理任务都携带 `task_id`、`session_id`、`lane`、`command_type`、`payload` 和 + `timeout_ms`。完成时请把 `task_id` 传回 `completeExternalTask` / `complete_external_task`, + 以便将本次完成匹配到正确的任务。 +- 结果的结构为 `{ success, result?, error? }`。其中 `result` 可以是任意可 JSON 序列化的负载; + `error` 是失败时的可选消息。 +- 成功时,返回紧凑的结构化证据(一段摘要加上关键事实),而不是只有原始日志——agent 会基于 + 该结果进行推理,所以请保持其精简且机器可读。 +- 当任务被找到并完成时,`completeExternalTask` / `complete_external_task` 返回 `true`,否则 + 返回 `false`。在 Python 中这些队列方法是同步的;在 Node 中 `pendingExternalTasks` 和 + `completeExternalTask` 返回 promise。 diff --git a/website/docs/v6/zh/guide/examples/git-worktree.mdx b/website/docs/v6/zh/guide/examples/git-worktree.mdx new file mode 100644 index 00000000..355523ab --- /dev/null +++ b/website/docs/v6/zh/guide/examples/git-worktree.mdx @@ -0,0 +1,105 @@ +--- +title: 'Git Worktree' +description: '通过 session 的 git 工具操作 git 与 git worktree' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Git Worktree + +session 的 `git` 工具以宿主侧特权操作的方式运行 git。它接收一个结构化的命令对象 +(`command`,worktree 操作还可带 `subcommand`/`name`/`path`),并返回包含 +`output` 与 `exitCode` 的工具结果。本示例先检查仓库,然后直接通过工具表面创建、 +列出并移除一个 worktree。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; +import * as path from 'path'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/path/to/repo'); + +// Inspect the repo +const status = await session.git({ command: 'status' }); +console.log(status.output); + +// Create a worktree on a new branch +const wtPath = path.join('/path/to/repo', 'wt-feature-auth'); +const created = await session.git({ + command: 'worktree', + subcommand: 'create', + name: 'feature-auth', + path: wtPath, +}); +if (created.exitCode !== 0) throw new Error(`create failed: ${created.output}`); + +// List worktrees +const list = await session.git({ command: 'worktree', subcommand: 'list' }); +console.log(list.output); + +// Remove the worktree when done +const removed = await session.git({ + command: 'worktree', + subcommand: 'remove', + path: wtPath, +}); +if (removed.exitCode !== 0) throw new Error(`remove failed: ${removed.output}`); + +session.close(); +``` + + + + +```python +import os +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session("/path/to/repo", SessionOptions()) + +# Inspect the repo +status = session.git({"command": "status"}) +print(status.output) + +# Create a worktree on a new branch +wt_path = os.path.join("/path/to/repo", "wt-feature-auth") +created = session.git({ + "command": "worktree", + "subcommand": "create", + "name": "feature-auth", + "path": wt_path, +}) +if created.exit_code != 0: + raise RuntimeError(f"create failed: {created.output}") + +# List worktrees +listing = session.git({"command": "worktree", "subcommand": "list"}) +print(listing.output) + +# Remove the worktree when done +removed = session.git({ + "command": "worktree", + "subcommand": "remove", + "path": wt_path, +}) +if removed.exit_code != 0: + raise RuntimeError(f"remove failed: {removed.output}") + +session.close() +``` + + + + +请传入命令对象,而不是位置参数:`{ command: 'status' }`、`{ command: 'diff' }` +或 `{ command: 'worktree', subcommand: 'list' }`。每次调用都会返回一个工具结果, +因此在使用 output 之前,应先检查 `exitCode`(Node)/ `exit_code`(Python)。 + +直接 git 调用是宿主侧特权操作。push、publish 和 release workflow 应放在应用级确认 +或自动化关卡后面。 + +可运行版本位于 `crates/code/sdk/node/examples/git/test_worktree_git.ts`。 diff --git a/website/docs/v6/zh/guide/examples/hooks.mdx b/website/docs/v6/zh/guide/examples/hooks.mdx new file mode 100644 index 00000000..d5a3fb34 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/hooks.mdx @@ -0,0 +1,91 @@ +--- +title: '生命周期钩子' +description: '注册、统计并注销用于观测和把控 Agent 活动的生命周期事件回调。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 生命周期钩子 + +钩子让你在 Agent 活动发生时进行观测和把控。你针对某个生命周期事件注册一个具名回调, +运行时会在该节点调用它,回调返回一个决策,例如 `{ action: "continue" }`。钩子可用于 +审计、脱敏、日志记录,或在不修改 Agent 提示词的前提下实施策略。 + +其生命周期是对称的:`registerHook` 按名称添加回调,`hookCount` 告诉你当前有多少个钩子 +处于活动状态,`unregisterHook` 则按名称移除某个钩子。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session(process.cwd()); + +// Register a named hook on a lifecycle event. The callback must NOT throw — +// always return a decision such as { action: 'continue' }. +session.registerHook( + 'observe-env-read', + 'pre_tool_use', + { pathPattern: '**/.env*' }, + { priority: 100 }, + () => ({ action: 'continue' }), +); + +console.log('active hooks:', session.hookCount()); // 1 + +await session.run('Read the project README and summarize it.'); + +// Remove the hook by name when you no longer need it. +session.unregisterHook('observe-env-read'); +console.log('active hooks:', session.hookCount()); // 0 + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session('.', SessionOptions()) + +# Register a named hook on a lifecycle event. The callback returns a decision. +session.register_hook( + 'observe-env-read', + 'pre_tool_use', + {'pathPattern': '**/.env*'}, + {'priority': 100}, + lambda: {'action': 'continue'}, +) + +print("active hooks:", session.hook_count()) # 1 + +session.run("Read the project README and summarize it.") + +# Remove the hook by name when you no longer need it. +session.unregister_hook('observe-env-read') +print("active hooks:", session.hook_count()) # 0 + +session.close() +``` + + + + +注意事项: + +- 钩子回调返回一个决策。返回 `{ action: "continue" }`(Node)/ + `{"action": "continue"}`(Python)即可让 Agent 继续执行。 +- 匹配器(`{ pathPattern: '**/.env*' }`)将钩子限定到路径匹配该模式的事件,而 + `{ priority: 100 }` 用于对同一事件上的多个钩子排序(数值越大越先执行)。 +- Node 钩子回调**不得**抛出异常——未捕获的抛出可能会终止进程。请让处理逻辑保持完备, + 并始终返回一个决策。 +- `hookCount` / `hook_count` 反映当前已注册钩子的数量,在测试中可方便地断言注册与 + 清理是否生效。 +- `unregisterHook` / `unregister_hook` 接收你注册时使用的名称。请始终拆除不再需要的 + 钩子,以免它们在多次运行之间泄漏。 +- 把钩子当作生产关卡前,请先验证你所依赖的具体 event path。 diff --git a/website/docs/v6/zh/guide/examples/index.mdx b/website/docs/v6/zh/guide/examples/index.mdx new file mode 100644 index 00000000..97651ee6 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/index.mdx @@ -0,0 +1,22 @@ +--- +title: '示例' +description: '当前 A3S Code runtime 和 SDK surface 示例' +--- + +# 示例 + +这些示例使用当前 A3S Code runtime 的概念:ACL 配置、环境变量注入、session API、 +流式输出、结构化输出、基于 task 的委派与自动 subagent 委派、可编程编排 +(`parallel` / `pipeline` / `parallelResumable`)、`.a3s/agents`、技能、记忆、 +直接工具、workspace backend、验证、Git 工作流以及可选的 MCP/队列基础设施。 + +建议从[快速开始](/guide/examples/quick-start)开始,然后按主题阅读: + +- **Session 与运行时** — [快速开始](/guide/examples/quick-start)、[流式输出](/guide/examples/streaming)、[模型切换](/guide/examples/model-switching)、[自动压缩](/guide/examples/auto-compact) +- **结构化与可编程** — [结构化输出](/guide/examples/structured-output)、[编排](/guide/examples/orchestration)、[规划](/guide/examples/planning)、[批处理](/guide/examples/batch) +- **工具与上下文** — [直接工具](/guide/examples/direct-tools)、[ripgrep 上下文](/guide/examples/ripgrep-context)、[Prompt 插槽](/guide/examples/prompt-slots)、[Git worktree](/guide/examples/git-worktree) +- **技能与记忆** — [技能](/guide/examples/skills)、[技能工具](/guide/examples/skill-tool)、[记忆](/guide/examples/memory)、[Hooks](/guide/examples/hooks) +- **安全与验证** — [安全](/guide/examples/security) +- **MCP 与队列** — [Lane 队列](/guide/examples/lane-queue)、[外部任务](/guide/examples/external-tasks) + +参见[编排](/guide/examples/orchestration)示例,了解扇出(`parallel`)、分阶段(`pipeline`)和可恢复(`parallelResumable`)的多 agent 工作流。 diff --git a/website/docs/v6/zh/guide/examples/lane-queue.mdx b/website/docs/v6/zh/guide/examples/lane-queue.mdx new file mode 100644 index 00000000..a9ff18d6 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/lane-queue.mdx @@ -0,0 +1,90 @@ +--- +title: 'Lane Queue' +description: '将某条 lane 路由到外部 worker,并显式排空其待处理任务。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Lane Queue + +默认情况下,A3S Code 会在进程内运行每一个任务,没有任何队列。lane 队列属于**可选基础设施**:为某条 lane 注册一个外部 handler 后,路由到该 lane 的工具不会由 agent 执行,而是排入队列、等待外部 worker 取走。随后你自己排空这些待处理任务,按需运行它们,再把结果回报回来。只有当外部 worker 确实是你架构的一部分时,才需要用到它。 + +共有四条 lane:`control`、`query`、`execute` 和 `generate`。每个 handler 的 `mode` 可以是 `internal`(默认)、`external` 或 `hybrid`。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('./agent.acl'); +const session = agent.session(process.cwd()); + +// Route the "execute" lane to an external worker. +// Tools on this lane are NOT run by the agent; they are queued for +// an outside worker to pick up and complete. +await session.setLaneHandler('execute', { + mode: 'external', + timeoutMs: 300000, +}); + +// hasQueue() is false until at least one external/hybrid lane is registered. +console.log('queue active:', session.hasQueue()); + +// Drain whatever is waiting for an external worker. +const pending = await session.pendingExternalTasks(); +for (const task of pending) { + console.log('pending:', task.task_id, task.lane, task.command_type); + + // ... hand off to your worker, run it, then report the outcome back: + await session.completeExternalTask(task.task_id, { + success: true, + result: { note: 'done by external worker' }, + }); +} + +console.log('lane queue drained'); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +session = agent.session(".", opts) + +# Route the "execute" lane to an external worker. +# Tools on this lane are NOT run by the agent; they are queued for +# an outside worker to pick up and complete. +session.set_lane_handler("execute", "external", 300000) + +# has_queue() is False until at least one external/hybrid lane is registered. +print("queue active:", session.has_queue()) + +# Drain whatever is waiting for an external worker. +pending = session.pending_external_tasks() +for task in pending: + print("pending:", task["task_id"], task["lane"], task["command_type"]) + + # ... hand off to your worker, run it, then report the outcome back: + session.complete_external_task( + task["task_id"], + success=True, + result={"note": "done by external worker"}, + ) + +print("lane queue drained") +``` + + + + +说明: + +- **默认路径不含队列。** 在你至少注册一个 `external`(或 `hybrid`)lane handler 之前,`hasQueue()` / `has_queue()` 始终返回 `false`。如果你从不调用 `setLaneHandler` / `set_lane_handler`,每个任务都会在进程内运行,也就没有任何待排空的内容。 +- 每个待处理任务都带有 `task_id`、`session_id`、`lane`、`command_type`、`payload` 和 `timeout_ms`。工作完成后,把 `task_id` 传回给 `completeExternalTask` / `complete_external_task`。 +- 结果结构为 `{ success, result?, error? }`——`result` 可承载任意可 JSON 序列化的载荷,`error` 是失败时的可选消息。`completeExternalTask` / `complete_external_task` 在找到并完成任务时返回 `true`,否则返回 `false`。 +- 在 Python 中这些队列方法是同步的;在 Node 中 `setLaneHandler`、`pendingExternalTasks` 和 `completeExternalTask` 返回 promise,而 `hasQueue` 是同步的。 diff --git a/website/docs/v6/zh/guide/examples/memory.mdx b/website/docs/v6/zh/guide/examples/memory.mdx new file mode 100644 index 00000000..246f3270 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/memory.mdx @@ -0,0 +1,91 @@ +--- +title: '记忆' +description: '记录任务结果,之后按相似度、标签或时间召回。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 记忆 + +持久化记忆让会话能够记录哪些做法成功(以及哪些失败),并在之后取回这些事实。 +SDK session 默认在 `/.a3s/memory` 使用文件存储;TUI 默认使用 +`~/.a3s/memory`,让 `/memory` 面板和实时 session 浏览同一份长期记忆。只有当你想 +覆盖路径或后端时,才需要传入 `memoryStore`。你仍然可以用 `rememberSuccess` / +`rememberFailure` 显式写入结果,再用 `recallSimilar`、`recallByTags` 或 +`memoryRecent` 取回它们。 + +LLM 抽取也默认开启。它会在重要 turn 完成后整理可复用的偏好、workflow、decision +和失败教训,普通寒暄或短 turn 会被跳过。 +默认 store 会把完全重复和保守近重复 memory 合并到 canonical item;带冲突意味的 +memory 会保留为独立项,方便后续 recall。 + + + + +```ts +import { Agent, FileMemoryStore } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/repo', { + memoryStore: new FileMemoryStore('./.a3s/memory'), +}); + +// Record outcomes as the agent works. +await session.rememberSuccess( + 'refactored auth module', + ['read', 'edit', 'bash'], + 'all tests passed after extracting AuthService', +); +await session.rememberFailure( + 'migration attempt', + ['bash'], + 'psql connection refused on port 5432', +); + +// Recall later — by recency, by tool tags, or by semantic similarity. +const recent = await session.memoryRecent(10); +const byTags = await session.recallByTags(['read', 'edit'], 5); +const similar = await session.recallSimilar('auth refactor', 5); + +console.log(recent.length, byTags.length, similar.length); +``` + + + + +```python +from a3s_code import Agent, SessionOptions, FileMemoryStore + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.memory_store = FileMemoryStore("./.a3s/memory") +session = agent.session("/repo", opts) + +# Record outcomes as the agent works. Python helpers are synchronous — no await. +session.remember_success( + "refactored auth module", + ["read", "edit", "bash"], + "all tests passed after extracting AuthService", +) +session.remember_failure( + "migration attempt", + ["bash"], + "psql connection refused on port 5432", +) + +# Recall later — by recency, by tool tags, or by semantic similarity. +recent = session.memory_recent(10) +by_tags = session.recall_by_tags(["read", "edit"], 5) +similar = session.recall_similar("auth refactor", 5) + +print(len(recent), len(by_tags), len(similar)) +``` + + + + +`rememberSuccess` 和 `rememberFailure` 都接收一段简短的任务描述、相关工具列表 +(这些工具同时充当可检索的标签),以及结果文本。三个召回方法相互补充: +`memoryRecent(limit)` 返回最新的记录,`recallByTags(tags, limit)` 按你记录的 +工具标签过滤,`recallSimilar(query, limit)` 则按与查询的语义相关度对记录排序。 +若默认文件 store 初始化失败,session 会降级到进程内 memory,并暴露 init warning。 diff --git a/website/docs/v6/zh/guide/examples/model-switching.mdx b/website/docs/v6/zh/guide/examples/model-switching.mdx new file mode 100644 index 00000000..04eb950e --- /dev/null +++ b/website/docs/v6/zh/guide/examples/model-switching.mdx @@ -0,0 +1,173 @@ +--- +title: '模型切换' +description: '为每个会话选择模型,并可针对每个 worker 智能体单独覆盖,以平衡成本与能力。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 模型切换 + +会话会使用你在 `model` 选项中传入的模型运行。先声明一次智能体可以访问的所有模型, +然后为每个会话选择其一——用快速模型处理高频、低风险的工作,用能力更强的模型进行 +评审。当你希望在不改动任何提示词的前提下,于成本与能力之间取得平衡时,可以使用这 +种方式。 + +## 声明模型 + +模型在智能体文件中配置。每个 provider 列出它对外暴露的模型,当会话未设置 `model` +时则使用 `default_model`。 + +```text +default_model = "provider/fast-model" + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "fast-model" { tool_call = true } + models "review-model" { tool_call = true } +} +``` + +## 按会话设置模型 + +`model` 选项在打开会话时设置。该会话运行的一切——`send`、`run`、`task`、 +`parallel`、`pipeline`——都会使用该模型。同一个智能体配置可以为不同会话驱动不同的 +模型选择。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +// A fast model for high-volume, low-stakes work. +const fast = agent.session('/repo', { model: 'provider/fast-model' }); +const draft = await fast.run('Draft a short README intro for this project.'); +console.log('draft:', draft); +await fast.close(); + +// A stronger model for review / higher-stakes reasoning. +const review = agent.session('/repo', { model: 'provider/review-model' }); +const critique = await review.run(`Critique this README intro:\n${draft}`); +console.log('critique:', critique); +await review.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") + +# A fast model for high-volume, low-stakes work. +fast_opts = SessionOptions() +fast_opts.model = 'provider/fast-model' +fast = agent.session('/repo', fast_opts) +draft = fast.run('Draft a short README intro for this project.') +print('draft:', draft) +fast.close() + +# A stronger model for review / higher-stakes reasoning. +review_opts = SessionOptions() +review_opts.model = 'provider/review-model' +review = agent.session('/repo', review_opts) +critique = review.run(f'Critique this README intro:\n{draft}') +print('critique:', critique) +review.close() +``` + + + + +## 按 worker 智能体覆盖模型 + +worker 智能体通过各自的规格进行注册。为某个 worker 指定自己的 `model`,即可让它运行 +在与委派它的会话不同(通常更小、更廉价)的模型上。编排会话保留自己的 `model`,只有被 +委派出去的工作才会运行在该 worker 的模型上。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +const session = agent.session('/repo', { + // Orchestrator stays on the stronger model. + model: 'provider/review-model', + // High-volume exploration runs on the cheaper model. + workerAgents: [ + { + name: 'scout', + description: 'Reads files and reports findings.', + model: 'provider/fast-model', + }, + ], +}); + +// Delegate exploration to the cheaper worker, then reason on the strong model. +const findings = await session.task({ + agent: 'scout', + description: 'List public APIs', + prompt: 'List every public API in src/.', +}); +const plan = await session.run( + `Given these findings, propose a refactor:\n${findings.output}`, +); +console.log(plan); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions, WorkerAgentSpec + +agent = Agent.create("agent.acl") + +opts = SessionOptions() +# Orchestrator stays on the stronger model. +opts.model = 'provider/review-model' +# High-volume exploration runs on the cheaper model. +scout = WorkerAgentSpec( + name='scout', + description='Reads files and reports findings.', +) +scout.model = 'provider/fast-model' +opts.worker_agents = [scout] +session = agent.session('/repo', opts) + +# Delegate exploration to the cheaper worker, then reason on the strong model. +findings = session.task({ + "agent": "scout", + "description": "List public APIs", + "prompt": "List every public API in src/.", +}) +plan = session.run(f'Given these findings, propose a refactor:\n{findings.output}') +print(plan) + +session.close() +``` + + + + +说明: + +- `model` 的取值是一个标识符字符串,由你的运行时解析为智能体文件中声明的某个模型—— + SDK 中没有任何硬编码的模型名称。 +- worker 智能体的 `model` 仅作用于该智能体被委派的工作。会话自身的 + `send`/`run`/`task` 调用仍使用会话的 `model`。 +- 未设置 `model` 的 worker 会继承会话的 `model`,因此你只需为那些换用不同模型确有收益 + 的智能体进行覆盖即可。 + +展示会话 `model` 选项的可运行版本位于 +`crates/code/sdk/node/examples/basic/test_api_alignment.ts`。 diff --git a/website/docs/v6/zh/guide/examples/orchestration.mdx b/website/docs/v6/zh/guide/examples/orchestration.mdx new file mode 100644 index 00000000..17cde394 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/orchestration.mdx @@ -0,0 +1,333 @@ +--- +title: '编排' +description: '用 session.parallel 扇出独立任务,用 session.pipeline 构建按条目执行的多阶段链,用 session.parallelResumable 恢复带日志的运行。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 编排 + +本页展示 A3S Code 中的可编程编排原语:用于扇出的 `session.parallel`、用于按条目执行多阶段链的 `session.pipeline`,以及用于在崩溃后仍可恢复的带日志运行的 `session.parallelResumable`。`parallel` 还接受一个可选的 token 预算,让整个扇出共享同一个账本。当你有多个相互独立的子代理任务时使用 parallel;当你需要让每个输入流经一组有序阶段时使用 pipeline。 + +关于这些原语背后的概念模型,请参阅[编排](/guide/orchestration)。 + +## 用 `session.parallel` 扇出 + +`parallel` 接收一个 `AgentStepSpec` 数组并发执行它们,**按输入顺序**(而非完成顺序)为每个 spec 返回一个 `StepOutcome`。每个 spec 路由到一个具名子代理(`explore`、`plan`、`review`、`verification`、`general` 等)。在 spec 上设置 `outputSchema` / `output_schema` 即可拿到经过 schema 校验的 `structured` 结果。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.', {}); + +// Independent steps; outcomes come back in input order, not completion order. +const outcomes = await session.parallel([ + { + taskId: 'langs', + agent: 'general', + description: 'list', + prompt: 'Name three systems languages.', + maxSteps: 2, + }, + { + taskId: 'safe', + agent: 'general', + description: 'classify', + prompt: 'Is Rust memory-safe without a GC? yes/no.', + maxSteps: 2, + }, +]); + +for (const o of outcomes) { + console.log(`[parallel] ${o.taskId}: success=${o.success}`); +} + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session(".", SessionOptions()) + +# Independent steps; outcomes come back in input order, not completion order. +outcomes = session.parallel([ + { + "task_id": "langs", + "agent": "general", + "description": "list languages", + "prompt": "Name three systems programming languages, comma-separated.", + "max_steps": 2, + }, + { + "task_id": "verdict", + "agent": "general", + "description": "classify", + "prompt": "Is Rust memory-safe without a GC? Answer yes or no.", + "max_steps": 2, + # Schema-validated structured output for this step. + "output_schema": { + "type": "object", + "properties": {"memory_safe": {"type": "boolean"}}, + "required": ["memory_safe"], + }, + }, +]) + +for o in outcomes: + print(f"[parallel] {o['task_id']}: success={o['success']} structured={o.get('structured')}") + +session.close() +``` + + + + +结果在 Python 中是字典(`o['task_id']`、`o['success']`、`o.get('structured')`),在 Node 中是对象(`o.taskId`、`o.success`、`o.structured`)。会话选项 `maxParallelTasks` / `max_parallel_tasks` 限制并发量;多出的 spec 会排队,而返回的结果数组仍然是完整的,且保持顺序。 + +## 用 `session.pipeline` 构建按条目执行的链 + +`pipeline` 接收一个输入 `items` 列表和一个有序的 `stages` 列表。每个条目独立地流经各个阶段——阶段之间**没有屏障**,因此一个较快的条目可以在一个较慢的条目仍处于阶段 1 时就到达阶段 2。阶段回调接收一个 `ctx`:第一个阶段看到 `ctx.item`,后续阶段看到 `ctx.previous`(上一个 `StepOutcome`,你可以基于其 `.output` 继续构建)。返回下一个 spec 以继续,或返回 `null` / `None` 以提前停止该条目的链。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.', {}); + +// Stage 2 builds on stage 1's output. A stage callback MUST NOT throw — +// return null to stop this item's chain. +const results = await session.pipeline( + ['the Rust programming language'], + [ + (ctx) => ({ + taskId: 'sum', + agent: 'general', + description: 'summarize', + prompt: `In one sentence, what is ${ctx.item}?`, + maxSteps: 2, + }), + (ctx) => ({ + taskId: 'cls', + agent: 'general', + description: 'classify', + prompt: `Reply YES or NO: does this describe a programming language?\n\n${ctx.previous.output}`, + maxSteps: 2, + }), + ], +); + +for (const r of results) { + console.log( + `[pipeline] final=${r === null ? null : JSON.stringify(r.output.slice(0, 60))}`, + ); +} + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session(".", SessionOptions()) + +# Each item chains through stages; stage 2 builds on stage 1. Return None from a +# stage (or raise — caught and treated as None) to stop that item's chain. +results = session.pipeline( + ["the Rust programming language"], + [ + lambda ctx: { + "task_id": "summarize", + "agent": "general", + "description": "summarize", + "prompt": f"In one sentence, what is {ctx['item']}?", + "max_steps": 2, + }, + lambda ctx: { + "task_id": "classify", + "agent": "general", + "description": "classify", + "prompt": "Reply with one word YES or NO: does this describe a " + f"programming language?\n\n{ctx['previous']['output']}", + "max_steps": 2, + }, + ], +) + +for r in results: + print(f"[pipeline] final={None if r is None else r['output'][:60]!r}") + +session.close() +``` + + + + +与 `parallel` 的关键区别:阶段是有序且相互依赖的,但各条目在阶段之间**不会**彼此等待。Node 的阶段回调绝不能抛出异常——出错时返回 `null`;Python 的阶段可以抛出(抛出的阶段会被捕获并视为 `None`)。 + +## 用 `session.parallelResumable` 恢复运行 + +`parallelResumable` 就是带日志的 `parallel`。它的第一个参数是 `specs`,第二个参数是稳定的 `workflowId`;每个步骤的结果都会被记录到会话的 store,因此如果进程在运行中途崩溃,你可以用同一个 `workflowId` 再次调用它,已完成的步骤会从日志中重放而不会重新执行。它**需要一个会话 store**——在打开会话时传入 `sessionStore` / `session_store`,否则调用会抛错。 + + + + +```ts +import { Agent, FileSessionStore } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +// parallelResumable journals to the session store; it throws without one. +const session = agent.session('.', { + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); + +// Signature is (specs, workflowId): specs first, stable workflowId second. +const outcomes = await session.parallelResumable( + [ + { + taskId: 'deps', + agent: 'general', + description: 'audit deps', + prompt: 'Check manifests for outdated dependencies.', + maxSteps: 2, + }, + { + taskId: 'tests', + agent: 'verification', + description: 'run tests', + prompt: 'Run the test suite and summarize failures.', + maxSteps: 2, + }, + ], + 'nightly-audit', +); + +// Re-running with the same workflowId replays completed steps from the journal. +console.log(outcomes.map((o) => `${o.taskId}:${o.success}`).join(' ')); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions, FileSessionStore + +agent = Agent.create("agent.acl") +# parallel_resumable journals to the session store; it raises without one. +opts = SessionOptions() +opts.session_store = FileSessionStore("./.a3s/sessions") +session = agent.session(".", opts) + +# Signature is (specs, workflow_id): specs first, stable workflow_id second. +outcomes = session.parallel_resumable( + [ + {"task_id": "deps", "agent": "general", "description": "audit deps", "prompt": "Check manifests for outdated dependencies.", "max_steps": 2}, + {"task_id": "tests", "agent": "verification", "description": "run tests", "prompt": "Run the test suite and summarize failures.", "max_steps": 2}, + ], + "nightly-audit", +) + +# Re-running with the same workflow_id replays completed steps from the journal. +print(" ".join(f"{o['task_id']}:{o['success']}" for o in outcomes)) + +session.close() +``` + + + + +## 用 `session.parallel` 做预算受限的扇出 + +把 token 预算作为第二个参数传入,所有子代理就会汇入**同一个账本**。传入预算时, +`parallel` 解析为 `{ outcomes, budget }`(账本快照)而非原来的结果数组;一旦达到 +上限,之后启动的 step 会被拒绝(`success: false`)。它是软上限——宽扇出可能冲过 +上限几个在飞回合;在飞工作绝不会被强杀。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.', {}); + +const specs = [ + { + taskId: 'a', + agent: 'general', + description: 'q1', + prompt: 'Reply with one word: ready.', + maxSteps: 2, + }, + { + taskId: 'b', + agent: 'general', + description: 'q2', + prompt: 'Reply with one word: go.', + maxSteps: 2, + }, +]; + +// 传入预算时,parallel() 解析为 { outcomes, budget } —— 所有子代理共享一个账本。 +// (不传时,parallel(specs) 返回原来的数组。) +const { outcomes, budget } = await session.parallel(specs, 50_000); +for (const o of outcomes) + console.log(`[budget] ${o.taskId}: success=${o.success}`); +console.log(`spent ${budget.consumedTokens} / ${budget.limitTokens} tokens`); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +session = agent.session(".", SessionOptions()) + +specs = [ + {"task_id": "a", "agent": "general", "description": "q1", "prompt": "Reply with one word: ready.", "max_steps": 2}, + {"task_id": "b", "agent": "general", "description": "q2", "prompt": "Reply with one word: go.", "max_steps": 2}, +] + +# 传入预算时,parallel() 返回 {"outcomes", "budget"}(共享账本)。 +res = session.parallel(specs, budget_tokens=50_000) +for o in res["outcomes"]: + print(f"[budget] {o['task_id']}: success={o['success']}") +print(f"spent {res['budget']['consumed_tokens']} / {res['budget']['limit_tokens']} tokens") + +session.close() +``` + + + + +说明: + +- 三个原语都返回按输入顺序对齐的结果:`{ taskId, success, output, error?, structured? }`(Node 对象)/ `{ "task_id", "success", "output", "error"?, "structured"? }`(Python 字典)。 +- 在 spec 上设置 `outputSchema` / `output_schema` 可在 `structured` 中拿到解析后的结果。 +- `maxSteps` / `max_steps` 限制每个子代理的步数;会话选项 `maxParallelTasks` / `max_parallel_tasks` 限制扇出并发量。 +- 给 `parallel` 传入 token 预算(第 2 个参数 `budgetTokens` / `budget_tokens=`)即可让整个扇出对同一个账本计数;此时返回形状变为 `{ outcomes, budget }`。它是软上限(用量在每次调用之后记账)。 +- Node 的 pipeline 阶段回调绝不能抛出异常——出错时返回 `null`。Python 的阶段可以抛出(抛出的阶段会被捕获并视为 `None`)。 + +可运行版本见 `crates/code/sdk/node/examples/orchestration/parallel-pipeline.mjs` 和 `crates/code/sdk/python/examples/orchestration_workflow.py`。 diff --git a/website/docs/v6/zh/guide/examples/planning.mdx b/website/docs/v6/zh/guide/examples/planning.mdx new file mode 100644 index 00000000..214859cd --- /dev/null +++ b/website/docs/v6/zh/guide/examples/planning.mdx @@ -0,0 +1,70 @@ +--- +title: 'Planning' +description: '使用 planningMode 让 agent 先规划再行动' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Planning + +Planning 模式让 session 在开始调用工具之前先生成一份结构化计划。它适合多步骤工作 +(重构、发布评审、审计),你希望 agent 先把目标拆解清楚,而不是直接动手修改。 + +通过 session 选项 `planningMode`(Node)/ `planning_mode`(Python)进行设置。 +可接受的值为: + +| 值 | 行为 | +| ------------ | ----------------------------------------- | +| `"auto"` | 由 runtime 根据消息内容判断何时值得规划。 | +| `"enabled"` | 对每个请求都强制规划,即使是简单请求。 | +| `"disabled"` | 完全跳过规划,走最低延迟路径。 | + +`"auto"` 是默认的结构化预分析路径。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/repo', { + planningMode: 'auto', +}); + +const result = await session.send( + 'Plan and complete the release-readiness review.', +); +console.log(result.text); +console.log(`${result.toolCallsCount} tool calls executed`); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.planning_mode = "auto" +session = agent.session("/repo", opts) + +result = session.send("Plan and complete the release-readiness review.") +print(result.text) +print(f"{result.tool_calls_count} tool calls executed") + +session.close() +``` + + + + +Planning 状态会挂到 run 上,因此宿主 UI 可以从 run events 渲染任务清单,并在 agent +工作时更新完成情况。Planning 负责组织工作;完成证据仍然来自验证命令。 + +可运行的 session 示例位于 +`crates/code/sdk/node/examples/orchestration/parallel-pipeline.mjs` 和 +`crates/code/sdk/python/examples/orchestration_workflow.py`。 diff --git a/website/docs/v6/zh/guide/examples/prompt-slots.mdx b/website/docs/v6/zh/guide/examples/prompt-slots.mdx new file mode 100644 index 00000000..17695f6f --- /dev/null +++ b/website/docs/v6/zh/guide/examples/prompt-slots.mdx @@ -0,0 +1,156 @@ +--- +title: 'Prompt Slots' +description: '在不覆盖核心行为的前提下,定制智能体的角色设定、准则与回复风格。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Prompt Slots + +Prompt slots 是以声明式方式塑造智能体系统提示词的会话选项。请把它们用于宿主级行为—— +角色设定、编码规范、输出风格——这些内容不应写在每次的用户 prompt 里。这些槽位叠加在 +智能体内置指令之上,因此核心工具行为(读取、写入、运行命令)会被保留。 + +共有四个槽位: + +| 槽位 | 用途 | +| ---------------------------------- | ---------------------------- | +| `role` / `role` | 智能体采用的角色设定。 | +| `guidelines` / `guidelines` | 智能体必须遵循的规范与规则。 | +| `responseStyle` / `response_style` | 智能体回复的格式方式。 | +| `extra` / `extra` | 逐字追加的自由格式指令。 | + +## 基本用法 + +在打开会话时设置任意子集的槽位。它们会应用于该会话的每一轮对话。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +async function main() { + // create() accepts an .acl file path or inline ACL string. + const agent = await Agent.create('agent.acl'); + + const session = agent.session('/repo', { + role: 'release-readiness reviewer', + guidelines: + 'Find blockers before improvements. Require command evidence for done claims.', + responseStyle: 'concise, findings first', + }); + + const result = await session.send('Is this repo ready to ship?'); + console.log(result.text); + + session.close(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +def main(): + # create() accepts an .acl file path or inline ACL string. + agent = Agent.create('agent.acl') + + opts = SessionOptions() + opts.role = 'release-readiness reviewer' + opts.guidelines = 'Find blockers before improvements. Require command evidence for done claims.' + opts.response_style = 'concise, findings first' + session = agent.session('/repo', opts) + + result = session.send('Is this repo ready to ship?') + print(result.text) + + session.close() + +main() +``` + + + + +## 逐个槽位说明 + +这四个槽位彼此独立组合。仅设置角色的会话、带有严格准则的评审者、以及追加自由格式指令的 +会话,使用的都是同一组选项。 + + + + +```ts +// 1. Custom role only. +let session = agent.session(workspace, { + role: 'You are a senior Rust developer who specializes in async programming.', +}); + +// 2. Role + guidelines + response style. +session = agent.session(workspace, { + role: 'You are a Python code reviewer.', + guidelines: 'Always check for type hints. Flag any use of `eval()`.', + responseStyle: 'Reply in bullet points. Be concise.', +}); + +// 3. Extra freeform instructions only. +session = agent.session(workspace, { + extra: "Always end your response with '-- A3S'", +}); + +// Core tool behavior is preserved regardless of the slots. +session = agent.session(workspace, { + role: 'You are a minimalist file manager.', + guidelines: 'Only create files when explicitly asked.', +}); +const result = await session.send( + "Create a file called test.txt with the content 'prompt slots work'. Then read it back.", +); +``` + + + + +```python +# 1. Custom role only. +opts = SessionOptions() +opts.role = 'You are a senior Rust developer who specializes in async programming.' +session = agent.session(workspace, opts) + +# 2. Role + guidelines + response style. +opts = SessionOptions() +opts.role = 'You are a Python code reviewer.' +opts.guidelines = 'Always check for type hints. Flag any use of `eval()`.' +opts.response_style = 'Reply in bullet points. Be concise.' +session = agent.session(workspace, opts) + +# 3. Extra freeform instructions only. +opts = SessionOptions() +opts.extra = "Always end your response with '-- A3S'" +session = agent.session(workspace, opts) + +# Core tool behavior is preserved regardless of the slots. +opts = SessionOptions() +opts.role = 'You are a minimalist file manager.' +opts.guidelines = 'Only create files when explicitly asked.' +session = agent.session(workspace, opts) +result = session.send( + "Create a file called test.txt with the content 'prompt slots work'. Then read it back.", +) +``` + + + + +槽位用于定制角色设定与行为规则;它们不会禁用工具,也不会改变智能体的核心循环。请把 +任务相关的请求保留在 `send` 消息中,而把应在会话每一轮都生效的行为交给这些槽位。 + +可运行版本位于 `crates/code/sdk/node/examples/skills/test_prompt_slots.ts`。 diff --git a/website/docs/v6/zh/guide/examples/quick-start.mdx b/website/docs/v6/zh/guide/examples/quick-start.mdx new file mode 100644 index 00000000..ea1e0d57 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/quick-start.mdx @@ -0,0 +1,60 @@ +--- +title: '快速开始' +description: '创建一个 agent、打开会话、运行一轮对话并读取结果。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 快速开始 + +最小可用的程序:从 ACL 文件创建一个 agent,在项目目录上打开会话,使用 `send` +运行一轮对话,打印回复文本,并查看运行时为该轮生成的验证摘要。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +// Agent.create accepts an .acl file path or inline ACL string. +const agent = await Agent.create('agent.acl'); +const session = agent.session('.'); + +const result = await session.send('List the files in this directory.'); +console.log(result.text); + +// What the runtime checked while producing that turn. +console.log(result.verificationSummaryText); + +session.close(); +``` + + + + +```python +from a3s_code import Agent + +# Agent.create accepts an .acl file path or inline ACL string. +agent = Agent.create('agent.acl') +session = agent.session('.') + +result = session.send('List the files in this directory.') +print(result.text) + +# What the runtime checked while producing that turn. +print(result.verification_summary_text) + +session.close() +``` + + + + +在两个 SDK 中,`Agent.create()` 都接收 `.acl` 文件路径或内联 ACL 源文本。使用完毕后, +请务必调用 `close()` 关闭会话,以便运行时刷新状态并释放资源。 + +## 下一步 + +- [流式输出](/guide/examples/streaming) — 在 token 到达时实时读取 +- [会话](/guide/sessions) — 持久化并恢复对话 diff --git a/website/docs/v6/zh/guide/examples/ripgrep-context.mdx b/website/docs/v6/zh/guide/examples/ripgrep-context.mdx new file mode 100644 index 00000000..0e8cd5b6 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/ripgrep-context.mdx @@ -0,0 +1,74 @@ +--- +title: 'Ripgrep 上下文构建器' +description: '在向 agent 提问前,使用 grep 和 glob 收集代码上下文。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Ripgrep 上下文构建器 + +通过 `session.grep` 和 `session.glob` 进行快速代码搜索,可以收集相关的文件和匹配行, +然后将它们注入到提示中 —— 这是在 agent 开始推理之前的一个轻量级检索步骤。当你希望将 +agent 限定在大型代码库的某个特定切片,而不是让它从头开始探索时,可以使用这种方式。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.'); + +// 1. Find candidate files by glob pattern (returns a list of paths). +const files = await session.glob('src/**/*.ts'); + +// 2. Search the workspace for the symbol we care about (returns ripgrep text). +const hits = await session.grep('createSession'); + +// 3. Build a context string and feed it into a focused prompt. +const context = [ + `Files in scope:\n${files.join('\n')}`, + `Matches for "createSession":\n${hits}`, +].join('\n\n'); + +const answer = await session.run( + `Using only this context, explain how createSession is wired up:\n\n${context}`, +); +console.log(answer); +``` + + + + +```python +from a3s_code import Agent + +agent = Agent.create("agent.acl") +session = agent.session('.') + +# 1. Find candidate files by glob pattern (returns a list of paths). +files = session.glob('src/**/*.ts') + +# 2. Search the workspace for the symbol we care about (returns ripgrep text). +hits = session.grep('createSession') + +# 3. Build a context string and feed it into a focused prompt. +context = '\n\n'.join([ + 'Files in scope:\n' + '\n'.join(files), + 'Matches for "createSession":\n' + hits, +]) + +answer = session.run( + f'Using only this context, explain how createSession is wired up:\n\n{context}' +) +print(answer) +``` + + + + +`glob` 返回匹配的文件路径列表,而 `grep` 则以单个字符串的形式返回原始的 ripgrep 输出。 +两者都在本地运行并能快速返回,因此你可以在花费一次模型调用之前,链式执行多次搜索来低成本地 +组装上下文。当你需要文件的完整内容而不仅仅是匹配行时,可以将它们与 `session.readFile` +搭配使用。 diff --git a/website/docs/v6/zh/guide/examples/security.mdx b/website/docs/v6/zh/guide/examples/security.mdx new file mode 100644 index 00000000..39a37131 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/security.mdx @@ -0,0 +1,198 @@ +--- +title: '安全' +description: '通过权限策略、人工确认流程和安全提供器对特权操作进行管控' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 安全 + +智能体可能产生的每一个副作用——写文件、运行 `bash`、执行 git 推送——都会经过权限策略。 +先从 `ask` 或 `deny` 兜底开始,再列出应被 `allow`(放行)、`deny`(拒绝)或进入 `ask`(询问) +路径的模式。若要引入人工把关,可加上确认策略:`ask` 决策会在 `confirmation_required` +事件处暂停,让你的应用(或人工)对每次调用进行批准或拒绝。只要智能体面向真实仓库运行, +就应使用这套机制。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +const session = agent.session(process.cwd(), { + permissionPolicy: { + allow: ['read(*)', 'grep(*)', 'glob(*)', 'ls(*)', 'bash(git status:*)'], + deny: ['write(**/.env*)', 'bash(rm -rf*)'], + ask: [ + 'write(*)', + 'edit(*)', + 'bash(git push:*)', + 'bash(npm publish:*)', + 'bash(*)', + ], + defaultDecision: 'ask', + }, + // Turn the `ask` patterns into a human-in-the-loop confirmation flow. + confirmationPolicy: { + enabled: true, + defaultTimeoutMs: 120000, + timeoutAction: 'reject', + }, +}); + +// Stream execution and resolve confirmations as they arrive. +const stream = await session.stream('Bump the version and push the release'); +while (true) { + const next = await stream.next(); + if (next.done || !next.value) break; + + const event = next.value; + if (event.type === 'confirmation_required') { + // Look up the pending request for richer display. + const [pending] = await session.pendingConfirmations(); + const toolId = pending?.toolId ?? event.toolId; + console.log(`[confirm] ${pending?.toolName ?? event.toolName}`); + console.log(JSON.stringify(pending?.args ?? {}, null, 2)); + + // In a real app, prompt the user here. + const approved = false; // deny risky operations by default + if (toolId) + await session.confirmToolUse(toolId, approved, 'Reviewed by host'); + } +} + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions, PermissionPolicy, ConfirmationPolicy + + +def main() -> None: + agent = Agent.create("agent.acl") + + opts = SessionOptions() + opts.permission_policy = PermissionPolicy( + allow=["read(*)", "grep(*)", "glob(*)", "ls(*)", "bash(git status:*)"], + deny=["write(**/.env*)", "bash(rm -rf*)"], + ask=["write(*)", "edit(*)", "bash(git push:*)", "bash(npm publish:*)", "bash(*)"], + default_decision="ask", + ) + # Turn the `ask` patterns into a human-in-the-loop confirmation flow. + opts.confirmation_policy = ConfirmationPolicy( + enabled=True, + default_timeout_ms=120_000, + timeout_action="reject", + ) + + session = agent.session(".", opts) + + # Stream execution and resolve confirmations as they arrive. + for event in session.stream("Bump the version and push the release"): + if event.event_type == "confirmation_required": + # Look up the pending request for richer display. + pending = session.pending_confirmations() + first = pending[0] if pending else {} + tool_id = first.get("tool_id") or event.tool_id + print(f"[confirm] {first.get('tool_name') or event.tool_name}") + + # In a real app, prompt the user here. + approved = False # deny risky operations by default + if tool_id: + session.confirm_tool_use(tool_id, approved, "Reviewed by host") + + session.close() + + +if __name__ == "__main__": + main() +``` + + + + +## 添加安全提供器 + +`DefaultSecurityProvider` 会启用输入污点追踪和输出净化,独立于权限策略对工具的输入输出 +进行筛查。通过 `securityProvider`(Node)/ `security_provider`(Python)传入;省略则完全 +关闭安全功能。 + + + + +```ts +import { Agent, DefaultSecurityProvider } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +const session = agent.session(process.cwd(), { + securityProvider: new DefaultSecurityProvider(), + permissionPolicy: { + allow: ['bash(echo:*)'], + ask: ['bash(*)'], + defaultDecision: 'ask', + }, +}); + +// Privileged host operations run through the provider + policy. +const out = await session.bash('echo "screened by the security provider"'); +console.log(out); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions, PermissionPolicy, DefaultSecurityProvider + + +def main() -> None: + agent = Agent.create("agent.acl") + + opts = SessionOptions() + opts.security_provider = DefaultSecurityProvider() + opts.permission_policy = PermissionPolicy( + allow=["bash(echo:*)"], + ask=["bash(*)"], + default_decision="ask", + ) + + session = agent.session(".", opts) + + # Privileged host operations run through the provider + policy. + out = session.bash('echo "screened by the security provider"') + print(out) + + session.close() + + +if __name__ == "__main__": + main() +``` + + + + +## 说明 + +- `defaultDecision` 是所有未被 `allow` / `deny` / `ask` 匹配到的模式的兜底决策(取值为 + `allow`、`deny` 或 `ask` 之一)。真实仓库优先使用 `ask`,仅对自动化确实需要的部分逐步放开。 +- 设置 `enabled: true` 的 `confirmationPolicy` 才会把 `ask` 决策变成会暂停的 + `confirmation_required` 事件。通过 `session.confirmToolUse(toolId, approved, reason?)` + 逐个处理;若在 `defaultTimeoutMs` 内未收到答复,则由 `timeoutAction`(`reject`)决定结果。 +- 除非最终步骤由受控自动化负责,否则 release 和 publish 操作(`bash(git push*)`、 + `bash(npm publish*)`)应保持在 `ask` 或 `deny` 路径上。 +- `session.tool()`、`session.bash()`、`session.git()` 这类宿主直接调用都是特权操作。 + 它们由你的应用代码发起,应在调用 SDK 前先由宿主授权;上面的 permission policy 管控的是 + `send`、`run` 和 `stream` 内由模型选择的工具调用。 + +可运行的确认循环示例位于 +`crates/code/sdk/node/examples/streaming/hitl_confirmation_loop.ts` 和 +`crates/code/sdk/python/examples/hitl_confirmation_loop.py`。 diff --git a/website/docs/v6/zh/guide/examples/skill-tool.mdx b/website/docs/v6/zh/guide/examples/skill-tool.mdx new file mode 100644 index 00000000..ee2acf5c --- /dev/null +++ b/website/docs/v6/zh/guide/examples/skill-tool.mdx @@ -0,0 +1,93 @@ +--- +title: 'Skill 工具' +description: '将已注册的 skill 作为可调用工具来调用' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Skill 工具 + +skills 以两个核心工具的形式暴露给模型:`search_skills`(按意图查找 skill)和 +`Skill`(按名称调用 skill)。tool 类型的 skill 会运行其处理器;instruction 类型的 +skill 会返回其正文供模型应用。你既可以让模型在一次运行中调用这些工具,也可以通过 +`session.tool('Skill', ...)` 直接从 SDK 调用某个 skill。 + +通过 `skillDirs` / `skill_dirs` 注册 skill 目录(包含若干 `SKILL.md` 文件的文件夹), +或通过 session options 传入 inline skills。A3S Code 不再内置默认 skills, +`builtinSkills` / `builtin_skills` 只是兼容标记。注册后的 skills 会通过 +`Skill` 与 `search_skills` 可见。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session(process.cwd(), { + skillDirs: ['./skills'], // a folder of SKILL.md files +}); + +// Skill and search_skills are core tools — confirm they're on the surface. +console.log(session.toolNames()); + +// Option A: let the model search for and apply a skill during a run. +const run = await session.run( + 'Search available skills, then apply the most relevant one.', +); +console.log(run.text); + +// Option B: invoke a skill directly as a callable tool. +// Canonical args: { skill_name, prompt? }. +const result = await session.tool('Skill', { + skill_name: 'release-review', + prompt: 'Review this release patch for blockers and verification gaps.', +}); +if (result.exitCode !== 0) { + throw new Error(result.output); +} +console.log(result.output); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.skill_dirs = ['./skills'] # a folder of SKILL.md files +session = agent.session('.', opts) + +# Skill and search_skills are core tools — confirm they're on the surface. +print(session.tool_names()) + +# Option A: let the model search for and apply a skill during a run. +run = session.run('Search available skills, then apply the most relevant one.') +print(run.text) + +# Option B: invoke a skill directly as a callable tool. +# Canonical args: { skill_name, prompt? }. +result = session.tool('Skill', { + 'skill_name': 'release-review', + 'prompt': 'Review this release patch for blockers and verification gaps.', +}) +if result.exit_code != 0: + raise RuntimeError(result.output) +print(result.output) + +session.close() +``` + + + + +`SKILL.md` 在 frontmatter 中声明其 `kind`(`tool`、`instruction` 或 `agent`)。对于 +tool 类型的 skill,`Skill` 工具会运行该 skill 的处理器并返回其输出;对于 instruction +类型的 skill,则返回正文供模型应用。skill 管理由 SDK 注册、skill 目录或项目文件完成, +而不是通过模型可见的管理工具处理。 + +可运行版本位于 `crates/code/sdk/node/examples/skills/test_custom_skills_agents.ts`。 diff --git a/website/docs/v6/zh/guide/examples/skills.mdx b/website/docs/v6/zh/guide/examples/skills.mdx new file mode 100644 index 00000000..f521e206 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/skills.mdx @@ -0,0 +1,93 @@ +--- +title: '技能与自定义智能体' +description: '从文件系统约定加载项目技能和自定义子智能体。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 技能与自定义智能体 + +A3S Code 不再内置默认 skills。请从磁盘上的目录加载自己的技能和子智能体来扩展会话。 +使用 `skillDirs` 加载 Markdown skills,使用 `agentDirs` 加载 worker/subagent 定义。 +`registerAgentDir` 可以在 session 创建后继续追加 agent 定义目录;skill 目录在 +session 创建时加载。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); + +// 显式加载项目 skills 和 agents。 +const session = agent.session('/path/to/project', { + skillDirs: ['./.a3s/skills'], + agentDirs: ['./.a3s/agents'], +}); + +// session 创建后可以继续注册 agent 定义目录。 +session.registerAgentDir('./team/shared-agents'); + +// Inspect what the session loaded. +console.log('Tools:', session.toolNames()); +console.log('Commands:', session.listCommands()); + +// The agent now has access to project skills. +const result = await session.run( + 'Use the project conventions skill to scaffold a new module.', +); +console.log(result.text); + +session.close(); +``` + + + + +```python +from a3s_code import Agent, SessionOptions + +agent = Agent.create("agent.acl") + +# 显式加载项目 skills 和 agents。 +opts = SessionOptions() +opts.skill_dirs = ['./.a3s/skills'] +opts.agent_dirs = ['./.a3s/agents'] + +session = agent.session('/path/to/project', opts) + +# session 创建后可以继续注册 agent 定义目录。 +session.register_agent_dir('./team/shared-agents') + +# Inspect what the session loaded. +print('Tools:', session.tool_names()) +print('Commands:', session.list_commands()) + +# The agent now has access to project skills. +result = session.run( + 'Use the project conventions skill to scaffold a new module.', +) +print(result.text) + +session.close() +``` + + + + +## Skill registry 行为 + +A3S Code 不再内置默认 skills。默认 effective skill registry 为空。 +`builtinSkills: true` / `builtin_skills = True` 会被接受以保持兼容,但当前不会 +添加任何默认 skill。请通过 `skillDirs` / `skill_dirs`、inline skills 或显式 +`SkillRegistry` 加载需要的技能。 + +日常项目里,把可复用行为放进 `.a3s/skills` 或配置的 skill 目录。 + +从 `agentDirs` 加载的自定义子智能体可以在 +[`session.parallel(...)`](/guide/examples/orchestration) 和 +[`session.pipeline(...)`](/guide/examples/orchestration) 中按名称引用, +与内置注册表中的智能体(`explore`、`plan`、`general`、`verification`、`review`)一起使用。 + +可运行版本位于 `crates/code/sdk/node/examples/skills/test_custom_skills_agents.ts`。 diff --git a/website/docs/v6/zh/guide/examples/streaming.mdx b/website/docs/v6/zh/guide/examples/streaming.mdx new file mode 100644 index 00000000..7158eb47 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/streaming.mdx @@ -0,0 +1,99 @@ +--- +title: '流式输出' +description: '在回合运行时读取增量 AgentEvent' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 流式输出 + +`session.stream(prompt)` 会在回合运行过程中逐步产出事件,因此你可以在文本到达时即时渲染,并实时响应工具活动。当你需要实时 UI,或希望 CLI 逐 token 打印输出(而不是等待 `send` 或 `run` 返回完整结果)时,请使用它。 + +每个事件都带有稳定的 `version`、`type`、`payload` 和可选 `metadata` 信封字段。常见类型包括 `agent_start`、`text_delta` / `reasoning_delta`、`tool_start` / `tool_end`、`agent_end` 和 `error`。验证信息位于 `agent_end` 的 payload 与便捷字段中。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session(process.cwd(), { planningMode: 'disabled' }); + +const stream = await session.stream( + 'Use the bash tool to run the tests, then summarize the result.', +); + +while (true) { + const next = await stream.next(); + if (next.done || !next.value) break; + + const event = next.value; + if (event.type === 'text_delta' && event.text) { + process.stdout.write(event.text); + } else if (event.type === 'tool_start') { + console.log(`\n[tool:start] ${event.toolName ?? 'unknown'}`); + } else if (event.type === 'tool_end') { + console.log( + `\n[tool:end] ${event.toolName ?? 'unknown'} exit=${event.exitCode ?? 0}`, + ); + } else if (event.type === 'agent_end') { + console.log(`\n[verification] ${event.verificationSummaryText ?? ''}`); + } else if (event.type === 'error') { + throw new Error(event.error ?? 'stream error'); + } +} + +console.log('\n[stream] complete'); +session.close(); +``` + + + + +```python +import os + +from a3s_code import Agent, SessionOptions + + +def main() -> None: + agent = Agent.create("agent.acl") + + opts = SessionOptions() + opts.planning_mode = "disabled" + session = agent.session(".", opts) + + prompt = "Use the bash tool to run the tests, then summarize the result." + + try: + for event in session.stream(prompt): + if event.type == "text_delta" and event.text: + print(event.text, end="", flush=True) + elif event.type == "tool_start": + print(f"\n[tool:start] {event.tool_name or 'unknown'}") + elif event.type == "tool_end": + print(f"\n[tool:end] {event.tool_name or 'unknown'} exit={event.exit_code or 0}") + elif event.type == "agent_end": + print(f"\n[verification] {event.verification_summary_text or ''}") + elif event.type == "error": + raise RuntimeError(event.error or "stream error") + print("\n[stream] complete") + finally: + session.close() + + +if __name__ == "__main__": + main() +``` + + + + +说明: + +- Node 端使用 `stream.next()` 手动迭代流,检查 `next.done` 与 `next.value`。在当前构建中,Python SDK 将流式输出暴露为同步迭代器,因此你可以用普通的 `for` 循环来消费它(而 `parallel`、`pipeline` 等编排 API 仍为 `async`)。 +- 两个 SDK 都公开规范的 `event.type`;Python 保留 `event.event_type` 作为兼容别名。`event.payload` 是完整、无损的 payload,未来未知事件类型也会原样保留。便捷字段遵循各语言的大小写约定:Node 中为 `toolName` / `exitCode` / `verificationSummaryText`,Python 中为 `tool_name` / `exit_code` / `verification_summary_text`。 +- 当启用确认策略时,流式事件还可能包含人工介入(human-in-the-loop)确认信号(`confirmation_required`、`confirmation_received`、`confirmation_timeout`)。 + +可运行的流式示例位于 `crates/code/sdk/node/examples/streaming/` 目录下。一个完整的人工介入确认循环示例位于 `crates/code/sdk/node/examples/streaming/hitl_confirmation_loop.ts`,对应的 Python 版本位于 `crates/code/sdk/python/examples/hitl_confirmation_loop.py`。 diff --git a/website/docs/v6/zh/guide/examples/structured-output.mdx b/website/docs/v6/zh/guide/examples/structured-output.mdx new file mode 100644 index 00000000..716a43d3 --- /dev/null +++ b/website/docs/v6/zh/guide/examples/structured-output.mdx @@ -0,0 +1,284 @@ +--- +title: '结构化输出' +description: '使用 generate_object 工具生成符合 Schema 校验的 JSON 对象。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 结构化输出 + +内置的 `generate_object` 工具会让配置的 LLM 生成 JSON 对象,对响应执行你提供的 +JSON Schema 校验,并且只在零退出码结果中返回校验后的对象。当你需要机器可读的结果时 +使用它:抽取、分类、配置生成,或者作为另一个程序的输入。 + +你可以通过 `session.tool('generate_object', ...)` 直接调用它。工具结果会把校验后的对象以 JSON 形式放在 `result.output` 上——解析它并读取 `object` 字段。同一个工具也支持 agent 自主调用,即模型在 `send` 过程中自行决定调用它。 + +## 直接工具调用 + +最简单的方式:直接调用 `generate_object`,先检查工具退出码,再从结果中解析出校验后的对象。 + + + + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('.'); + +const result = await session.tool('generate_object', { + schema: { + type: 'object', + required: ['name', 'age', 'skills'], + properties: { + name: { type: 'string' }, + age: { type: 'integer', minimum: 0 }, + skills: { + type: 'array', + items: { type: 'string' }, + minItems: 1, + }, + }, + }, + prompt: 'Extract: "Alice is 28, skilled in Rust, TypeScript, and Python."', + schema_name: 'developer', + mode: 'tool', +}); + +if (result.exitCode !== 0) { + throw new Error(result.output); +} + +const { object } = JSON.parse(result.output); +console.log(object); +// { name: "Alice", age: 28, skills: ["Rust", "TypeScript", "Python"] } + +session.close(); +``` + + + + +```python +import json +from a3s_code import Agent + +agent = Agent.create('agent.acl') +session = agent.session('.') + +result = session.tool("generate_object", { + "schema": { + "type": "object", + "required": ["name", "age", "skills"], + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer", "minimum": 0}, + "skills": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1, + }, + }, + }, + "prompt": 'Extract: "Alice is 28, skilled in Rust, TypeScript, and Python."', + "schema_name": "developer", + "mode": "tool", +}) + +if result.exit_code != 0: + raise RuntimeError(result.output) + +obj = json.loads(result.output)["object"] +print(obj) +# {"name": "Alice", "age": 28, "skills": ["Rust", "TypeScript", "Python"]} + +session.close() +``` + + + + +校验后的值位于解析输出的 `object` 键上。当 `result.exitCode`(Node)/ +`result.exit_code`(Python)为零时,`required` 中声明的字段已经通过运行时校验。 +如果模型在修复重试后仍无法满足 schema,工具会报告非零退出码。 + +## 枚举分类 + +用 `enum` 把字段约束到一个固定集合。这会把自由文本分类变成带 schema 闸门的结果。 + + + + +```ts +const result = await session.tool('generate_object', { + schema: { + type: 'object', + required: ['sentiment', 'confidence'], + properties: { + sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] }, + confidence: { type: 'number', minimum: 0, maximum: 1 }, + }, + }, + prompt: 'Classify sentiment: "This is the worst product I have ever used."', + schema_name: 'sentiment', +}); + +const { object } = JSON.parse(result.output); +console.log(object.sentiment, object.confidence); // "negative" 0.97 +``` + + + + +```python +result = session.tool("generate_object", { + "schema": { + "type": "object", + "required": ["sentiment", "confidence"], + "properties": { + "sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]}, + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, + }, + }, + "prompt": 'Classify sentiment: "This is the worst product I have ever used."', + "schema_name": "sentiment", +}) + +obj = json.loads(result.output)["object"] +print(obj["sentiment"], obj["confidence"]) # "negative" 0.97 +``` + + + + +## 嵌套 schema 与数组 + +Schema 可以任意深度地嵌套对象和数组,运行时会校验整个结构。这能在一次调用中建模真实的配置文件、清单或 API 载荷。 + + + + +```ts +const result = await session.tool('generate_object', { + schema: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + minItems: 3, + maxItems: 5, + items: { + type: 'object', + required: ['name', 'category'], + properties: { + name: { type: 'string' }, + category: { type: 'string', enum: ['fruit', 'vegetable', 'grain'] }, + }, + }, + }, + }, + }, + prompt: 'List 3 food items with their categories.', + schema_name: 'food_list', +}); + +const { items } = JSON.parse(result.output).object; +console.log( + items.length, + items.map((i) => i.name), +); +``` + + + + +```python +result = session.tool("generate_object", { + "schema": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "minItems": 3, + "maxItems": 5, + "items": { + "type": "object", + "required": ["name", "category"], + "properties": { + "name": {"type": "string"}, + "category": {"type": "string", "enum": ["fruit", "vegetable", "grain"]}, + }, + }, + }, + }, + }, + "prompt": "List 3 food items with their categories.", + "schema_name": "food_list", +}) + +items = json.loads(result.output)["object"]["items"] +print(len(items), [i["name"] for i in items]) +``` + + + + +## Agent 自主调用 + +你也可以让 agent 自行决定何时使用结构化输出。让它在 `send` 过程中调用 `generate_object`;它会先收集上下文,再输出对象。 + + + + +```ts +const result = await session.send( + 'Use the generate_object tool to extract the following into an object ' + + 'with fields "title" (string), "year" (integer), "genre" (string): ' + + 'The movie "Inception" was released in 2010 and is a sci-fi thriller.', +); + +console.log( + `tool calls: ${result.toolCallsCount}, tokens: ${result.totalTokens}`, +); +``` + + + + +```python +result = session.send( + 'Use the generate_object tool to produce a JSON object with schema ' + '{"type":"object","required":["language","paradigm"],"properties":' + '{"language":{"type":"string"},"paradigm":{"type":"string"}}} ' + 'for: "Rust is a systems programming language with a focus on safety."' +) + +print(f"tool calls: {result.tool_calls_count}, tokens: {result.total_tokens}") +``` + + + + +## Schema 校验覆盖 + +内置校验器支持: + +- `type`(包括 nullable 数组如 `["string", "null"]`) +- `required`、`properties`、`additionalProperties` +- `enum`、`const` +- `anyOf`、`oneOf` +- `minLength`、`maxLength`、`pattern` +- `minimum`、`maximum`、`exclusiveMinimum`、`exclusiveMaximum` +- `minItems`、`maxItems`、`items` +- 嵌套对象和数组校验 + +## 说明 + +- 校验后的值位于解析后 `result.output` 的 `object` 键上。当前跨 provider 默认路径传 `mode: 'tool'`,仅 prompt 的回退方式传 `mode: 'prompt'`。`auto`、`strict` 和 `json` 在当前运行路径中都会解析为 `tool`。 +- 把你依赖的每个字段都列入 `required`——运行时会强制执行,因此缺失或类型错误的字段会导致校验失败,而不是悄悄返回部分数据。 +- `generate_object` 是独立注册的内置工具,不依赖 built-in skills。 +- 直接 `session.tool(...)` 调用是宿主控制面调用。允许模型在 `send` / `run` / `stream` 中自行选择工具时使用 `permissionPolicy`;直接 SDK 调用前应使用宿主自己的授权逻辑。 + +可运行版本随源码提供,位于 `crates/code/sdk/node/examples/basic/test_generate_object.ts` 和 `crates/code/sdk/python/examples/test_generate_object.py`。 diff --git a/website/docs/v6/zh/guide/filesystem-agents.mdx b/website/docs/v6/zh/guide/filesystem-agents.mdx new file mode 100644 index 00000000..285c8c58 --- /dev/null +++ b/website/docs/v6/zh/guide/filesystem-agents.mdx @@ -0,0 +1,72 @@ +--- +title: 'agents/ 角色目录' +description: '用 .a3s/agents 定义可被 task、parallel_task 和自动委派调用的 worker agents。' +--- + +# agents/ 角色目录 + +`agents/` 目录存放 worker/subagent 定义。推荐使用 `.a3s/agents/` 作为 A3S 原生位置;迁移项目可以继续读取 `.claude/agents/`,但新文档和新项目应优先使用 `.a3s/agents/`。 + +```text +repo/ +└── .a3s/ + └── agents/ + ├── explorer.md + ├── security-reviewer.md + └── verification-runner.md +``` + +这些文件不是主 Agent 目录。它们会被 `task`、`parallel_task`、`session.task(...)`、`session.tasks(...)` 或 `autoDelegation` 调用,父 session 仍负责最终汇总、验证和权限边界。 + +## Agent 文件格式 + +```md +--- +name: security-reviewer +description: Use for permission, secret, and external side-effect review +tools: Read, Grep, Glob, Bash(rg *) +disallowedTools: + - Write + - Bash(git push *) +--- + +Review security risks first. Return blockers, evidence paths, and required verification. +``` + +`name` 是调用名,`description` 决定自动委派时是否匹配,正文是该 worker 的角色说明。工具字段收窄 worker 可见能力;不要依赖 worker 自己“承诺不做危险事”。 + +## 手动委派 + +```ts +const session = agent.session('/repo', { + agentDirs: ['./.a3s/agents'], + maxParallelTasks: 4, +}); + +await session.task({ + agent: 'security-reviewer', + description: 'Review release side effects', + prompt: 'Check changed auth, permission, and external API paths.', +}); +``` + +固定流程更适合手动委派或可编程编排;自动委派适合“父 Agent 读到目标后自行选择专家”的场景。 + +## 自动委派 + +```ts +const session = agent.session('/repo', { + agentDirs: ['./.a3s/agents'], + autoDelegation: { enabled: true, minConfidence: 0.72, maxTasks: 4 }, +}); +``` + +自动委派依赖 agent 描述和置信度评分。写 description 时要说清楚“何时使用”,而不是只写角色口号。 + +## 最佳实践 + +- 一个文件只做一个角色,避免“万能 reviewer”。 +- description 面向路由,正文面向执行。 +- worker 输出应包含证据、风险和建议下一步,方便父 session 汇总。 +- 高权限动作留给父 session 或显式工具,不要让子 Agent 默认获得发布、删除、推送权限。 +- 需要动态创建的一次性 worker,用 `workerAgents` 或 `registerWorkerAgent()`,不用落盘。 diff --git a/website/docs/v6/zh/guide/filesystem-config.mdx b/website/docs/v6/zh/guide/filesystem-config.mdx new file mode 100644 index 00000000..d218c3ac --- /dev/null +++ b/website/docs/v6/zh/guide/filesystem-config.mdx @@ -0,0 +1,90 @@ +--- +title: 'agent.acl' +description: '模型、provider、队列、技能目录和 worker agent 目录的文件化运行配置。' +--- + +# agent.acl + +`agent.acl` 是文件系统优先形态里的运行配置入口。它负责把模型、provider、并行度、队列、存储、skill 目录和 worker agent 目录固化为可版本化配置。 + +SDK 宿主可以用 `Agent.create("agent.acl")` 显式传入任意 `.acl` 文件。 +`a3s code` TUI 会从 workspace 向上发现 `.a3s/config.acl`,然后读取 +`~/.a3s/config.acl`;它并不要求仓库根部存在 `agent.acl`。AgentDir 内的 +`agent.acl` 服务于该目录的长期 Agent。三者格式一致,但发现方式和作用范围不同。 + +## 基础配置 + +```text +default_model = "provider/model-id" +max_parallel_tasks = 4 +auto_parallel = false + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "model-id" { + tool_call = true + limit = { + context = 128000 + output = 4096 + } + } +} +``` + +`apiKey` / `api_key` 和 `baseUrl` / `base_url` 都是可接受别名。运行时不硬编码 +模型名;`default_model` 和 session 级 `model` 覆盖必须匹配这里声明的 +`provider/model-id`。 + +把 token 通过环境变量注入,不要写进 `agent.acl`。配置文件进入仓库后,它就是产品行为的一部分,应该像代码一样审查。 + +## 目录发现 + +```text +skill_dirs = ["./.a3s/skills"] +agent_dirs = ["./.a3s/agents"] + +auto_delegation { + enabled = true + min_confidence = 0.72 + max_tasks = 4 + auto_parallel = false +} +``` + +`skill_dirs` 指向可复用技能目录;`agent_dirs` 指向 worker/subagent 定义目录。自动委派只决定是否让模型选择 worker,不会取消父 session 的权限策略、工具可见性或验证要求。 + +## Session 存储 + +```text +storage_backend = "file" +sessions_dir = ".a3s/sessions" +``` + +当 session 没有显式收到 SDK `sessionStore` 时,`sessions_dir` 是本地文件型 +session persistence 路径。`storage_backend = "memory"` 表示 session 是临时的。 +`storage_url` 会被解析为自定义存储元数据,但它本身不会创建本地 +`FileSessionStore`。 + +## AgentDir 中的配置 + +AgentDir 的 `agent.acl` 可以省略;省略时使用默认配置。存在时,`AgentDir::load` 会把它解析成 `CodeConfig`,并与 `instructions.md`、`skills/`、`tools/`、`schedules/` 一起合成长期 Agent。 + +```text +release-agent/ +├── instructions.md +├── agent.acl +├── skills/ +├── tools/ +└── schedules/ +``` + +适合放在 AgentDir `agent.acl` 的内容包括该 Agent 默认模型、provider、运行限制、队列策略和私有 skill 目录。不要在这里硬编码部署环境差异;用环境变量或宿主注入区分开发、测试和生产。 + +## 配置边界 + +- 配置决定“可以连接什么”和“默认如何运行”,不决定“模型能越过权限门”。 +- 目录路径应相对 workspace 或 AgentDir,避免依赖个人机器绝对路径。 +- 自动委派需要和 `agents/` 的描述质量一起调优;低质量 description 会让运行时错误分派。 +- 高风险工具即使被目录发现,也应继续走 HITL 或 allow-list。 diff --git a/website/docs/v6/zh/guide/filesystem-first.mdx b/website/docs/v6/zh/guide/filesystem-first.mdx new file mode 100644 index 00000000..ce88c43f --- /dev/null +++ b/website/docs/v6/zh/guide/filesystem-first.mdx @@ -0,0 +1,68 @@ +--- +title: '文件系统优先' +description: '把角色、配置、技能、工具、调度和团队定义沉淀为可审查、可版本化的文件系统约定。' +--- + +# 文件系统优先 + +A3S Code 把文件系统当作 Agent 产品的第一层接口:能长期保存的角色、工具、技能、调度和团队定义都先落成文件,再由运行时按约定加载。这样做的目标不是减少配置项,而是把“Agent 如何工作”变成可以审查、版本化、复用和迁移的工程资产。 + +文件系统优先有两种常用形态: + +```text +repo/ +├── AGENTS.md # 项目长期指令 +├── agent.acl # 模型、provider、队列与委派配置 +└── .a3s/ + ├── agents/ # 可被 task / autoDelegation 调用的 worker agents + └── skills/ # 可复用技能 + +release-agent/ +├── instructions.md # 独立长期 Agent 的角色 slot +├── agent.acl # 该 Agent 的运行配置 +├── skills/ # 该 Agent 私有技能 +├── tools/ # 该 Agent 可声明的 MCP / script tools +└── schedules/ # 周期性 turn +``` + +第一种是仓库工作区约定,适合交互式开发、团队委派和项目知识注入。第二种是 AgentDir 约定,适合长期运行、定时触发和目录级工具声明。 + +## 路径总览 + +| 路径 | 作用 | 什么时候用 | +| ------------------------- | -------------------------------------------- | ---------------------------------------------------------- | +| `AGENTS.md` | 给当前 workspace 注入稳定项目指令 | 代码风格、验证命令、安全边界和发布流程需要长期生效。 | +| `instructions.md` | 定义一个 AgentDir 主 Agent 的角色 slot | 需要把单个目录加载成可长期运行的 Agent。 | +| `agent.acl` | 配置模型、provider、队列、技能目录和委派策略 | 运行时策略需要随仓库或 Agent 一起版本化。 | +| `.a3s/agents/` | 存放 worker/subagent 定义 | 父 agent 需要 `task`、`parallel_task` 或自动委派。 | +| `.a3s/skills/`、`skills/` | 存放可复用技能 | 多个任务共享一组操作准则、检查清单或领域流程。 | +| `tools/` | 声明 AgentDir 的 MCP 或 script tools | 需要把连接器或受限脚本作为模型可见能力暴露给调度 session。 | +| `schedules/` | 声明周期性 turn | 需要日报、巡检、同步、回归检查等定时 Agent。 | + +这些约定不是新的 prompt 系统。`AGENTS.md`、`instructions.md` 和 skills 都会进入 A3S Code 的上下文组合流程;工具可见性、权限门、HITL、响应契约和验证仍由 harness 控制。 + +## 加载顺序 + +一次典型 session 会先解析 `agent.acl`,再绑定 workspace,随后加载项目指令、skills、agent definitions、direct tools、MCP 连接和运行时策略。AgentDir 的 `serve_agent_dir` 会先用 `instructions.md`、本地 `agent.acl`、`skills/`、`tools/` 和 `schedules/` 合成配置,再为每个 schedule 创建独立 session。 + +路径越靠近具体 Agent,语义越局部:仓库根部的 `AGENTS.md` 描述整个项目,`.a3s/agents/*.md` 描述某个 worker,AgentDir 内的 `instructions.md` 描述该目录的主 Agent。不要把全局规则复制到每个文件里,除非确实需要覆盖上下文边界。 + +## 设计原则 + +- 约定只负责发现和组装,不负责绕过安全。高权限工具仍需要权限策略和确认门。 +- 文件应可被 code review。模型、provider、工具、调度和角色变更都应该能在 diff 中看清楚。 +- secrets 不进入仓库。用环境变量、宿主连接或密钥管理系统注入。 +- 一次性实验可以用 SDK 参数;需要复用、审计或迁移时再固化成文件。 +- `AgentDir` 是主 Agent 目录;`.a3s/agents/` 是 worker/subagent 定义目录,两者不要混用。 + +## 阅读顺序 + +1. [约定大于配置](/guide/convention-over-configuration) +2. [AGENTS.md](/guide/agents-md) +3. [instructions.md](/guide/filesystem-instructions) +4. [agent.acl](/guide/filesystem-config) +5. [Agent 目录](/guide/agent-dir) +6. [agents/ 角色目录](/guide/filesystem-agents) +7. [skills/ 技能目录](/guide/filesystem-skills) +8. [tools/ 工具目录](/guide/filesystem-tools) +9. [schedules/ 调度目录](/guide/filesystem-schedules) diff --git a/website/docs/v6/zh/guide/filesystem-instructions.mdx b/website/docs/v6/zh/guide/filesystem-instructions.mdx new file mode 100644 index 00000000..ed6390e0 --- /dev/null +++ b/website/docs/v6/zh/guide/filesystem-instructions.mdx @@ -0,0 +1,50 @@ +--- +title: 'instructions.md' +description: 'AgentDir 主 Agent 的角色 slot,以及它和 AGENTS.md 的边界。' +--- + +# instructions.md + +`instructions.md` 是 AgentDir 主 Agent 的角色文件。它是唯一必需文件,内容会作为 `SystemPromptSlots.role` 注入到每个调度 session 中。 + +它和 `AGENTS.md` 的边界不同: + +| 文件 | 范围 | 适合内容 | +| ----------------- | ---------------------- | -------------------------------------------- | +| `AGENTS.md` | workspace 或子目录 | 项目规则、验证命令、代码风格、安全要求。 | +| `instructions.md` | 一个 AgentDir 主 Agent | 角色身份、工作目标、输出偏好、长期任务边界。 | + +`instructions.md` 是纯 Markdown,不需要 frontmatter。它不会覆盖 harness 的 `BOUNDARIES`、响应格式、工具权限或验证要求。 + +## 推荐写法 + +```md +You are a release-readiness agent for this repository. + +Responsibilities: + +- Track release blockers and risky changes. +- Separate shipped changes from follow-up work. +- Never invent CI status, versions, or owners. + +Output: + +- Start with blockers. +- Include evidence paths. +- End with required verification commands. +``` + +保持短、稳定、可审查。把项目级命令放进 `AGENTS.md`,把可复用流程放进 `skills/`,把周期触发目标放进 `schedules/*.md`。`instructions.md` 只回答“这个长期 Agent 是谁,以及它默认如何工作”。 + +## 会被哪里使用 + +`serve_agent_dir` 会在启动时读取 `instructions.md`,并把它应用到每个已启用 schedule 的 session。配合 `SessionStore` 恢复历史时,历史上下文来自 store,但当前的 `instructions.md` 会重新加载,因此修改角色文件会在下一次重启后生效。 + +交互式 session 如果只需要临时角色,可以直接使用 SDK 的 prompt slots;当角色需要随仓库保存、接受 review 或被多个调度复用时,再把它固化到 `instructions.md`。 + +## 不要放什么 + +- 不要放密钥、token、私有 endpoint 凭据。 +- 不要写“忽略安全规则”或“自动批准所有高风险动作”。 +- 不要复制大型项目手册;用链接和 skills 组织细节。 +- 不要把 schedule prompt 写在这里;每个周期性任务应放在 `schedules/*.md`。 diff --git a/website/docs/v6/zh/guide/filesystem-schedules.mdx b/website/docs/v6/zh/guide/filesystem-schedules.mdx new file mode 100644 index 00000000..93ed58e5 --- /dev/null +++ b/website/docs/v6/zh/guide/filesystem-schedules.mdx @@ -0,0 +1,74 @@ +--- +title: 'schedules/ 调度目录' +description: '用 Markdown schedule 文件声明 cron turn、独立 session 与可恢复上下文。' +--- + +# schedules/ 调度目录 + +`schedules/` 为 AgentDir 声明周期性 turn。每个 Markdown 文件是一条 schedule:frontmatter 描述 cron 元数据,正文是每次触发时发送给 Agent 的 prompt。 + +```text +release-agent/ +└── schedules/ + ├── daily.md + └── weekly-risk-review.md +``` + +## Schedule 文件 + +```md +--- +cron: '0 9 * * *' +name: daily-release-check +enabled: true +--- + +Summarize merged changes since the last run, inspect release risks, +and report only blockers plus required verification. +``` + +| 字段 | 必填 | 默认值 | 说明 | +| --------- | ---- | ----------- | ----------------------------------- | +| `cron` | 是 | - | 5 字段或 6 字段 cron 表达式。 | +| `name` | 否 | 文件名 stem | 调度名称,也是 session id 后缀。 | +| `enabled` | 否 | `true` | 设为 `false` 可保留文件但暂停调度。 | + +5 字段 cron 会被归一化为 6 字段,前面补 `0` 秒。时间按 UTC 评估。 + +## 独立 Session + +每条 schedule 使用稳定 session id:`schedule:`。同一条 schedule 的多次触发会累积上下文;不同 schedule 彼此隔离。每个 session 都会注入 AgentDir 的 `instructions.md`、`agent.acl`、`skills/` 和 `tools/`。 + +这意味着日报和周报可以共享同一个 AgentDir,但不共享会话历史。需要共享状态时,应使用外部存储、A3S Memory 或显式工具。 + +## Serve 守护进程 + +```rust +use a3s_code_core::config::AgentDir; +use a3s_code_core::serve::serve_agent_dir; +use a3s_code_core::Agent; +use tokio_util::sync::CancellationToken; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let agent_dir = AgentDir::load("./release-agent")?; + let agent = Agent::from_config(agent_dir.config.clone()).await?; + let cancel = CancellationToken::new(); + + serve_agent_dir(&agent, &agent_dir, "./workspace", None, cancel).await?; + Ok(()) +} +``` + +`serve_agent_dir` 会运行所有 enabled schedules,直到 cancellation token 触发。优雅取消会让进行中的 turn 完成当前迭代后停止。 + +## 恢复与限制 + +传入 `SessionStore` 后,守护进程重启会恢复已有 `schedule:` session 的历史上下文。当前目录里的 `instructions.md`、`skills/` 和 `tools/` 会在每次启动时重新应用。 + +需要注意: + +- 恢复只恢复对话历史,不追补停机期间错过的触发。 +- store 目录是信任边界,它决定恢复后的历史和 workspace。 +- 无效 cron 会在构建调度器时失败,并指出出问题的 schedule。 +- 没有 enabled schedules 的 AgentDir 会立即返回。 diff --git a/website/docs/v6/zh/guide/filesystem-skills.mdx b/website/docs/v6/zh/guide/filesystem-skills.mdx new file mode 100644 index 00000000..3f4546ed --- /dev/null +++ b/website/docs/v6/zh/guide/filesystem-skills.mdx @@ -0,0 +1,74 @@ +--- +title: 'skills/ 技能目录' +description: '用 workspace 或 AgentDir skills/ 组织可复用技能、检查清单和领域流程。' +--- + +# skills/ 技能目录 + +`skills/` 目录存放可复用技能。技能适合表达稳定流程、检查清单、领域术语和工具使用准则;它不是 worker agent,也不负责启动独立会话。 + +A3S Code 有两类常见路径: + +```text +repo/.a3s/skills/ # workspace 级技能 +release-agent/skills/ # AgentDir 私有技能 +``` + +workspace 技能通过 `skillDirs` 或 `agent.acl` 的 `skill_dirs` 加载;agent 定义使用 +`agentDirs`,不要把 skill 目录放进 `agentDirs`。AgentDir 私有技能会在 +`serve_agent_dir` 为调度 session 创建上下文时注入。 + +## 技能文件 + +```md +--- +name: release-readiness +description: Check whether a repository is ready to release +allowed-tools: read(*), grep(*), bash(pnpm test*), bash(cargo test*) +--- + +Always inspect: + +- package or crate version changes +- migration compatibility +- release notes +- required verification commands + +Return blockers first, then risks, then follow-up work. +``` + +frontmatter 帮助发现和筛选,正文描述执行方式。`allowed-tools` 应保持最小集合。 +当 skill 通过 `Skill` 工具被调用时,省略 `allowed-tools` 不会授予任何工具, +因此 invocation 默认 fail-secure。技能只是指导模型,不应该扩大权限。 + +## 何时用 skills/ + +适合: + +- 反复出现的 review checklist。 +- 产品、协议、发布、迁移等领域流程。 +- 一组工具调用的推荐顺序。 +- 对多个 worker agents 都有用的共享背景。 + +不适合: + +- 需要独立身份、独立上下文或被 `task` 调用的角色;放进 `agents/`。 +- 需要周期性触发的任务;放进 `schedules/`。 +- 需要连接外部系统的能力声明;放进 `tools/` 或宿主 MCP 配置。 + +## 加载方式 + +```ts +const session = agent.session('/repo', { + skillDirs: ['./.a3s/skills'], +}); +``` + +模型可以通过 `search_skills` 查找相关技能。文件型 skills 和 inline skills 使用同一套 discovery 语义。目录越多,越要保证 `name` 和 `description` 可检索、无重名歧义。 + +## 维护建议 + +- 技能文件应该短而稳定,避免塞入整本文档。 +- 需要示例时给最小可执行片段,不要复制大量日志。 +- 当 skill 只对某个长期 Agent 有效,放在该 AgentDir 的 `skills/` 中。 +- 当 skill 是整个仓库规范,放在 `.a3s/skills/` 中,并在 `AGENTS.md` 中说明它的使用边界。 diff --git a/website/docs/v6/zh/guide/filesystem-tools.mdx b/website/docs/v6/zh/guide/filesystem-tools.mdx new file mode 100644 index 00000000..e4fa798e --- /dev/null +++ b/website/docs/v6/zh/guide/filesystem-tools.mdx @@ -0,0 +1,67 @@ +--- +title: 'tools/ 工具目录' +description: '用 AgentDir tools/ 声明 MCP 和 script tools,并保持权限、HITL 与 allow-list 边界。' +--- + +# tools/ 工具目录 + +`tools/` 是 AgentDir 的目录级工具声明。每个 `tools/.md` 描述一个模型可见能力,当前支持 `kind: mcp` 和 `kind: script` 两类。 + +```text +release-agent/ +└── tools/ + ├── github.md + └── search-auth.md +``` + +工具定义来自文件系统,但工具是否可见、能否执行、是否需要确认,仍由 harness、权限策略和 AgentDir loader 控制。文件存在不等于无限权限。 + +## MCP 工具 + +```md +--- +kind: mcp +name: github +transport: stdio +command: npx +args: ['-y', '@modelcontextprotocol/server-github'] +env: + GITHUB_TOKEN: '${GITHUB_TOKEN}' +--- + +GitHub issues and pull request tools. +``` + +每个已启用的调度 session 会在启动时连接该 MCP server,并获得命名空间化后的 `mcp__github__*` tools。secret 应通过环境变量注入,不要写进工具文件。 + +## Script 工具 + +```md +--- +kind: script +name: search-auth +path: scripts/search-auth.js +allowed_tools: [grep, glob, read] +limits: + timeoutMs: 30000 + maxToolCalls: 30 + maxOutputBytes: 65536 +--- + +Find authentication-related files and return an evidence list. +``` + +`kind: script` 把一个预先参数化的 QuickJS `program` 调用暴露成模型可见工具。脚本源码必须定义 `async function run(ctx, inputs)`。它没有文件系统、网络、进程或环境变量权限,只能通过 `ctx.tool(...)` 调用 allow-list 中的工具。 + +## 安全边界 + +- `allowed_tools` 是脚本内部能力边界;只列出最小集合。 +- 未知 `kind`、逃逸 workspace 的路径、重复 tool 名称、非法 limit 都应在加载时失败。 +- 不要让不可信目录声明高权限 MCP server 或脚本工具。 +- 高风险工具应继续走 HITL、allow-list 和审计。 + +## 当前作用域 + +`tools/` 由 `serve_agent_dir` 按调度 session 安装。也就是说,它服务于长期 Agent 和周期性任务。普通交互式 session 应优先使用宿主 direct tools、MCP 连接或 SDK 的 `session.tool(...)` 注册路径。 + +如果一个能力是项目通用连接器,放在宿主配置或 MCP 层更清晰;如果它只属于某个长期 Agent 的定时工作,放进该 AgentDir 的 `tools/`。 diff --git a/website/docs/v6/zh/guide/hooks.mdx b/website/docs/v6/zh/guide/hooks.mdx new file mode 100644 index 00000000..95be2579 --- /dev/null +++ b/website/docs/v6/zh/guide/hooks.mdx @@ -0,0 +1,25 @@ +--- +title: 'Hooks' +description: '生命周期拦截与策略回调' +--- + +# Hooks + +Hooks 用于在 session 内注册生命周期回调。管理路径是 `registerHook()`、 +`hookCount()` 和 `unregisterHook()`。 + +事件包括 `pre_tool_use`、`post_tool_use`、`generate_start`、`generate_end`、`session_start`、`session_end`、`skill_load`、`skill_unload`、`pre_prompt`、`post_response` 和 `on_error`。 + +```ts +session.registerHook( + 'release-publish-observer', + 'pre_tool_use', + { tool: 'bash', commandPattern: 'npm publish|twine upload|cargo publish' }, + { priority: 50, timeoutMs: 1000 }, + () => ({ action: 'continue' }), +); +``` + +handler 可返回 `continue`、`skip`、`block`,或者返回空表示继续。委派和自动 +subagent fan-out 都会走 `task` / `parallel_task` 路径;把 hook 当作生产关卡前, +应对产品实际依赖的 event path 做集成测试。 diff --git a/website/docs/v6/zh/guide/index.mdx b/website/docs/v6/zh/guide/index.mdx new file mode 100644 index 00000000..38f2e91a --- /dev/null +++ b/website/docs/v6/zh/guide/index.mdx @@ -0,0 +1,169 @@ +--- +title: '概览' +description: 'Rust coding-agent runtime 与 a3s code 终端工作区的执行内核' +--- + +# A3S Code + +A3S Code 是 `a3s code` 背后的 Rust runtime。它让 agent loop 保持可观测,并由 +同一个运行时内核负责上下文组装、受治理的 provider/tool 调用、权限策略、任务 +委派、workspace 访问、持久化、验证证据与 run replay。 + +`a3s-code` 是可嵌入的运行时和 SDK surface。`a3s code` 是 +[`a3s` CLI](https://github.com/A3S-Lab/Cli) 提供的交互式终端应用;它驱动 +A3S Code session,并用 [`a3s-tui`](https://github.com/A3S-Lab/TUI) 框架渲染 +事件流。想要现成 TUI 时安装 CLI;要构建自己的宿主、runner、IDE bridge 或产品 +UI 时安装 SDK。 + +## Surfaces + +| 名称 | 用途 | 仓库 | +| -------------- | ------------------------------------------------------------ | ----------------------------------------------- | +| `a3s-code` | 通过 Rust、Node.js 或 Python API 嵌入 coding-agent session。 | [A3S-Lab/Code](https://github.com/A3S-Lab/Code) | +| `a3s code` TUI | 运行现成的终端编程 agent。 | [A3S-Lab/Cli](https://github.com/A3S-Lab/Cli) | +| `a3s-tui` | 构建终端 UI surface;它是 UI 框架,不是 agent runtime。 | [A3S-Lab/TUI](https://github.com/A3S-Lab/TUI) | +| A3S Flow | `DynamicWorkflowRuntime` 使用的持久化 workflow engine。 | [A3S-Lab/Flow](https://github.com/A3S-Lab/Flow) | +| A3S monorepo | 产品文档、发布编排、submodule pin 与相关 crates。 | [A3S-Lab/a3s](https://github.com/A3S-Lab/a3s) | + +## 当前能力图 + +| 领域 | 当前能力 | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Agent 会话 | 异步优先的 `SessionBuilder` 构建 workspace-bound `AgentSession`。Transcript 操作使用 fail-fast single-flight,并暴露 `send`、`run`、`stream`、直接工具、run 状态、取消、持久化与生命周期清理。 | +| TUI | [`a3s code`](/guide/tui) 将运行时事件流渲染为终端 transcript,并提供 tool activity、approval、memory、file panel 与 session controls。 | +| 文件系统优先 | [文件系统优先](/guide/filesystem-first) 把 `AGENTS.md`、`agent.acl`、`.a3s/agents/`、`skills/`、`tools/` 与 `schedules/` 组织成可审查的 Agent 产品接口。 | +| 工具 | 稳定工具名覆盖文件、搜索、shell、git、web fetch/search、batch、结构化输出、QuickJS 程序化工具链、skills、MCP tools 与子任务委派。Model、nested、delegated 与 trusted host-direct call 通过同一个 invocation kernel,并携带显式 origin policy。 | +| 命令 | [命令](/guide/commands) 区分 TUI slash commands 与 SDK command registry,后者用于宿主自定义 `/command` handler。 | +| 委派 | 内置角色与自定义 Markdown/YAML agents 可通过 `task`、`parallel_task`、`session.task(...)`、`session.tasks(...)` 和自动委派控制使用。 | +| 编排 | [`session.parallel`](/guide/orchestration)、`session.pipeline`、workflow phases、可恢复 checkpoint、loop caps 与 workflow budget ledger 支持确定性的宿主侧 fan-out。 | +| 定时服务 | [AgentDir](/guide/agent-dir) 可通过 `serveAgentDir` / `serve_agent_dir` 运行定时 turn,每个启用 schedule 对应一个稳定的 `schedule:` session。 | +| 安全 | 单个 run invocation context 传递 identity、cancellation、event 与 governance。权限策略、HITL、budget、workspace 路径检查、工具超时、hook 和 output sanitization 进入统一 provider/tool 边界。 | +| Workspace | [Workspace 后端](/guide/workspace-backends) 让内置工具运行在本地文件、宿主提供的 workspace、可选 S3 兼容存储和可选 remote-git 服务上。 | +| 事件 | `EventEnvelopeV1` 是 Rust/Node/Python 共用的无损 wire shape,event type 保持开放,并为未来事件完整保留 payload/metadata。 | +| 持久化 | Atomic `SessionSnapshotV1` generation、memory/file store、session ID、auto-save、run snapshot/event、trace artifact、loop/workflow checkpoint、memory recall 与 retention cap 支持可恢复会话和可 replay 的产品状态。 | +| 验证 | [验证](/guide/verification) 覆盖显式验证命令、预设、结构化 reports、summaries、artifacts、trace events 与 run replay 证据。 | + +当前执行形态: + +```text +Agent / AgentSession + -> 上下文组装 + -> 可选 planning + -> 选择工具或委派子任务 + -> 权限与确认策略 + -> 执行 + -> trace、artifact 与验证证据 + -> 压缩与持久化 +``` + +## 安装 + +想使用交互式终端应用时安装 CLI: + +```bash +brew install A3S-Lab/tap/a3s + +# 或从 crates.io 安装 +cargo install a3s + +# 或从 CLI 仓库安装 +cargo install --git https://github.com/A3S-Lab/Cli +``` + +当你要把 A3S Code 嵌入自己的产品时,安装 SDK 包: + +```bash +npm install @a3s-lab/code +pip install a3s-code +cargo add a3s-code-core +``` + +## 配置 + +A3S Code 使用 ACL。不要把真实 API Key、私有模型端点、本地配置路径或 +tenant/user 标识提交到仓库;提交只通过环境变量解析凭据的模板。 + +```text +default_model = "provider/model-id" +max_parallel_tasks = 4 +auto_parallel = false + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "model-id" { + tool_call = true + limit = { + context = 128000 + output = 4096 + } + } +} + +agent_dirs = ["./.a3s/agents"] +skill_dirs = ["./skills"] +storage_backend = "file" +sessions_dir = ".a3s/sessions" +``` + +`auto_parallel = false` 只关闭自动并行子 agent fan-out。手动 +`parallel_task` 和 SDK `session.tasks(...)` 仍然可用,除非你单独关闭手动委派。 +本地 session 持久化需要把 `storage_backend = "file"` 和 `sessions_dir` 配对; +SDK embedding 场景也可以直接传 typed `FileSessionStore`。 + +## 使用 TUI + +在你希望 agent 检查的 workspace 中运行 `a3s code`: + +```bash +a3s code +a3s code resume +a3s code update +``` + +TUI 会按顺序发现配置:`A3S_CONFIG_FILE`、从当前目录向上查找的 +`.a3s/config.acl`,以及 `~/.a3s/config.acl`。 +顶层 `a3s update` 命令会进入同一个 updater。 + +## 使用 SDK + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/path/to/workspace', { + planningMode: 'auto', + permissionPolicy: { + allow: ['read(*)', 'grep(*)', 'glob(*)'], + ask: ['bash(*)', 'write(*)'], + deny: ['write(**/.env*)', 'bash(rm -rf*)'], + defaultDecision: 'ask', + enabled: true, + }, +}); + +const result = await session.send('Find the authentication entry points.'); +console.log(result.text); +console.log(result.verificationSummaryText); + +session.close(); +await agent.close(); +``` + +## 入口 + +- [A3S Code TUI](/guide/tui) 说明安装、配置发现、slash commands,以及 CLI 与 runtime 的区别。 +- [文件系统优先](/guide/filesystem-first)覆盖可审查的项目约定和 AgentDir 约定。 +- [API 契约](/guide/api-contract)说明经过集成测试的 Node SDK surface。 +- [会话](/guide/sessions)覆盖生命周期、streaming、run 状态、持久化与取消。 +- [命令](/guide/commands) 区分 TUI 命令和 SDK command registry 中的自定义处理器。 +- [工具](/guide/tools)覆盖直接工具、typed tool errors、结构化输出与 QuickJS programs。 +- [验证](/guide/verification)覆盖命令证据、reports 与 summaries。 +- [任务](/guide/tasks)覆盖模型驱动的子任务委派。 +- [编排](/guide/orchestration)覆盖宿主驱动的 fan-out、pipeline 与可恢复 workflow。 +- [安全](/guide/security)覆盖权限、确认、hooks 与验证 gate。 +- [Hooks](/guide/hooks)覆盖工具、prompt、skill、session 与 error 生命周期拦截。 +- [Agent Directory](/guide/agent-dir)覆盖持久化文件系统优先 agents、tools 与 schedules。 +- [Memory](/guide/memory) 与 [持久化](/guide/persistence)覆盖可复用事实、session snapshot 与恢复路径。 +- [Provider 配置](/guide/providers)覆盖 ACL provider 配置与环境变量注入。 diff --git a/website/docs/v6/zh/guide/isolation.mdx b/website/docs/v6/zh/guide/isolation.mdx new file mode 100644 index 00000000..5835cb1b --- /dev/null +++ b/website/docs/v6/zh/guide/isolation.mdx @@ -0,0 +1,19 @@ +--- +title: '隔离' +description: '工作区、上下文与委派任务隔离' +--- + +# 隔离 + +隔离从 session 边界开始:每个 session 绑定一个 workspace,委派子运行接收有边界的 context。宿主直接工具调用是特权操作;暴露给用户前,应在宿主应用内先做权限判断。 + +```ts +import { FileMemoryStore, FileSessionStore } from '@a3s-lab/code'; + +const session = agent.session('/repo', { + memoryStore: new FileMemoryStore('./.a3s/memory'), + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); +``` + +文件、搜索、shell 和 git 操作从 session 工作区求值。`task`、`parallel_task` 与自动 subagent 委派会隔离子运行上下文,父 agent 只合并紧凑结果。Security provider、hooks 和 permission policy 是集成点;把它们作为生产边界前,应对实际策略路径做 harness 测试。 diff --git a/website/docs/v6/zh/guide/lane-queue.mdx b/website/docs/v6/zh/guide/lane-queue.mdx new file mode 100644 index 00000000..6c32ea7d --- /dev/null +++ b/website/docs/v6/zh/guide/lane-queue.mdx @@ -0,0 +1,41 @@ +--- +title: 'Lane Queue' +description: '可选外部与混合分发基础设施' +--- + +# Lane Queue + +普通 session 默认不启用队列。Lane queue 是显式的高级基础设施,独立于 `task`、`parallel_task` 和自动 subagent 委派。 + +只有需要外部 worker、混合本地/远程执行、队列指标、死信或多机器分发时才启用 lane queue。 + +```ts +const session = agent.session('/repo', { + queueConfig: { + enableDlq: true, + enableMetrics: true, + }, +}); + +await session.setLaneHandler('execute', { + mode: 'external', + timeoutMs: 300000, +}); +``` + +lane 名称是 `control`、`query`、`execute` 和 `generate`。 + +外部 worker 完成任务时调用: + +```ts +const pending = await session.pendingExternalTasks(); + +if (pending.length > 0) { + await session.completeExternalTask(pending[0].id, { + success: true, + result: { summary: 'worker completed the task' }, + }); +} +``` + +普通本地 agent session 不需要引入队列。 diff --git a/website/docs/v6/zh/guide/limits.mdx b/website/docs/v6/zh/guide/limits.mdx new file mode 100644 index 00000000..a0a2f51b --- /dev/null +++ b/website/docs/v6/zh/guide/limits.mdx @@ -0,0 +1,150 @@ +--- +title: '运行限制' +description: '运行时限制、压缩、超时与熔断' +--- + +# 运行限制 + +Limit options 是 session 级控制项,用于长任务、噪声工具输出和 provider +失败场景。 + +## Session Options + +```ts +const session = agent.session('/repo', { + maxToolRounds: 24, + maxParseRetries: 3, + toolTimeoutMs: 120000, + circuitBreakerThreshold: 4, + autoCompact: true, + autoCompactThreshold: 0.75, + continuationEnabled: true, + maxContinuationTurns: 3, +}); +``` + +## Option Intent + +这些 option 的意图: + +- `maxToolRounds` 是单个 turn 的 tool iteration 预算。 +- `maxParseRetries` 是 malformed tool-call recovery 预算。 +- `toolTimeoutMs` 是每个 tool 的超时时间,单位毫秒。 +- `circuitBreakerThreshold` 是连续 provider 失败阈值。 +- `autoCompact` 和 `autoCompactThreshold` 控制 context compaction 行为。 +- `continuationEnabled` 和 `maxContinuationTurns` 控制 continuation injection。 + +## Practical Defaults + +CI、发布和用户可见自动化应使用严格限制;本地探索可以放宽预算,但有副作用的任务仍要显式验证。 + +## Retention Limits + +session 会把 run 历史、trace events 和 subagent task 快照保存在内存里。对于短任务没有问题;但对于在 cluster 负载下运行数小时甚至数天的 session,这些内存存储会无限增长。`SessionRetentionLimits`(CHANGELOG [3.3.0] "SessionRetentionLimits")提供可选的 FIFO 上限来防止泄漏。**默认是无上限的——不设置任何上限就不会改变任何行为。** + +四个相互独立的上限,可以只设置其中一部分,其余保持无上限: + +| 字段 | 触发上限时的效果 | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `max_runs_retained` | 新建 run 超过上限时,**最旧**的 run 及其全部 events 被丢弃。 | +| `max_events_per_run` | run 中最旧的 events 按 FIFO 丢弃。run 快照的 `event_count` **不会**被递减——它保持有史以来记录的累计总数。 | +| `max_trace_events` | trace sink 超过上限后,每次新写入都会丢弃最旧的一条 event。 | +| `max_terminal_subagent_tasks` | 超过上限后丢弃最旧的**终态**(completed / failed / cancelled)subagent task 快照。**running 的任务永远不会被丢弃。** | + +所有上限都是软上限:触发时在插入处丢弃最旧条目,绝不返回错误。 + +```ts +const session = agent.session('/repo', { + retentionLimits: { + maxRunsRetained: 100, + maxEventsPerRun: 5000, + maxTraceEvents: 20000, + maxTerminalSubagentTasks: 500, + }, +}); +``` + +```python +opts = SessionOptions() +opts.retention_limits = { + 'max_runs_retained': 100, + 'max_events_per_run': 5000, + 'max_trace_events': 20000, + 'max_terminal_subagent_tasks': 500, +} +session = agent.session('/repo', opts) +``` + +## Budget Guard + +`BudgetGuard`(CHANGELOG [3.3.0] "BudgetGuard")是一套由 host 提供的成本 / 配额契约。框架本身不强制执行预算——它只定义决策点,并咨询 host 注入的 guard。在 LLM / tool 调用处接入三个 hook: + +- `check_before_llm` —— 在每次 LLM 调用之前。 +- `record_after_llm` —— 在每次成功的 LLM 调用之后,带上 provider 的实际 usage,以便 host 保持累计消费的准确。 +- `check_before_tool` —— 在每次 tool 调用之前。 + +每个 `check_*` 返回三种决策之一: + +- `Allow` —— 正常继续,不发出 event。 +- `SoftLimit { resource, consumed, limit, message }` —— 发出 `AgentEvent::BudgetThresholdHit { kind: "soft" }` 并**继续执行**。session 内的 hook 可以据此反应(auto-compact、下一轮换用更便宜的 model)。 +- `Deny { resource, reason }` —— 以 `CodeError::BudgetExhausted` 中止本次调用。**session 仍然保持打开**——调用方可以稍后重试,或在 host 重新分配预算后重试。 + +### Node —— `session.setBudgetGuard({...})` + +每个回调接收**单个 `ctx` 对象**(不是位置参数),并返回一个决策 dict(或 `null` / `{ decision: 'allow' }` 表示放行): + +```ts +session.setBudgetGuard({ + checkBeforeLlm: (ctx) => { + // ctx.sessionId, ctx.estimatedTokens + if (overMonthlyCap(ctx.sessionId)) { + return { + decision: 'deny', + resource: 'llm_tokens', + reason: 'monthly cap', + }; + } + return { decision: 'allow' }; + }, + recordAfterLlm: (ctx) => { + // ctx.sessionId, ctx.usage —— usage 的 key 是 camelCase: + // promptTokens, completionTokens, totalTokens, cacheReadTokens, cacheWriteTokens + addSpend(ctx.sessionId, ctx.usage.totalTokens); + }, + checkBeforeTool: (ctx) => { + // ctx.sessionId, ctx.toolName + return { decision: 'allow' }; + }, + timeoutMs: 5000, // 可选,默认 5000 +}); +``` + +Node bridge **fail-closed(失败即拒绝)**:`check_*` 回调若未在 `timeoutMs` 内返回,或返回了无法解析的值,都会被当作 **deny** 处理——预算控制绝不能在 guard 卡住时悄悄自我失效(CHANGELOG [3.3.0] Fixed "Node BudgetGuard fail-open")。 + +回调**绝不能 throw。** 受 napi-rs 约束,回调抛出的异常会在返回值转换阶段中止 host 进程。请用 try/catch 包裹逻辑并返回一个决策(例如 deny),而不是抛异常。卡住(hang)的情况由 fail-closed 超时安全处理(CHANGELOG [3.3.0] Known limitations)。 + +### Python —— `budget_guard` session option + +Python 在 `budget_guard` 这个 `SessionOptions` 字段上提供一个 `BudgetGuard` 形态的对象。未定义的方法表现为 Allow / no-op。Python 回调使用**位置参数**,框架会捕获它们抛出的任何异常(抛异常的 `check_*` 默认视为 Allow): + +```python +class MyBudgetGuard: + def check_before_llm(self, session_id, est_tokens): + if over_monthly_cap(session_id): + return {'decision': 'deny', 'resource': 'llm_tokens', 'reason': 'monthly cap'} + return {'decision': 'allow'} + + def record_after_llm(self, session_id, usage): + # usage 是一个 dict,key 为 snake_case: + # total_tokens, cache_read_tokens(以及 prompt_tokens, completion_tokens, cache_write_tokens) + add_spend(session_id, usage['total_tokens']) + + def check_before_tool(self, session_id, tool_name): + return {'decision': 'allow'} + +opts = SessionOptions() +opts.budget_guard = MyBudgetGuard() +session = agent.session('/repo', opts) +``` + +决策返回 dict `{"decision": "deny", "resource": ..., "reason": ...}`(以及 `"soft"` / `"allow"`)在两个 SDK 上是相同的形状。 diff --git a/website/docs/v6/zh/guide/mcp.mdx b/website/docs/v6/zh/guide/mcp.mdx new file mode 100644 index 00000000..b737ba80 --- /dev/null +++ b/website/docs/v6/zh/guide/mcp.mdx @@ -0,0 +1,101 @@ +--- +title: 'MCP' +description: 'Model Context Protocol 服务集成' +--- + +# MCP + +MCP 把 A3S Code session 连接到外部工具服务器。stdio server 可以加入 live +session、检查状态、通过注册工具调用,并在不需要时移除。server 的工具会注册到 +session 中,名称格式为 `mcp____`。 + +## 添加 Server + +新代码优先使用 object-shaped `addMcp(...)` API。它直接对应 core 的 +`McpServerConfig` 形状,避免 positional overload 的参数顺序问题。 + +```ts +const count = await session.addMcp({ + name: 'echo', + transport: { + type: 'stdio', + command: process.execPath, + args: ['tools/mcp_echo_server.mjs', 'example-value'], + }, + timeoutMs: 30000, +}); + +console.log('registered tools:', count); +``` + +Python 暴露相同的对象式 API:`session.add_mcp({...})`。 +`addMcpServer(...)` / `add_mcp_server(...)` 和 +`addMcpServerConfig(...)` / `add_mcp_server_config(...)` 仍作为兼容别名保留。 + +远程 server 使用嵌套 transport object,`type` 取 `'http'` 或 +`'streamable-http'`。凭据应由宿主环境或 secret manager 注入;在生产文档或发布说明依赖前,先针对你的 server 做集成测试。 + +## 检查和移除 + +```ts +console.log(await session.mcpStatus()); +console.log(session.toolNames().filter((name) => name.startsWith('mcp__'))); +await session.tool('mcp__echo__echo', { message: 'docs mcp ok' }); +await session.removeMcpServer('echo'); +``` + +## 所有权与 Session 隔离 + +MCP capability discovery 与 live mutation 分属不同 owner: + +- Agent-global manager 持有从全局配置加载的 server。 +- 通过 Rust `SessionOptions` 传入的 manager 是该 session 继承的只读 capability + source。 +- 每个已构建 session 都新建一个私有 manager,专门承接 live `addMcp` 与 + `removeMcpServer` 操作。 + +Session assembly 会读取继承 source 的 tool definition,但不会把 session 配置 merge +回这些 manager。本地新增 server 可以在当前 session 内 shadow 同名的 inherited +fully-qualified tool;移除本地 server 后,继承工具会重新出现。这个操作不会 +unregister、disconnect 或改变 global/host-owned source,sibling session 也不会受 +影响。 + +Delegated child 会收到有序 capability source,从而调用同一批 MCP tool,但不会 +取得 owner 身份。`session.close()` 只断开私有 manager 的 server;`agent.close()` +和 `disconnectIdleMcp(...)` 仍负责 agent-global manager。 + +Rust 中,宿主传入的 MCP source 需要异步 session 构建,使 discovery 不阻塞: + +```rust +let session = agent + .session_builder("/repo") + .options(SessionOptions::new().with_mcp(manager)) + .build() + .await?; +``` + +## Idle Disconnect + +已连接的 stdio MCP server 即使处于空闲状态也会占用资源——file descriptor 和一个后台 worker。在长期运行的 cluster session 中,这些安静的 server 会越积越多。`disconnectIdleMcp`(CHANGELOG [3.3.0] "MCP idle disconnect")按需回收它们:断开每个最后活动时间早于阈值的全局 MCP server,释放 FD 和 worker,**同时保留每个 server 已注册的 config**,这样后续的 tool 调用可以按需重连。 + +这是一个 agent 级方法(它只作用于 agent 的全局 MCP manager,底层由 `McpManager::disconnect_idle(threshold_ms)` 支持)。它返回被断开的 server 名称列表。 + +```ts +// 回收空闲超过 5 分钟的 server,返回被断开的名称。 +const dropped = await agent.disconnectIdleMcp(5 * 60 * 1000); +console.log('disconnected:', dropped); +``` + +```python +# 回收空闲超过 5 分钟的 server,返回被断开的名称。 +dropped = agent.disconnect_idle_mcp(5 * 60 * 1000) +print('disconnected:', dropped) +``` + +运行成千上万长期 session 的 host 应通过 sweeper 周期性调用它(例如每 60s、阈值 5 分钟)。对已断开 server 的后续 tool 调用会从其保留的 config 重新建立连接——无需重新注册。 + +disconnect 还会清除孤儿时间戳:`touch()`-without-connect 的条目(为一个从未真正连接过的 server 记录的时间戳)会在每次 `disconnect_idle` 调用时被清扫,使活动映射在 manager 的长期生命周期内不会无限增长(CHANGELOG [3.3.0] Fixed "MCP timestamp leak")。 + +## 安全 + +外部 tool server 应视为特权集成,凭据只放在环境变量或宿主 secret manager 中。 diff --git a/website/docs/v6/zh/guide/memory.mdx b/website/docs/v6/zh/guide/memory.mdx new file mode 100644 index 00000000..59ab909f --- /dev/null +++ b/website/docs/v6/zh/guide/memory.mdx @@ -0,0 +1,82 @@ +--- +title: '记忆' +description: 'Session memory store 与显式 recall' +--- + +# 记忆 + +Memory 记录可复用事实,帮助驾驭层复用经验。 + +## 默认 Store + +每个 session 默认都有 memory store。普通 SDK session 若不传入自定义 store,会使用 +`/.a3s/memory` 下的文件存储。`a3s code` TUI 使用和 `/memory` 面板相同的 +`memory_dir`;默认是 `~/.a3s/memory`,所以不同项目会共享这份长期记忆,除非你配置 +项目本地路径。可以通过 config 的 `memory_dir` 移动默认位置,也可以给单个 session +传入 typed store object 覆盖默认后端。若文件 store 初始化失败,session 会降级到 +in-memory store,并通过 init warning 暴露该问题。 + +## LLM 抽取 + +LLM memory extraction is enabled by default when memory is available. Every +completed non-empty turn is submitted to the active model. The model, not a +keyword list, tool type, or turn-length heuristic, decides whether anything can +change a future answer or action. It must return an empty `items` array when +nothing qualifies. Tool calls and results are judgment context and are never +mechanically copied into long-term memory. + +Accepted output is limited to durable `semantic` or `procedural` memory with a +validated source, importance, confidence, scope, and future-value reason. The +runtime rejects malformed output and obvious credentials as structure and +safety checks; these checks do not decide semantic value. + +The model receives a bounded set of related memories and may identify supplied +IDs as `supersedes` or `conflicts_with`. The runtime validates those relations, +removes accepted superseded items, and preserves conflicts. Streaming hosts +register extraction before publishing the final event, process turns in FIFO +order, and wait for already accepted extraction during graceful close. + +`rememberSuccess` and `rememberFailure` remain explicit SDK operations. The +normal agent runtime does not invoke either operation for tool results. + +## Store 清理 + +Default stores merge only normalized exact duplicate content. Punctuation +remains significant, and distinct related wording remains separate unless the +LLM explicitly consolidates it through `supersedes`. The storage layer does not +infer semantic equivalence from keyword overlap. + +配置自动 pruning 后,系统会删除陈旧且低重要度的 memory;但 curated memory 会被硬保护: +pinned/protected item、频繁召回 item、consolidated memory,以及带 `supersedes` 或 +`conflicts_with` 关系 metadata 的 item 都不会因为容量上限被直接裁掉。 + +可以在 `config.acl` 中调整抽取上限: + +```text +memory { + llmExtraction = true + llmExtractionMaxItems = 5 + llmExtractionMaxInputChars = 8000 +} +``` + +## 手动写入与召回 + +```ts +import { FileMemoryStore } from '@a3s-lab/code'; + +const session = agent.session('/repo', { + memoryStore: new FileMemoryStore('./.a3s/memory'), +}); + +await session.rememberSuccess( + 'release preflight', + ['bash', 'grep'], + 'provider 验证通过', +); +const related = await session.recallSimilar('release provider verification', 5); +const recent = await session.memoryRecent(10); +const tagged = await session.recallByTags(['grep'], 10); +``` + +Memory 是辅助上下文,当前任务的验证证据仍来自当前命令和 trace。 diff --git a/website/docs/v6/zh/guide/multi-machine.mdx b/website/docs/v6/zh/guide/multi-machine.mdx new file mode 100644 index 00000000..2d9a738c --- /dev/null +++ b/website/docs/v6/zh/guide/multi-machine.mdx @@ -0,0 +1,242 @@ +--- +title: '多机器' +description: '通过 AgentExecutor seam 将编排步骤分布到多台机器' +--- + +# 多机器 + +A3S Code 把多智能体编排表达为代码中的一套*语法*(grammar),再把由此产生的步骤 +放到你希望它运行的任何地方。这一划分沿着**框架 / 宿主边界**展开,于 +`[3.4.0]` 引入: + +- **框架**拥有编排语法和可序列化的数据契约。它从不决定步骤在哪里运行。 +- **宿主**拥有放置(placement)、传输和调度——哪个节点执行某个步骤、step 规格如何 + 送达、以及并发如何映射到集群。 + +两者之间唯一的接触点是一个 trait:`AgentExecutor`。 + +## 框架 / 宿主边界 + +框架的契约是两个可序列化类型: + +- `AgentStepSpec`——_运行什么_,与*在哪里运行*无关:`task_id`、`agent`、 + `description`、`prompt`,以及可选的 `max_steps`、`parent_session_id`、 + `output_schema`。 +- `StepOutcome`——运行一个 spec 的结果:`task_id`、`session_id`、`agent`、 + `output`、`success`,以及可选的 `structured`。 + +两者都能干净地序列化,因此宿主可以把一个 spec 送到另一个节点,并把 outcome 持久化 +到 checkpoint。组合这些 spec 的 combinators 完全针对 `AgentExecutor` trait 编写, +从不观察步骤实际在哪里运行——所以同一套编排可以从单进程扩展到集群而无需改动。 + +## AgentExecutor seam + +`AgentExecutor` 是语法与宿主之间的边界: + +```text +combinators (parallel / pipeline / resumable) + -> AgentExecutor::execute_step(spec) -> StepOutcome + ├─ 内置 TaskExecutor:把步骤作为本地子 agent 运行 + └─ 宿主 executor:把步骤放到远程节点 +``` + +内置的 `TaskExecutor` 把每个步骤作为子 agent 在本地运行——在进程内、基于 Tokio—— +继承会话的 agent 注册表、LLM 客户端、工作区、MCP 工具和 subagent 跟踪器。诸如 +集群运行时这样的宿主用自己的 executor 替换它,把步骤分布到集群;combinators 不受影响。 + +`concurrency_hint()` 是**建议性的,不是本地硬上限**。本地默认返回会话的 +`max_parallel_tasks`;由调度器支撑的宿主可以返回其集群范围的目标值。因为它是 hint +而非 ceiling,编排得以扩展到单进程之外。 + +会话直接暴露内置 seam: + +- `AgentSession::agent_executor()` 返回一个由会话支撑的 `AgentExecutor`。 +- `AgentSession::session_store()` 返回会话的 store(在配置了的情况下),可恢复的 + combinator 需要它来记录进度。 + +下面的 SDK 语法会替你调用 `agent_executor()`;只有在实现或替换自定义 executor 时 +你才需要直接使用它们。 + +## Parallel:屏障式 fan-out + +`execute_steps_parallel` 把 `specs` 在 executor 上 fan-out 并等待全部完成(一个 +屏障 / barrier)。结果保持输入顺序,panic 的分支会变成一个失败的 `StepOutcome` +而不会拖垮整批,并且并发受 executor 的 concurrency hint 限制。 + +```ts +const outcomes = await session.parallel([ + { + taskId: 'a', + agent: 'explore', + description: 'survey', + prompt: 'Map the auth module', + }, + { + taskId: 'b', + agent: 'review', + description: 'audit', + prompt: 'Review error handling', + }, +]); + +for (const o of outcomes) { + console.log(o.taskId, o.success, o.output); +} +``` + +```python +outcomes = session.parallel([ + {"task_id": "a", "agent": "explore", "description": "survey", "prompt": "Map the auth module"}, + {"task_id": "b", "agent": "review", "description": "audit", "prompt": "Review error handling"}, +]) + +for o in outcomes: + print(o["task_id"], o["success"], o["output"]) +``` + +## Pipeline:逐项链路,stage 之间无屏障 + +`execute_pipeline` 让每个 item 独立地流经一条由 `PipelineStage` 组成的链路。 +**stage 之间没有屏障**——item A 可以处于 stage 3,而 item B 仍在 stage 1——因此 +墙钟时间取决于最慢的单条链路,而不是每个 stage 最慢步骤之和。 + +一个 stage 是 `(ctx) => spec | null` 回调,其中 `ctx` 携带上一步的 outcome 和原始 +item。返回一个 spec 以运行下一步,或返回 `null` 提前停止该 item 的链路;当某步失败 +时链路也会停止(后续 stage 只会建立在失败结果之上)。这些桥接**fail closed**:一个 +挂起、返回 `null` 或抛出的 stage 只会停止它自己的链路。 + +> Node 的 stage 回调**绝不能 throw**——把逻辑包在 `try`/`catch` 中并在出错时返回 +> `null`(与 `setBudgetGuard` 相同的约束)。挂起超过超时的 stage 会对该链路 fail +> closed。Python 的 stage 若 raise 会被捕获并当作 `null` 处理。 + +```ts +const outcomes = await session.pipeline( + ['src/auth', 'src/api'], + [ + (ctx) => ({ + taskId: 's1', + agent: 'explore', + description: 'survey', + prompt: `Survey ${ctx.item}`, + }), + (ctx) => + ctx.previous?.success + ? { + taskId: 's2', + agent: 'review', + description: 'review', + prompt: `Review: ${ctx.previous.output}`, + } + : null, + ], +); +``` + +```python +def survey(ctx): + return {"task_id": "s1", "agent": "explore", "description": "survey", + "prompt": f"Survey {ctx['item']}"} + +def review(ctx): + prev = ctx["previous"] + if prev and prev["success"]: + return {"task_id": "s2", "agent": "review", "description": "review", + "prompt": f"Review: {prev['output']}"} + return None + +outcomes = session.pipeline(["src/auth", "src/api"], [survey, review]) +``` + +## Resumable:跨节点恢复 + +`execute_steps_parallel_resumable` 是 `parallel` 加上一份日志。在每个步骤边界,它把 +一个 `WorkflowCheckpoint` 以 `workflowId` 为键写入 `SessionStore`。恢复时它跳过已 +完成的步骤,重新派发其余步骤。它**只记录成功的步骤**,因此失败的步骤会在恢复时重试 +——它的效果尚未完成。 + +需要一个 `SessionStore`。当未配置时,Node 桥接以 +`"parallelResumable requires a sessionStore"` 拒绝;Python 抛出等价异常。 + +```ts +import { Agent, FileSessionStore } from '@a3s-lab/code'; + +const session = agent.session('/repo', { + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); + +const outcomes = await session.parallelResumable( + [ + { + taskId: 'a', + agent: 'explore', + description: 'survey', + prompt: 'Map the auth module', + }, + { + taskId: 'b', + agent: 'review', + description: 'audit', + prompt: 'Review error handling', + }, + ], + 'release-audit', +); +``` + +```python +from a3s_code import Agent, FileSessionStore, SessionOptions + +opts = SessionOptions() +opts.session_store = FileSessionStore("./.a3s/sessions") +session = agent.session("/repo", opts) + +outcomes = session.parallel_resumable([ + {"task_id": "a", "agent": "explore", "description": "survey", "prompt": "Map the auth module"}, + {"task_id": "b", "agent": "review", "description": "audit", "prompt": "Review error handling"}, +], "release-audit") +``` + +因为 checkpoint 是可序列化的,且 executor 是一个参数,宿主可以通过传入另一个节点的 +executor,在**不同的节点**上恢复被中断的工作流——框架迁移*运行什么*,宿主提供 +_在哪里运行_。 + +## Schema 强制的步骤输出 + +携带 `output_schema`(Node 中为 `outputSchema`)的 spec 必须返回一个符合该 JSON +Schema 的值;经校验的对象落入 `StepOutcome.structured`。executor 复用结构化输出的 +coercion 与 repair 机制。coercion 失败会**把该步骤降级为不成功**,因此调用方永远不会 +把未经校验的文本当作所承诺的对象。 + +```ts +const [finding] = await session.parallel([ + { + taskId: 'classify', + agent: 'review', + description: 'classify', + prompt: 'Classify this defect', + outputSchema: { + type: 'object', + properties: { severity: { type: 'string' }, summary: { type: 'string' } }, + required: ['severity', 'summary'], + }, + }, +]); + +if (finding.success && finding.structured) { + console.log(finding.structured.severity); +} +``` + +## 跨机器放置编排步骤 + +lane queue 仍然是一种有效的传输——但它现在是**`AgentExecutor` seam 背后的一个选项, +而不是唯一的集成点**。要分布工作,宿主针对任何适合其平台的传输(HTTP、消息队列、 +任务系统、lane queue、内部 RPC)实现 `AgentExecutor::execute_step`,把 +`concurrency_hint()` 接到其集群目标,再把该 executor 交给 combinators。语法—— +parallel、pipeline、resumable——保持不变;移动的只有放置。 + +结果是:coordinator 会话拥有对话、最终合成和发布决策,而它的编排步骤在宿主放置的 +任何地方执行,跨节点恢复由可序列化的 checkpoint 承载。 + +参见 [Orchestration](/guide/orchestration) 深入了解 combinator 语法,以及 +[Persistence](/guide/persistence) 了解 checkpoint store。 diff --git a/website/docs/v6/zh/guide/orchestration.mdx b/website/docs/v6/zh/guide/orchestration.mdx new file mode 100644 index 00000000..07fd89a4 --- /dev/null +++ b/website/docs/v6/zh/guide/orchestration.mdx @@ -0,0 +1,438 @@ +--- +title: '编排' +description: '可编程、确定性的多 agent 编排:并行 fan-out、无屏障流水线与可恢复工作流' +--- + +# 编排 + +编排是模型驱动委派的*可编程*姊妹能力。在 [Tasks](/guide/tasks) 与 +[Teams](/guide/teams) 中,由 LLM 在运行时决定是否调用 `task` / +`parallel_task`——fan-out 的形状取决于模型的选择。编排把这个决策搬进你的代码:你 +用一套语法表达 fan-out、流水线与验证面板,因此形状是可复现、可测试、受预算约束 +且可恢复的——与模型的临场选择无关。 + +当工作的*结构*是宿主提前已知的(并行跑这三个 reviewer;让每个候选项依次经过 +explore → verify → review;崩溃后恢复这一批),就用编排。当你希望由模型决定是否 +以及如何委派时,就用 Tasks/Teams。 + +## 框架 / 宿主边界(接缝) + +本层的一切都围绕单一接缝 `AgentExecutor` 编写:"运行这个 step,把结果给我。"这 +条接缝把职责切得很干净: + +- **框架**拥有*语法*——有哪些 step、如何组合、并发*提示*,以及可序列化契约 + `AgentStepSpec` / `StepOutcome`。 +- **宿主**拥有*放置*——传输、调度,以及 step 实际在哪里运行。 + +内置的默认 executor(`TaskExecutor`)在本地、进程内、基于 tokio 运行每个 step。 +宿主可以替换为自己的 `AgentExecutor`,把 step 放置到集群各处;组合子从不观察 step +在哪里运行,因此同一套编排无需改动即可从单进程扩展到集群。 + +`concurrency_hint()` 是**建议性**的,而非硬性本地上限——正是它让编排能扩展到单进 +程之外(由调度器支撑的宿主会返回其集群级目标,而不是本地上限)。 + +SDK 已为你接好这一切:`AgentSession::agent_executor()` 返回由 session 支撑的 +executor(它把每个 step 作为子 agent 在本节点运行,继承 session 的 agent registry、 +LLM client、workspace 与 MCP 工具),`session_store()` 返回 session 的 store。 +`parallel` / `pipeline` / `parallelResumable` 方法会替你调用它们。 + +## Step 契约 + +一个 step 由 `AgentStepSpec` 描述,并解析为 `StepOutcome`。两者都刻意保持可序列 +化:宿主可以把 spec 发送到另一个节点,可恢复组合子也会把 outcome 持久化进 +checkpoint。 + +`AgentStepSpec` 字段: + +- `task_id`——step 的稳定 id(由你指定);会流入生命周期事件与 checkpoint。 +- `agent`——要运行的 agent 的 registry key(例如 `explore`、`review`)。 +- `description`——用于展示/追踪的简短人类标签。 +- `prompt`——交给子 agent 的指令。 +- `max_steps`_(可选)_——每个 step 的 tool-round 上限。 +- `parent_session_id`_(可选)_——用于事件关联的父 session id。 +- `output_schema`_(可选)_——设置后,step 必须返回符合此 JSON Schema 的值(见 + [强制 schema 的 step 输出](#schema-forced-step-output))。 + +`StepOutcome` 字段: + +- `task_id`——产生该结果的 step id。 +- `session_id`——子运行的 session id(失败的 step 仍可寻址)。 +- `agent`——运行的 agent。 +- `output`——step 的文本输出。 +- `success`——失败或 panic 的 step 为 `false`(绝不会丢弃兄弟 step)。 +- `structured`_(可选)_——经 schema 校验的对象,仅当 spec 携带 `output_schema` + 时存在。 + +key 的大小写风格因 SDK 而异: + +| 概念 | Node(camelCase) | Python(snake_case) | +| --------------- | ----------------- | -------------------- | +| step id | `taskId` | `task_id` | +| tool-round 上限 | `maxSteps` | `max_steps` | +| 父 session | `parentSessionId` | `parent_session_id` | +| 强制 schema | `outputSchema` | `output_schema` | + +`agent`、`description`、`prompt`、`output`、`success` 与 `structured` 在两个 SDK +中拼写相同。 + +## parallel —— 屏障式 fan-out [#parallel--barrier-fan-out] + +`session.parallel(specs)` 把每个 spec 作为 fan-out 运行,并按**输入顺序**解析出每 +个 spec 对应的一个 `StepOutcome`。它映射到核心组合子 `execute_steps_parallel`。它 +是一个**屏障**:在返回前会等待每个 step。 + +每个分支彼此隔离——失败*或 panic* 的 step 会变成 `success: false`,绝不会丢弃兄弟 +step。并发受 executor 的并发提示约束(默认即 session 配置的并行度)。 + +```ts +const outcomes = await session.parallel([ + { + taskId: 'explore', + agent: 'explore', + description: 'Risky changes', + prompt: 'Find risky changed files in this diff.', + }, + { + taskId: 'verify', + agent: 'verification', + description: 'Test gaps', + prompt: 'Identify missing or weak verification.', + }, + { + taskId: 'review', + agent: 'review', + description: 'Correctness', + prompt: 'Review the diff for correctness risks.', + }, +]); + +for (const outcome of outcomes) { + if (outcome.success) { + console.log(outcome.taskId, outcome.output); + } else { + console.warn('failed:', outcome.taskId, outcome.output); + } +} +``` + +```python +outcomes = session.parallel([ + {"task_id": "explore", "agent": "explore", "description": "Risky changes", + "prompt": "Find risky changed files in this diff."}, + {"task_id": "verify", "agent": "verification", "description": "Test gaps", + "prompt": "Identify missing or weak verification."}, + {"task_id": "review", "agent": "review", "description": "Correctness", + "prompt": "Review the diff for correctness risks."}, +]) + +for outcome in outcomes: + if outcome["success"]: + print(outcome["task_id"], outcome["output"]) + else: + print("failed:", outcome["task_id"], outcome["output"]) +``` + +## pipeline —— 阶段之间无屏障 + +`session.pipeline(items, stages)` 让每个 item **独立地**流经一连串阶段——阶段之间 +没有屏障,因此 item A 可以处于 stage 3,而 item B 还在 stage 1。墙钟时间是最慢的 +_单条链_,而不是逐阶段屏障会带来的"每阶段最慢之和"。 + +阶段是 spec 构造器,而非 spec:每个阶段接收上一个 outcome 和原始 item,返回要运行 +的下一个 step,或返回 `null` / `None` 提前停止该 item 的链。失败的 step 同样会停止 +链(后续阶段只会基于失败结果继续)。回调形状: + +- Node:`(ctx) => spec | null`,其中 `ctx = { previous: StepOutcome | null, item }` +- Python:`stage(ctx) -> spec | None`,其中 `ctx = {"previous": , "item": }` + +阶段可以基于上一个 outcome 分支——例如"验证 review 阶段产出的发现"。 + +约束(来自源码): + +- pipeline 阶段**不支持**逐阶段 `output_schema`——需要 schema 校验的 step 请用 + [`parallel`](#parallel--barrier-fan-out)。 +- **Node:** 阶段回调**绝不能 throw**——一次 throw 会中止进程(与 `setBudgetGuard` + 相同的约束)。请把逻辑包进 `try/catch`,出错时 `return null`。 +- **Node:** 超过 `timeoutMs`(第 3 个参数,默认 `30000`)仍挂起的阶段会 fail + closed——被当作 `null` 处理,仅停止该条链。 +- **Python:** 抛异常的阶段 callable 会被捕获并当作 `None`(仅停止该条链)。 + +> Node pipeline 阶段回调**绝不能 throw。** 在当前 napi 版本中,返回值转换时的 JS +> throw 会中止进程(与 `setBudgetGuard` 相同的 fail-closed 约束)。务必把阶段逻辑 +> 包进 `try/catch` 并在出错时 `return null`。 + +```ts +const outcomes = await session.pipeline( + ['src/auth.ts', 'src/payments.ts'], + [ + (ctx) => ({ + taskId: `explore-${ctx.item}`, + agent: 'explore', + description: 'Inspect file', + prompt: `Summarize the responsibilities and risks of ${ctx.item}.`, + }), + (ctx) => { + try { + if (!ctx.previous) return null; + return { + taskId: `review-${ctx.item}`, + agent: 'review', + description: 'Review of prior finding', + prompt: `Review this summary for correctness risks:\n${ctx.previous.output}`, + }; + } catch { + return null; // stages must not throw + } + }, + ], +); +``` + +```python +def explore_stage(ctx): + item = ctx["item"] + return { + "task_id": f"explore-{item}", + "agent": "explore", + "description": "Inspect file", + "prompt": f"Summarize the responsibilities and risks of {item}.", + } + +def review_stage(ctx): + prev = ctx["previous"] + if prev is None: + return None + item = ctx["item"] + return { + "task_id": f"review-{item}", + "agent": "review", + "description": "Review of prior finding", + "prompt": f"Review this summary for correctness risks:\n{prev['output']}", + } + +outcomes = session.pipeline( + ["src/auth.ts", "src/payments.ts"], + [explore_stage, review_stage], +) +``` + +## 可恢复 / 可迁移工作流 + +`session.parallelResumable(specs, workflowId)`(Node)/ +`session.parallel_resumable(specs, workflow_id)`(Python)是 `parallel` 加上一份 +日志。它映射到 `execute_steps_parallel_resumable`。 + +它在每个 step 边界把一份 `WorkflowCheckpoint` 写入 session store。恢复时它跳过已完 +成的 step(复用其缓存的 outcome),只重新派发其余 step。它**只记录成功的 step**—— +失败的 step 不入日志,因此恢复时会重试。完全成功后 checkpoint 会被删除;只有崩溃才 +会留下一份供恢复。 + +由于 checkpoint 可序列化、executor 是一个参数,宿主可以通过传入另一个节点的 +executor,在**另一个节点**上恢复被中断的工作流(迁移)。 + +该组合子**要求已配置 session store**——两个 SDK 方法在缺少 store 时都会 reject/ +raise(Node 的错误信息是 `parallelResumable requires a sessionStore`)。 + +`WorkflowCheckpoint` 的 schema 为 `schema_version` / `workflow_id` / `steps` / +`checkpoint_ms`。由*未来的*、不兼容的 `schema_version` 写入的 checkpoint 在加载时 +会被拒绝(`ensure_loadable`)。该失败是 fail-safe 而非致命的:不可读的 checkpoint +会记录一条 warning,工作流从头重跑,而不是从它无法解释的状态恢复。 + +store 相关见 [Persistence](/guide/persistence),迁移路径见 +[Multi-Machine](/guide/multi-machine)。 + +```ts +import { Agent, FileSessionStore } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/repo', { + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); + +// First attempt — may be interrupted partway through. +let outcomes = await session.parallelResumable(specs, 'release-batch-42'); + +// After a crash/restart: same workflowId resumes, skipping completed steps. +outcomes = await session.parallelResumable(specs, 'release-batch-42'); +``` + +```python +from a3s_code import Agent, FileSessionStore, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.session_store = FileSessionStore("./.a3s/sessions") +session = agent.session("/repo", opts) + +# First attempt — may be interrupted partway through. +outcomes = session.parallel_resumable(specs, "release-batch-42") + +# After a crash/restart: same workflow_id resumes, skipping completed steps. +outcomes = session.parallel_resumable(specs, "release-batch-42") +``` + +## 跨 fan-out 的共享预算 [#shared-budget] + +默认情况下,每个子代理各自统计自己的 LLM 成本。给 `parallel` 传入一个 token +预算后,所有子代理改为汇入**同一个账本**——为整个 fan-out 设一个统一上限。它映射 +到核心的 `WorkflowBudget`:一个安装到每个子运行上的、聚合型 `BudgetGuard`。 + +预算是一个**可选参数**,因此向后兼容: + +- 不传预算时,`parallel(specs)` 返回原来的结果数组,与之前完全一致。 +- 传入预算时,`parallel(specs, budgetTokens)` 解析为 `{ outcomes, budget }`, + 其中 `budget` 是账本快照(`consumedTokens` / `limitTokens`)。 + +一旦达到上限,之后*启动*的 step 会被拒绝——其结果为 `success: false`,并带有预算 +耗尽信息。它是一个**软上限**:由于用量是在每次 LLM 调用*之后*记账的,宽 fan-out +可能在账本追上之前冲过上限几个在飞回合。框架**绝不**强杀在飞的 fan-out;预算耗尽 +只是拒绝*下一次* LLM 调用。 + +```ts +// 不传预算 → 原样返回结果数组(行为不变)。 +const outcomes = await session.parallel(specs); + +// 传入预算 → { outcomes, budget }。所有子代理共享一个账本。 +const { outcomes: out, budget } = await session.parallel(specs, 500_000); +console.log(budget.consumedTokens, budget.limitTokens); // 例如 48213, 500000 +// TS 返回类型是联合类型:Array | { outcomes, budget }。 +``` + +```python +# 不传预算 → 原样返回列表(行为不变)。 +outcomes = session.parallel(specs) + +# 传入预算 → {"outcomes": [...], "budget": {"consumed_tokens", "limit_tokens"}}。 +res = session.parallel(specs, budget_tokens=500_000) +print(res["budget"]["consumed_tokens"], res["budget"]["limit_tokens"]) +``` + +## 循环直到完成(`execute_loop`)[#looping] + +对于长度未知、需要迭代至收敛的工作(loop-until-dry、反复打磨直到满意),核心语法 +提供了 `execute_loop`。每一轮都是一个屏障(`execute_steps_parallel`);宿主提供的 +谓词看到本轮的结果,返回 `LoopDecision::Continue(next_specs)` 或 +`LoopDecision::Stop`。必填的 `max_iterations` 是一个**硬上限**——一旦达到,即使谓词 +还想继续也会停止,从而让 LLM 驱动的循环永远不会失控。 + +```rust +use a3s_code_core::orchestration::{execute_loop, AgentStepSpec, LoopDecision}; + +let outcomes = execute_loop(executor, initial_specs, /* max_iterations */ 5, None, |round| { + // 本轮没有新发现就停止;否则扇出后续 step。 + let follow_ups = derive_follow_ups(round); + if follow_ups.is_empty() { + LoopDecision::Stop + } else { + LoopDecision::Continue(follow_ups) + } +}) +.await; +``` + +> 从宿主 SDK 你并不需要专门的 `loop` 动词——直接用你自己语言里的 `while`/`for` +> 围绕 `parallel` 写循环,根据结果决定下一轮即可。`execute_loop` 是为 Rust 语法 +> 而存在,并为循环提供一个单一、强制的终止守卫。 + +## Workflow facade(Rust / 嵌入)[#workflow-facade] + +`session.workflow()` 返回一个可廉价克隆的 `Workflow`,它预先接好了会话的 +executor、持久化 store、逐 step 事件流,以及一个稳定的、由会话派生的 root id。它是 +把以上能力打包起来的可编程句柄;控制流就是普通 Rust——`await` 一个动词、查看结果、 +决定下一步运行什么。 + +- **动词**——`agent`(单步)、`parallel`(屏障式 fan-out)、`phase`(*命名*的、 + 可恢复的屏障,并发出里程碑)、`pipeline`(按条目的链),以及不会失败的 `log`。 + 每个动词都只委派给一个 combinator。 +- **Phase 与事件**——`phase(name, specs)` 派生确定性 checkpoint id + (`{root}/{index}:{name}`),在配置了 store 时走可恢复屏障,并在一个广播上发出 + `WorkflowEvent::PhaseStart` / `PhaseEnd`,你可用 `subscribe()` 读取。`log()` + 发出 `WorkflowEvent::Log`。 +- **预算**——`session.workflow_with_token_budget(Some(limit))` 安装一个共享的 + `WorkflowBudget`;`budget_snapshot()` 读取账本,达到上限时会触发 + `WorkflowEvent::BudgetExhausted`。 + +```rust +let wf = session.workflow(); // 或 session.workflow_with_token_budget(Some(500_000)) +let mut events = wf.subscribe(); + +// 先跑一步,再根据其结果计算出一个*可变*数量的 fan-out——这正是“动态”所在: +// 形状在运行时决定,而非提前声明。 +let plan = wf.agent(AgentStepSpec::new("plan", "plan", "plan", goal)).await; +let specs = derive_specs(&plan); // 你的代码 +let done = wf.phase("implement", specs).await; // 可恢复屏障 + 里程碑 +let reviews = wf.phase("review", to_review(&done)).await; // 预算在各 phase 间共享 + +if let Some(b) = wf.budget_snapshot() { + println!("spent {} / {:?} tokens", b.consumed_tokens, b.limit_tokens); +} +``` + +SDK 暴露的是扁平的 `parallel` / `pipeline` / `parallelResumable` 动词(以及上面 +`parallel` 的预算重载);完整的 `Workflow` 句柄——phases、事件订阅、loop +combinator——属于 Rust / 嵌入层 API。 + +## 强制 schema 的 step 输出 [#schema-forced-step-output] + +携带 `output_schema`(Node 中为 `outputSchema`)的 spec 会强制 step 返回符合该 JSON +Schema 的值;经校验的对象落在 `StepOutcome.structured` 中。这复用了与 A3S Code 其余 +部分相同的结构化输出强转 + 修复机制。强转失败会把该 step **降级为不成功** +(`success: false`),因此调用方绝不会把未经校验的文本当作承诺的对象。 + +强制 schema 仅适用于 `parallel` / `parallelResumable` 的 spec——**不**适用于 +pipeline 阶段。 + +```ts +const [outcome] = await session.parallel([ + { + taskId: 'triage', + agent: 'review', + description: 'Structured triage', + prompt: 'Triage this diff.', + outputSchema: { + type: 'object', + properties: { + severity: { type: 'string', enum: ['low', 'medium', 'high'] }, + summary: { type: 'string' }, + }, + required: ['severity', 'summary'], + }, + }, +]); + +if (outcome.success) { + console.log(outcome.structured.severity, outcome.structured.summary); +} +``` + +```python +outcomes = session.parallel([ + { + "task_id": "triage", + "agent": "review", + "description": "Structured triage", + "prompt": "Triage this diff.", + "output_schema": { + "type": "object", + "properties": { + "severity": {"type": "string", "enum": ["low", "medium", "high"]}, + "summary": {"type": "string"}, + }, + "required": ["severity", "summary"], + }, + }, +]) + +outcome = outcomes[0] +if outcome["success"]: + print(outcome["structured"]["severity"], outcome["structured"]["summary"]) +``` + +## 成本治理与生命周期 + +编排 step 走的是同一个 session,因此 session 的各项控制对它们直接生效。 +`setBudgetGuard`(Node)/ `budget_guard`(Python)约束每个 step 的 LLM 成本; +`close()` 会连同 session 的其余工作一起取消进行中的 step;宿主提供的身份标签 +(`tenant_id`、`principal`、`agent_template_id`、`correlation_id`)会贯穿每个 step, +供宿主侧聚合与计费使用。这些控制的细节见 [Sessions](/guide/sessions) 与 +[Limits](/guide/limits)。 diff --git a/website/docs/v6/zh/guide/persistence.mdx b/website/docs/v6/zh/guide/persistence.mdx new file mode 100644 index 00000000..288f3eb9 --- /dev/null +++ b/website/docs/v6/zh/guide/persistence.mdx @@ -0,0 +1,142 @@ +--- +title: '持久化' +description: '保存与恢复 session' +--- + +# 持久化 + +持久化让 session 可以跨进程恢复,也让产品界面拥有稳定 session ID。 + +A3S Code 会持久化三种相关但不同的对象: + +| 对象 | 写入方 | 恢复入口 | 用途 | +| ------------------- | ------------------------------------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `SessionSnapshotV1` | `session.save()` 或 `autoSave` | `agent.resumeSession(id, options)` | 恢复一个带版本的完整 generation,其中包含对话、artifact、trace、run record、verification report 与 subagent task snapshot。 | +| Loop checkpoint | run 执行中的 agent loop | `session.resumeRun(runId)` | 从上一个完成的 tool-round 边界继续一次被中断的 run。进程内正常完成的 run 会删除这个 checkpoint。 | +| Workflow checkpoint | `parallelResumable` / workflow phase | `parallelResumable(specs, workflowId)` | 进程重启后跳过已经完成的编排 step。 | + +## File Session Store + +```ts +import { FileSessionStore } from '@a3s-lab/code'; + +const session = agent.session('/repo', { + sessionId: 'release-review', + sessionStore: new FileSessionStore('./.a3s/sessions'), + autoSave: true, +}); + +await session.send('检查发布就绪情况'); +await session.save(); +``` + +## 原子 Snapshot Generation + +`session.save()` 会把当前持久化状态收集成一个 `SessionSnapshotV1`,并且只调用一次 +`SessionStore::save_snapshot`。Envelope 包含: + +- `schema_version` 与 `SessionData` +- tool artifact +- trace event 与 run record +- verification report +- delegated subagent task snapshot + +File store 会把完整 JSON envelope 写入并同步临时文件,然后 atomic replace +`.json`。因此 reader 看到的是上一代或下一代,不会读到“新 conversation +搭配旧 run/trace fragment”的组合。Memory store 在同一把锁下发布同一个 aggregate。 +两者都报告 `SessionStoreCapabilities { atomic_session_snapshots: true }`。 + +历史文件仍可读取。Bare `SessionData` 会在 load 时与旧 artifact/trace/run/ +verification/subagent fragment location 合并,再通过 v1 内存形状恢复。新 aggregate +保存后,单个 envelope 成为 authoritative generation。已经具有 aggregate 外形、但 +schema 损坏或版本不支持的文档会直接被拒绝,不会重新解释成 legacy data。 + +自定义 store 必须显式实现 `save_snapshot`。默认实现返回错误,不会把 aggregate +拆成多次独立 write,也不会把 no-op 当成成功。默认 `load_snapshot` 只用于 +best-effort legacy assembly;宿主可以通过 `capabilities()` 区分这种行为与 atomic +backend。 + +## Resume + +```ts +const resumed = agent.resumeSession('release-review', { + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); +``` + +`resumeSession` 恢复的是已保存的 session snapshot。它不同于 +`resumeRun`:用户继续一个已保存对话时用 `resumeSession`;只有存在中断 run 的 +checkpoint 时,才用 `resumeRun(runId)`。Resume 会在恢复任何 history 或 runtime +evidence 前先校验 snapshot schema。 + +## Memory And Sessions + +Session 持久化保存对话和可回放证据;memory 保存可复用任务事实。当你需要既可 +恢复、又能从重复任务中学习的工作流时,两者一起使用。 + +## Loop Checkpoints and Run Resumption + +配置了 `SessionStore` 后,agent loop 会在每个完成的 tool round 之后持久化一个 +`LoopCheckpoint`。边界策略很严格:checkpoint **只**在 tool round 之间产生, +绝不在 tool 执行中途。如果进程在某个 tool 执行时崩溃,那一轮的工作会在 resume +时丢失,由 LLM 从上一个 checkpoint 重新推演——在边界错误的一侧重跑一个非幂等 +tool(write、bash)比让 LLM 重新提问更糟。 + +`session.resumeRun(runId)`(Node)/ `session.resume_run(run_id)`(Python)—— +对应 core 的 `AgentSession::resume_run(checkpoint_run_id)`——会加载该 run ID 下 +最新的 checkpoint,并从最后一个边界回放 loop。由于 checkpoint 存放在共享 store +中,resume 可以发生在**任何**共享该 store 的节点上。累计计量会延续而不是从零 +重启:`total_usage` 和 `tool_calls_count` 从 checkpoint 继续累加。resume 出来的 +工作会分配一个新的 run ID;新旧 run 的关系是宿主元数据,框架不予解释。 + +已正常完成的 run 不通过 `resumeRun` 恢复;它们的最终状态应通过 `runs()`、 +`runEvents(runId)`、artifacts、verification reports 和 session snapshot 查看。 + +```ts +const result = await session.resumeRun('run-abc123'); +console.log(result.totalTokens); +``` + +```python +result = session.resume_run('run-abc123') +print(result.total_tokens) +``` + +当 session 没有配置 `sessionStore`(或给定 ID 下不存在 checkpoint)时, +`resume_run` 会拒绝。`SessionStore` 新增了 `save_loop_checkpoint` / +`load_loop_checkpoint` / `delete_loop_checkpoint`;file store 的写入是 +crash-atomic 的。`LoopCheckpoint::ensure_loadable()` 在反序列化之后立即被调用, +会拒绝来自未来的、不兼容 schema 版本的 checkpoint,因此 `resume_run` 和实时 +run 的 sink 都不会对一个无法读取的 checkpoint 采取行动。 + +参见 CHANGELOG `[3.3.0]`——"Loop checkpoints + run resumption"——以及 +`[3.4.0]` 的 "LoopCheckpoint::ensure_loadable()"。 + +## Workflow Checkpoints + +`WorkflowCheckpoint` 是 tool-round `LoopCheckpoint` 在上一层的 step 边界对应物: +它把已完成的编排 step 记入日志,使被中断的工作流从最长的已完成前缀恢复。它的 +字段是 `schema_version`、`workflow_id`、`steps` 和 `checkpoint_ms`,schema 由 +`WORKFLOW_CHECKPOINT_SCHEMA_VERSION` 常量固定。恢复的 run 会跳过已记录的 step, +只重新派发其余的。 + +`SessionStore` 新增了 `save_workflow_checkpoint` / `load_workflow_checkpoint` / +`delete_workflow_checkpoint`(默认 no-op;file store 以 crash-atomic 方式写入)。 +来自未来的、不兼容 schema 版本的加载会通过 +`WorkflowCheckpoint::ensure_loadable()` 被拒绝。 + +这与编排语法配套使用——参见 +[Orchestration](/guide/orchestration) 和 +[Multi-Machine](/guide/multi-machine)。 + +参见 CHANGELOG `[3.4.0]`——"WorkflowCheckpoint"。 + +## Resuming on a Different Node + +两种 checkpoint 类型都是可序列化的。配合共享的 `SessionStore` 和可插拔的 +executor,宿主就能在与启动节点**不同**的节点上恢复被中断的 run 或工作流—— +框架持有可序列化的契约,宿主持有放置(placement)与传输(transport)。 + +## Operational Notes + +包含私有 prompt、tool output 或路径的 store 不应公开提交。 diff --git a/website/docs/v6/zh/guide/providers.mdx b/website/docs/v6/zh/guide/providers.mdx new file mode 100644 index 00000000..6378e550 --- /dev/null +++ b/website/docs/v6/zh/guide/providers.mdx @@ -0,0 +1,105 @@ +--- +title: 'Provider 配置' +description: 'ACL provider 配置与环境变量注入' +--- + +# Provider 配置 + +A3S Code 从 ACL 读取运行时配置。配置源可以是 `.acl` 文件路径,也可以是 +内联 ACL 字符串。JSON 和旧 HCL 配置不属于当前配置面。 + +## 基本结构 + +```text +default_model = "provider/model-id" +max_parallel_tasks = 4 +auto_parallel = false + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "model-id" { + name = "Human readable model name" + tool_call = true + limit = { + context = 128000 + output = 4096 + } + } +} + +storage_backend = "file" +sessions_dir = ".a3s/sessions" +``` + +`apiKey` 与 `api_key` 都可用,`baseUrl` 与 `base_url` 都可用。仓库里只提交模板 +和非敏感默认值;真实 API Key、私有端点和账号相关模型名应放在环境变量或密钥管理系统中。 + +## Provider Families + +内置 factory 覆盖三条路径: + +| Provider name | Client path | 说明 | +| ---------------------------- | -------------------------- | ---------------------------------------------- | +| `anthropic` / `claude` | Anthropic client | 使用配置中的模型 id 和可选 provider base URL。 | +| `openai` / `gpt` | OpenAI-compatible client | 用于兼容 OpenAI Chat Completions 的端点。 | +| `glm` / `zhipu` / `bigmodel` | Zhipu-compatible client | Key 和 base URL 仍应从环境变量注入。 | +| 其它 provider name | OpenAI-compatible fallback | 适合私有或自托管的 OpenAI-compatible 服务。 | + +运行时不硬编码模型名。`default_model` 和每个 session 的 `model` 都是 +`provider/model-id` 形式的标识符,必须能匹配你在 provider block 里声明的模型。 + +## 委派控制 + +```text +agent_dirs = ["./.a3s/agents"] + +auto_delegation { + enabled = true + auto_parallel = false + allow_manual_delegation = true + min_confidence = 0.72 + max_tasks = 4 +} +``` + +`max_parallel_tasks` 限制有边界的 sibling fan-out。`auto_delegation.enabled` +开启自动 subagent 委派。顶层 `auto_parallel = false` 会覆盖 +`auto_delegation.auto_parallel`,只关闭自动并行子 agent fan-out;手动 +`task` 与 `parallel_task` 仍可用。设置 `allow_manual_delegation = false` 时, +这两个模型可见的子 agent 工具都会从 session 工具表面移除。 + +## 存储 + +```text +storage_backend = "memory" +storage_backend = "file" +sessions_dir = ".a3s/sessions" +``` + +短测试使用 memory;通过 ACL 加载的可恢复本地 session 使用 +`storage_backend = "file"` 和 `sessions_dir`。`storage_url` 会被解析为自定义 +存储元数据,但它本身不会创建文件型 session store。SDK 宿主也可以直接传 +`sessionStore: new FileSessionStore(...)` / +`opts.session_store = FileSessionStore(...)`。 + +## 私有 Provider 检查 + +真实 provider 冒烟测试应通过 `A3S_CONFIG_FILE` 指向本地、git-ignored 的 ACL 文件。 +不要把 provider 值复制进命令、日志、文档、PR 或已提交 fixture。 + +```bash +A3S_CONFIG_FILE=/path/to/local/config.acl \ + scripts/real_config_env_integration.sh +``` + +SDK 对齐由单独的真实 provider 检查脚本覆盖: + +```bash +A3S_CONFIG_FILE=/path/to/local/config.acl \ + scripts/sdk_real_config_env_integration.sh +``` + +把 provider 证据保存在发布记录或 CI artifact 中,不写进公开文档。完整本地 API +表面见 [API Contract](/guide/api-contract)。 diff --git a/website/docs/v6/zh/guide/rfcs/_meta.json b/website/docs/v6/zh/guide/rfcs/_meta.json new file mode 100644 index 00000000..04371f2a --- /dev/null +++ b/website/docs/v6/zh/guide/rfcs/_meta.json @@ -0,0 +1 @@ +["workspace-remote-git"] diff --git a/website/docs/v6/zh/guide/rfcs/workspace-remote-git.mdx b/website/docs/v6/zh/guide/rfcs/workspace-remote-git.mdx new file mode 100644 index 00000000..6c5fe9e9 --- /dev/null +++ b/website/docs/v6/zh/guide/rfcs/workspace-remote-git.mdx @@ -0,0 +1,500 @@ +--- +title: 'RFC:远端 Workspace Git 后端' +description: '为非本地 workspace 后端提供 git 操作的 HTTP/JSON 协议' +--- + +# RFC:远端 Workspace Git 后端 + +| 字段 | 取值 | +| ---- | ------------------------------------------------------------------- | +| 状态 | **已实现** v2.6.x(`crates/code/core/src/workspace/remote_git.rs`) | +| 归属 | `crates/code` workspace 子系统 | +| 关联 | S3 / 非本地 workspace 加固 Phase 4.1 | +| 相关 | `S3WorkspaceBackend`、`WorkspaceGit*` trait 族 | + +本文档作为协议规范保留——讲此协议的客户端与服务端应当严格匹配下方的 +线协议形态。后续修订应作为独立的 amendment 文档发布,而非原地修改这 +份 RFC,以便"实际 ship 出去的是什么"这条历史线索可审计。 + +本 RFC 提出一套协议和 Rust 客户端,让 `a3s-code` 的内置 `git` 工具能够 +跑在非本地文件系统的 workspace 之上——首先服务 S3,将来覆盖容器 / DFS +等无法承载 `.git` 目录的后端。 + +## 1. 动机 + +Workspace 抽象层已经通过 `WorkspaceFileSystem` 把内置工具与具体文件系统 +解耦。对 `bash`、`grep`、`glob`、`git` 这四个工具,我们使用**能力门控**: +后端无法提供的能力对应的工具就不会被注册,模型根本看不到它。 + +这对 `bash` 是合理的(对象存储确实没法跑 shell),对 `grep` 是可接受的 +(我们后来加了 `LIST + GET + regex` 的降级路径)。但对 `git` 是**痛点**: +很多 a3s-code 工作流期望查询分支/提交状态、diff 工作区、创建分支、stash +变更。如果只对本地会话开放 `git`,云端 workspace 就成了二等公民。 + +本方案是引入一个**远端 `WorkspaceGit` 后端**,把这些操作通过网络委托 +给宿主运维的外部服务。该服务持有真实的工作区(或基于 libgit2 的实现), +对外暴露一小套 HTTP API。a3s-code 客户端讲这套 API,对工具层来说它 +就是另一个 `WorkspaceGit` provider。 + +```text + ┌──────────────────┐ + │ a3s-code │ + 模型 │ │ + ──────► │ git 工具 │ HTTP/JSON + │ │ │ ┌────────────────┐ + │ ▼ │ │ │ + │ WorkspaceGit ───┼──►│ gitserver │ + │ (RemoteGit…) │ │ (libgit2 / sh) │ + │ │ │ │ + │ WorkspaceFs ────┼──►│ (S3, etc.) │ + └──────────────────┘ └────────────────┘ +``` + +## 2. 非目标 + +- **托管 gitserver。** 本 RFC 只定义客户端与协议,服务端的实现 + (libgit2 封装、shell 外调、Gitea API 适配等)超出范围。 +- **替换本地 git。** 当 `WorkspaceFs` 是 `LocalWorkspaceBackend` 时, + 现有的 `LocalWorkspaceBackend` 的 `WorkspaceGit` 实现仍是默认。 + 宿主按会话自由组合。 +- **push / pull。** `WorkspaceGit` trait 对远程是只读的 + (`list_remotes` 返回已配置的远程;不暴露 `git push` / `git fetch`)。 + 如果有工具需要,未来 RFC 再加。 +- **Worktrees。** Worktree 是本地文件系统概念;远端后端**不**实现 + `WorkspaceGitWorktreeProvider`。详见 §8。 + +## 3. 协议选择 — HTTP/JSON + +**结论:** HTTP/JSON,不用 gRPC。 + +| 评估维度 | HTTP/JSON | gRPC | +| ---------------- | ------------------------ | ------------------------------ | +| 现有依赖 | `reqwest` 已在树中 | `tonic` + proto 工具链全是新增 | +| schema 严格度 | 手写 `serde` 类型 | `.proto` 强类型 | +| 测试 mock 难度 | `wiremock` 就够 | 需要 gRPC mock 基础设施 | +| 运维可调试性 | `curl` 直接打 | `grpcurl`(普及度低) | +| 流式 | chunked / SSE | 原生双向流 | +| 操作数量 | ~12 个 op,全部请求/响应 | 数量同上;流式罕用 | +| 服务端实现自由度 | 直接包 `git` 或 `gitea` | 必须实现 gRPC server | + +操作面 ~12 个 RPC、请求响应都是扁平结构。流式只对 `log` 和 `diff` 有 +点用,而这两个客户端已经有上限。HTTP/JSON 在 mock 与运维调试上的优势 +明显压过 gRPC 的 schema 优势。如果操作面剧增或流式变成关键路径, +再回头考虑 gRPC。 + +我们对**所有操作都用 `POST`**:git 操作本质是命令式而非资源 CRUD, +请求体让加字段(兼容方式)变得轻松——`GET` 强迫所有信息塞 query string。 + +## 4. 仓库标识 + +客户端知道自己在操作哪个仓库,服务端需要路由。仓库标识写在 **URL 路径** +里: + +``` +POST /v1/repos/{repo_id}/git/ +``` + +`repo_id` 是宿主与 gitserver 运维方协商的、不透明的、URL 安全字符串。 +典型值有: + +- `users/{user_id}/sessions/{session_id}`(与 S3 workspace prefix 1:1) +- UUID +- 服务端的工作区路径 + +客户端把 `repo_id` 当不透明字符串;服务端负责映射到实际的工作区/裸仓。 + +## 5. 端点参考 + +全部 `POST`,全部 `application/json` 收发。字段命名 `snake_case`,匹配 +Rust serde 默认值。 + +### 5.1 Status — `WorkspaceGit::status` + +``` +POST /v1/repos/{repo_id}/git/status +→ 200 { + "branch": "main", + "commit": "abc123...", + "is_worktree": false, + "is_dirty": true, + "dirty_count": 3 + } +→ 404 {"error":{"code":"REPO_NOT_FOUND", ...}} +``` + +### 5.2 Log — `WorkspaceGit::log` + +``` +POST /v1/repos/{repo_id}/git/log +{"max_count": 10} +→ 200 { + "commits": [ + {"id":"abc...", "message":"feat: ...", "author":"Alice ", "date":"2026-05-19T..."} + ] + } +``` + +### 5.3 List Branches — `WorkspaceGit::list_branches` + +``` +POST /v1/repos/{repo_id}/git/branches +→ 200 {"branches":[{"name":"main", "is_current":true}, ...]} +``` + +### 5.4 Create Branch — `WorkspaceGit::create_branch` + +``` +POST /v1/repos/{repo_id}/git/branches/create +{"name":"feat/x", "base":"main"} +→ 201 {} +→ 409 {"error":{"code":"BRANCH_EXISTS", ...}} +→ 404 {"error":{"code":"BASE_NOT_FOUND", ...}} +``` + +### 5.5 Checkout — `WorkspaceGit::checkout` + +``` +POST /v1/repos/{repo_id}/git/checkout +{"refspec":"feat/x", "force":false} +→ 200 {"stdout":"Switched to branch 'feat/x'"} +→ 409 {"error":{"code":"WORKING_TREE_DIRTY", ...}} +``` + +### 5.6 Diff — `WorkspaceGit::diff` + +``` +POST /v1/repos/{repo_id}/git/diff +{"target": null} // null 表示工作区 vs index +{"target": "main"} // 对指定 ref diff +→ 200 {"diff":"", "truncated": false} +``` + +`truncated` 为 `true` 表示服务端截断了 body — 见 §9。客户端会在 +`git diff` 工具结果里把这个信号透出去。 + +### 5.7 List Remotes — `WorkspaceGit::list_remotes` + +``` +POST /v1/repos/{repo_id}/git/remotes +→ 200 {"remotes":[{"name":"origin", "url":"git@github.com:...", "direction":"fetch"}]} +``` + +### 5.8 Is Repository — `WorkspaceGit::is_repository` + +``` +POST /v1/repos/{repo_id}/git/exists +→ 200 {"is_repository": true} +``` + +与"仓库不存在"(404)有别:服务端可能允许 `repo_id` 映射到非 git +目录。`is_repository` 让客户端不依赖 404 就能探测意图。 + +### 5.9 List Stashes — `WorkspaceGitStashProvider::list_stashes` + +``` +POST /v1/repos/{repo_id}/git/stashes +→ 200 {"stashes":[{"index":0, "message":"WIP on main: ..."}]} +``` + +### 5.10 Stash — `WorkspaceGitStashProvider::stash` + +``` +POST /v1/repos/{repo_id}/git/stashes/create +{"message":"wip", "include_untracked":true} +→ 201 {} +→ 409 {"error":{"code":"NOTHING_TO_STASH", ...}} +``` + +## 6. 认证 + +客户端支持两种传输认证模式,按会话配置: + +1. **Bearer token(默认)。** `Authorization: Bearer `。Token + 下发是宿主的责任(例如同一身份层签发的短期 JWT,复用 S3 访问的 + 门禁)。 +2. **mTLS。** 在 backend config 上设置 `client_cert_pem` 和 + `client_key_pem` 两个路径。客户端在构造时读两个文件,拼接后交给 + `reqwest::Identity::from_pem`。`rustls-tls` 后端要求密钥是 PKCS#8 + PEM 格式。只设一边会在构造期 fail-closed 报错。 + +两者可以同时启用——深度防御的部署直接两个都设。 + +无认证模式(本地开发场景)通过把 token 设为空、不设 mTLS 来开启; +客户端在构造时会 `tracing::warn!` 一条以让这个状态可见。 + +## 7. 错误模型 + +HTTP 状态码是**传输信号**。错误**类别**在 JSON body 里: + +```json +{ + "error": { + "code": "BRANCH_EXISTS", + "message": "branch 'feat/x' already exists" + } +} +``` + +客户端映射: + +| HTTP | 默认行为 | +| ------- | ------------------------------------------- | +| 200/201 | `Ok(...)` | +| 400 | `Err(anyhow!("bad request: {message}"))` | +| 401/403 | `Err(anyhow!("auth failed: {message}"))` | +| 404 | `Err(anyhow!("not found: {message}"))` | +| 409 | 类型化 conflict — 见下 | +| 5xx | `Err(anyhow!("gitserver internal: {...}"))` | + +客户端为可恢复 conflict 引入一个类型化错误: + +```rust +#[derive(Debug, Clone, thiserror::Error)] +#[error("remote git conflict: {code}: {message}")] +pub struct RemoteGitConflict { + pub code: String, + pub message: String, +} +``` + +希望从 `BRANCH_EXISTS` / `WORKING_TREE_DIRTY` / `NOTHING_TO_STASH` +恢复的工具用 `anyhow::Error::downcast_ref::()` +取出来——这与 `edit` / `patch` 在 S3 CAS 路径上用 `WorkspaceVersionConflict` +的模式相同。 + +协议正式定义的错误码(可扩展): + +| Code | 来源 | +| -------------------- | --------------------------------------------- | +| `REPO_NOT_FOUND` | `repo_id` 未注册 | +| `NOT_A_REPOSITORY` | 路径存在但不是 git 仓库 | +| `BRANCH_EXISTS` | `create_branch` 同名冲突 | +| `BRANCH_NOT_FOUND` | checkout / diff 目标缺失 | +| `BASE_NOT_FOUND` | `create_branch` 的 base ref 缺失 | +| `WORKING_TREE_DIRTY` | `checkout` 会丢工作区改动(且 `force=false`) | +| `NOTHING_TO_STASH` | 干净工作区上的 stash | +| `RATE_LIMITED` | 服务端限流(自定义阈值) | + +## 8. 可选 Trait + +`WorkspaceGit` 完整实现。 +`WorkspaceGitStashProvider` 实现。 +`WorkspaceGitWorktreeProvider` **故意不实现**。Worktree 是本地文件系统 +概念,对远端服务映射不干净: + +- "在路径 X 创建一个 worktree"——客户端没有路径概念;服务端的路径 + 布局对客户端是不透明的。 +- 工具里用 worktree 隔离的工作流(多个 agent 并行跑在隔离副本上), + 在云端的更优形式是**多个会话,每个会话有自己的 `repo_id`**,而不是 + 在客户端模拟一个本地文件系统特性。 + +依赖 `WorkspaceGitWorktreeProvider` 的工具会从 `services.git_worktree()` +拿到 `None`,并报"worktrees unavailable on remote git workspaces"。 + +## 9. 大小与成本上限 + +沿用 S3 后端的设防方式,远端 git 客户端强制几个客户端侧上限,避免 +模型触发无界响应: + +| 配置项 | 默认值 | 作用对象 | +| ------------------------ | ------ | ----------------------------- | +| `max_diff_bytes` | 1 MiB | `diff` 响应 body | +| `max_log_entries` | 200 | `log` 的 `max_count` 上限 | +| `request_timeout` | 30 s | 每次 HTTP 调用 | +| `operation_timeout` (WS) | 60 s | 叠加在 `WorkspaceServices` 层 | + +期望服务端也尊重这些上限——客户端会把相关上限传到请求里(例如 +`max_log_entries`),并把服务端返回的 `diff.truncated` 透出去,让 +工具能提示"diff 太大被截断,请缩小目标"。 + +## 10. Rust 客户端设计 + +```rust +// crates/code/core/src/workspace/remote_git.rs + +#[derive(Debug, Clone)] +pub struct RemoteGitBackendConfig { + pub base_url: String, // https://git.example.invalid + pub repo_id: String, // path-segment 安全 + pub bearer_token: Option, + pub client_cert_pem: Option, // mTLS + pub client_key_pem: Option, + pub request_timeout: Option, // 默认 30s + pub max_diff_bytes: Option, // 默认 1 MiB + pub max_log_entries: Option, // 默认 200 +} + +#[derive(Debug, Clone)] +pub struct RemoteGitBackend { + http: reqwest::Client, + base_url: String, + repo_id: String, + max_diff_bytes: u64, + max_log_entries: usize, +} + +#[async_trait] +impl WorkspaceGit for RemoteGitBackend { /* 见 §5 */ } + +#[async_trait] +impl WorkspaceGitStashProvider for RemoteGitBackend { /* 见 §5 */ } +``` + +组合工厂沿用 S3 的形式: + +```rust +impl WorkspaceServices { + /// 在已有文件系统后端之上挂一个远端 git provider。 + pub fn with_remote_git( + self: Arc, + cfg: RemoteGitBackendConfig, + ) -> Arc { ... } +} +``` + +或者作为 S3 + 远端 git 工作区的顶层便利方法: + +```rust +pub fn s3_with_remote_git( + s3: S3BackendConfig, + git: RemoteGitBackendConfig, +) -> Arc { ... } +``` + +接线沿用现有的 builder 模式: + +```rust +let backend = Arc::new(RemoteGitBackend::new(cfg)); +let git: Arc = backend.clone(); +let stash: Arc = backend; + +WorkspaceServices::builder(workspace_ref, fs) + .file_system_ext(fs_ext) // S3 ETag CAS + .git(git) + .git_stash(stash) + // 不设 git_worktree — 见 §8 + .operation_timeout(Duration::from_secs(60)) + .build() +``` + +之后能力门控会自动注册 `git` 工具。 + +## 11. 每调用可观测性 + +每次 HTTP 调用发一个 `tracing::debug!` 事件,字段形态与 +`S3WorkspaceBackend::emit_s3_call_event` 一致(见 +`crates/code/core/src/workspace/s3.rs`): + +| 字段 | 示例 | +| ------------- | ----------------------------- | +| `op` | `git.status`、`git.diff`、... | +| `repo_id` | `sessions/example` | +| `outcome` | `ok` \| `error` | +| `status` | HTTP 状态码 | +| `bytes` | 响应 body 长度 | +| `duration_ms` | 墙钟耗时 | + +已经 meter S3 成本的宿主可以把同一个 subscriber 接上来 meter gitserver +成本——不引入新依赖,也不要新接口。 + +## 12. 组合示例 + +### S3 workspace + 远端 git + +```rust +let ws = WorkspaceServices::s3_with_remote_git( + S3BackendConfig::new("workspace", "sessions/example", access_key, secret_key) + .endpoint("https://s3.example.invalid") + .force_path_style(true) + .enable_search(true), + RemoteGitBackendConfig::new( + "https://git.example.invalid", + "sessions/example", + ) + .bearer_token(token), +); + +let session = agent + .session_builder("s3://workspace/sessions/example") + .options(SessionOptions::new().with_workspace_backend(ws)) + .build() + .await?; +``` + +该会话注册的工具:`read`、`write`、`edit`、`patch`、`ls`、`grep`、`glob`、 +`git`。(`bash` 仍然隐藏——对象存储跑不了 shell。) + +### 本地文件系统 + 远端 git(混合) + +CI 跑在本地 checkout 上,但宿主想把 git 操作路由进沙箱化服务 +(例如做审计或限流)时有用。 + +```rust +let local = WorkspaceServices::local("/workspaces/repo"); +let ws = local.with_remote_git(remote_cfg); // 覆盖本地的 git provider +``` + +## 13. 未决问题 + +这些应在 Phase 4.2(实现)开始前敲定。 + +1. **Diff 方言。** 本地后端返回原始 `git diff` 的 stdout。远端 API 是否 + 应该强制具体 diff 方言(POSIX `diff -u`?libgit2 的变体?),还是 + 透传服务端产出?建议:透传,文档要求服务端必须生成 unified diff。 + +2. **并发操作。** 两个客户端同时访问同一 `repo_id`(一个跑 `checkout`、 + 一个跑 `diff`),服务端是否要串行?建议:服务端按 `repo_id` + 做串行;写进文档;客户端不在冲突上重试。 + +3. **长任务。** 大树上的 `checkout` 可能超过 `request_timeout`。协议是 + 否要支持轮询 / 异步 job 模式?建议:先不做。设合理的超时;我们 + 面向的(每会话沙箱)工作区不大。 + +4. **Hooks。** 服务端可能装了 pre-commit / pre-push hooks。响应里要 + 不要给一个"hook 输出"通道?建议:当非空时把 hook 的 stderr 塞进 + `checkout` / `stash` 的响应;hook 失败映射到 HTTP 422 + + `HOOK_FAILED`。 + +5. **schema 版本化。** 第一版端点放 `/v1/`。什么时候升 `/v2/`? + 建议:只在请求/响应 shape 出现不兼容变更时;新增字段保留在 + `/v1/`(客户端要忽略未知字段)。 + +6. **参考实现。** 是否要在本仓库放一个最小参考 gitserver + (比如 libgit2 + Rust)?建议:仓库外。客户端 + 协议足够;参考 + 服务归运维方。 + +## 14. 范围外(未来 RFC) + +- **push / fetch 到上游。** 让"agent 改完代码 push 回去"的工作流成立, + 涉及凭证下发。 +- **稀疏 / 局部 checkout。** 大型 monorepo 必要;当前面假定整个仓库 + 全量落地到服务端。 +- **流式 `log` / `diff`。** 当响应稳定超出 `max_diff_bytes` 时必要, + 会让 gRPC 的判断重新被讨论。 +- **Hooks 管理。** 通过客户端列举 / 配置服务端 hook。 + +## 15. 实现笔记(v2.6.x 已发布) + +最初草案给的是 8 步实施提纲;实际发布出去的(Phase 4.2 + 后续 Phase 5.x)如下: + +1. `RemoteGitBackend` / `RemoteGitBackendConfig` / `RemoteGitConflict` 位于 + `crates/code/core/src/workspace/remote_git.rs`。最终没有加 + `remote-git` cargo feature——`reqwest` 已经在依赖里,模块无条件编译。 +2. `WorkspaceGit` 与 `WorkspaceGitStashProvider` 完整实现; + `WorkspaceGitWorktreeProvider` 故意不实现(见 §8)。 +3. `WorkspaceServices::with_remote_git` 是公开挂载点。内部 helper + `with_git_provider`(v2.6.x 一个后续提交里加上)用 struct literal + 显式拷贝字段——确保 `WorkspaceServices` 未来加字段时装饰器不会静默丢失。 +4. 除 bearer token 外支持 mTLS(`client_cert_pem` + `client_key_pem`), + 在 Phase 5.2 一次后续提交里 ship;同一个提交把上方 §6 也更新到了 + 已实现状态。 +5. `diff` 客户端侧防 OOM:HTTP body 流式累积,硬上限 + `max_diff_bytes * 4`(Phase 6.2)。soft `max_diff_bytes` 显示截断 + 在 JSON decode 之后照旧生效。 +6. 测试面:`remote_git.rs` 里 25+ 个 wiremock 单元测试 + 1 个端到端 + 驱动内置 `git` 工具的集成测试;workspace 通用 conformance suite + 在 Phase 6.3 加入。 +7. README 与 CHANGELOG 已更新;TLS 后端选择与 AWS SDK 一致用 + rustls-tls。 +8. SDK 暴露在 Phase 5.1 完成(Node + Python)——草案曾说"trait 稳定之 + 后再补",结果是 Phase 4.2 完成两个提交后就上了。 diff --git a/website/docs/v6/zh/guide/security.mdx b/website/docs/v6/zh/guide/security.mdx new file mode 100644 index 00000000..817370fe --- /dev/null +++ b/website/docs/v6/zh/guide/security.mdx @@ -0,0 +1,39 @@ +--- +title: '安全' +description: '权限策略、HITL、hook 与验证关卡' +--- + +# 安全 + +A3S Code 在创建 session 时暴露安全控制,并通过 session hook 暴露生命周期回调。宿主直接调用 `session.tool()`、`session.bash()` 等 API 时,应把它们视为宿主侧特权操作;是否把这些能力暴露给用户,由宿主应用自己判断。 + +委派子运行会继承所选 subagent 或 worker spec 的有边界权限。使用 `confirmationInheritance` 控制子运行的 Ask 决策,并把高风险发布命令放在显式 policy 后面。 + +```ts +const session = agent.session('/repo', { + permissionPolicy: { + deny: ['bash(rm -rf*)', 'write(**/.env*)'], + ask: ['bash(git push*)', 'bash(npm publish*)'], + allow: ['read(*)', 'grep(*)', 'glob(*)', 'bash(cargo test*)'], + defaultDecision: 'ask', + enabled: true, + }, +}); +``` + +Hook API 的管理面包括注册、计数和卸载: + +```ts +session.registerHook( + 'observe-secret-read', + 'pre_tool_use', + { pathPattern: '**/.env*' }, + { priority: 100 }, + () => ({ action: 'continue' }), +); + +console.log(session.hookCount()); +session.unregisterHook('observe-secret-read'); +``` + +发布流程应要求测试、包检查、CI 和 provider 验证证据。 diff --git a/website/docs/v6/zh/guide/sessions.mdx b/website/docs/v6/zh/guide/sessions.mdx new file mode 100644 index 00000000..b0d53d24 --- /dev/null +++ b/website/docs/v6/zh/guide/sessions.mdx @@ -0,0 +1,256 @@ +--- +title: '会话' +description: '创建、流式输出、恢复和保存工作区绑定的 session' +--- + +# 会话 + +`Agent` 持有配置和 provider 状态。`Session` 把这个 agent 绑定到一个工作区和一次对话生命周期。 + +```ts +import { Agent } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/repo', { + model: 'provider/model-id', + planningMode: 'enabled', + goalTracking: true, + autoDelegation: { enabled: true, maxTasks: 4 }, + autoParallel: false, +}); +``` + +## Rust 构建路径 + +Rust session 构建以异步为先,因为默认 memory、文件型 store、queue、trajectory +recording 与 MCP discovery 都可能需要 I/O: + +```rust +let session = agent + .session_builder("/repo") + .options(options) + .build() + .await?; +``` + +`session_async`、`resume_session_async`、`session_for_agent_async` 与 +`session_for_worker_async` 是直接的异步入口。Node 和 Python 保留现有 factory +命名,由 native binding 内部委托给同一个异步构建内核。 + +同步 Rust `Agent::session` 是严格兼容路径:只接受已经显式初始化好的资源,绝不 +启动或阻塞 async runtime。仍需初始化的默认/文件 memory store、文件 session +store、queue、trajectory recorder,以及 `SessionOptions` 中的任何宿主 MCP +manager 都会返回 `CodeError::AsyncSessionBuildRequired`;session-option MCP +capability discovery 始终是异步的。应改用 builder,不要捕获错误后悄悄换 backend。 +同步路径只能继承 agent 初始化时已经缓存的 global MCP tools。 + +`planningMode` 是显式三态:`'auto'` 使用默认结构化预分析,`'enabled'` +强制 planning,`'disabled'` 在低延迟调用中关闭 planning。旧的布尔 +`planning` 选项仍保留兼容。 + +Planning 会写入 run 级状态。宿主应用可以把这些状态渲染成 TaskList,并随着运行事件更新每一项,而不是从文本 token 里猜进度。 + +## Single-Flight 操作 + +同一个 session 同时只准入一个会影响 transcript 的操作。`send`、`stream`、它们 +的 attachment variant、slash command 与 `resumeRun` 共用 fail-fast admission +gate。重叠调用会在读取历史或派发命令前返回 `CodeError::SessionBusy`,不会排队 +等待当前操作。 + +开始下一次对话操作前,应等待 active result、把 stream 消费到结束,或先取消它。 +即使公开 stream handle 被丢弃或中止,runtime 也会在 producer 真正停止前继续 +持有 lease。直接宿主工具 helper 不改变 transcript,因此不占用这把 lease。 + +Node 与 Python 的 stream iterator 会在 terminal boundary 等待这段 lifecycle +清理。iterator 完整消费并报告结束后,立即开始下一次对话操作不会继承上一个 +stream 留下的过期 busy 状态。 + +## Send + +```ts +const result = await session.send('检查仓库并列出发布阻塞项'); + +console.log(result.text); +console.log(result.totalTokens); +console.log(result.verificationStatus); +``` + +## Stream + +```ts +const stream = await session.stream('运行聚焦测试并解释失败'); + +while (true) { + const { value: event, done } = await stream.next(); + if (done) break; + if (!event) continue; + + if (event.text) process.stdout.write(event.text); + if (event.toolName) console.log('tool:', event.toolName); +} +``` + +每个 SDK event 都是 `EventEnvelopeV1` 投影,包含 `version === 1`、开放的 `type` +字符串、完整 `payload` 与可选 `metadata`。`text`、`toolName` 等 convenience field +由 envelope 统一派生。消费端应保留 default branch,并为未来 event type 保存原始 +payload。 + +## Side Questions + +SDK 没有专用的临时提问 helper。要提出临时问题,可以先快照当前历史,再把它 +显式传给 `send` 或 `stream`。显式 history 只服务这一次调用,不会把答案写回 +session 历史。 + +```ts +const snapshot = session.history(); +const answer = await session.send('这个 session 已经看过哪些文件?', snapshot); + +console.log(answer.text); +console.log(session.history().length === snapshot.length); +``` + +## Resume + +```ts +import { Agent, FileSessionStore } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.resumeSession('release-review', { + sessionStore: new FileSessionStore('./.a3s/sessions'), +}); +``` + +使用 session store 时设置 `autoSave: true`,或显式调用 `await session.save()`。 +这里恢复的是已保存的 session snapshot;中断 run 的 checkpoint 通过 +`session.resumeRun(runId)` 恢复。参见[持久化](/guide/persistence)。 + +## Lifecycle and Close + +`session.close()` 是一次完整的优雅停止。首次调用会把 session 切换到 +**closed** 状态——之后的 `send`/`stream` 调用会以 `CodeError::SessionClosed` +快速失败,而不会启动新 run——然后取消正在执行的 run、所有正在进行的委派子 +agent 任务,以及所有挂起的 human-in-the-loop 确认。后续调用是 no-op,且保证 +不会 panic。用 `session.isClosed()`(Node)/ `session.is_closed()`(Python) +查询关闭状态。 + +```ts +session.close(); +if (session.isClosed()) { + // send/stream 现在会以 CodeError::SessionClosed 拒绝 +} +``` + +```python +session.close() +if session.is_closed(): + # send/stream 现在会以 CodeError::SessionClosed 拒绝 + pass +``` + +### Cancellation token + +每个 run 都通过 `child_token()` 从同一个 session 级父 token 派生出自己的 +逐操作取消 token,因此 `close()` 会一次性级联到所有正在进行的工作。需要原始 +token 的嵌入方——例如把它接入宿主侧的 `select!`,或者绕过 `close()` 的 +run-store 和 hook 副作用直接中止 session——可以通过 +`AgentSession::session_cancel_token()` 克隆它。 + +### Agent-side registry + +所属的 `Agent` 通过 `Weak` 引用跟踪它的存活 session(惰性回收),这样控制面 +就能在不持有 session 句柄的情况下驱动生命周期: + +- `Agent::list_sessions()` 返回存活的 session ID(已排序,稳定)。 +- `Agent::close_session(id)` 按 ID 关闭单个 session——与 + `AgentSession::close()` 相同的清理流程,从带外调用。 +- `Agent::close()` 关闭每个存活 session 并拆除 agent 持有的后台资源(同时 + 断开全局 MCP 连接)。返回后,新的 `session` / `resumeSession` 调用会以 + `CodeError::SessionClosed` 快速失败。 +- `Agent::is_closed()` 报告 agent 自身是否已被关闭。 + +```ts +const ids = await agent.listSessions(); +await agent.closeSession(ids[0]); +await agent.close(); // 关闭所有剩余 session + 全局 MCP +console.log(agent.isClosed()); +``` + +```python +ids = agent.list_sessions() +agent.close_session(ids[0]) +agent.close() # 关闭所有剩余 session + 全局 MCP +print(agent.is_closed()) +``` + +参见 CHANGELOG `[3.3.0]`——"Session / Agent lifecycle control"。 + +## Host Identity Labels + +`SessionOptions` 携带四个不透明的身份字段,宿主可以在创建 session 时附加。 +框架只负责传递它们——从不解释或强制执行。它们会被传播进 `SessionData`、 +hooks 和 traces,并在 resume 时恢复,因此宿主可以据此驱动多租户聚合、计费 +和分布式 tracing: + +| Node (camelCase) | Python (snake_case) | 含义 | +| ----------------- | ------------------- | ---------------------------------------- | +| `tenantId` | `tenant_id` | 多租户标签 | +| `principal` | `principal` | 触发 session 的用户 / 服务 | +| `agentTemplateId` | `agent_template_id` | session 实例化所基于的 agent 模板 / 定义 | +| `correlationId` | `correlation_id` | 分布式 trace 关联 id | + +```ts +const session = agent.session('/repo', { + tenantId: 'tenant-example', + principal: 'principal-example', + agentTemplateId: 'agent-template-example', + correlationId: 'trace-example', +}); +``` + +```python +opts = SessionOptions() +opts.tenant_id = 'tenant-example' +opts.principal = 'principal-example' +opts.agent_template_id = 'agent-template-example' +opts.correlation_id = 'trace-example' +session = agent.session('/repo', opts) +print(session.tenant_id, session.principal) +``` + +参见 CHANGELOG `[3.3.0]`——"Host-provided identity labels"。 + +## Run Replay + +每次 `send()` 或 `stream()` 都会创建 run 记录。应用可以用这些记录做 UI 状态、审计、回放、取消和测试断言: + +```ts +const runs = await session.runs(); +const latest = runs.at(-1); + +if (latest) { + console.log(await session.runSnapshot(latest.id)); + console.log(await session.runEvents(latest.id)); +} +``` + +`currentRun()` 用来读取调用当下的 current run。`send()` 或 `stream()` 仍在 +执行时,可以把它的 `id` 传给 `cancelRun(id)` 请求取消。空闲时,`currentRun()` +可能返回 `null`,也可能保留一个 run snapshot;已完成历史应使用 `runs()`, +取消前必须检查 `status`: + +```ts +const current = await session.currentRun(); +if (current?.id && current.status === 'running') { + await session.cancelRun(current.id); +} +``` + +## Agent Definitions + +`sessionForAgent()` 应用一个命名的 agent 定义,来源是内置 agents、`.a3s/agents` 或配置的 `agentDirs`。 + +```ts +const session = agent.sessionForAgent('/repo', 'explore', ['./agents'], { + planningMode: 'auto', +}); +``` diff --git a/website/docs/v6/zh/guide/skills.mdx b/website/docs/v6/zh/guide/skills.mdx new file mode 100644 index 00000000..25082f43 --- /dev/null +++ b/website/docs/v6/zh/guide/skills.mdx @@ -0,0 +1,46 @@ +--- +title: 'Skills' +description: '提示时技能、内联技能与 Skill 工具' +--- + +# Skills + +Skills 是可复用指令。文件型 skill 目录、inline skills、显式 registry 和 +`search_skills` 使用同一套 discovery 路径。A3S Code 不再内置默认 skills; +SDK 仍接受 `builtinSkills: true`,但它只是兼容 no-op。 + +```ts +const session = agent.session('/repo', { + builtinSkills: true, + skillDirs: ['/repo/.a3s/skills'], + inlineSkills: [ + { + name: 'strict-release-review', + kind: 'instruction', + content: '始终先列 blocker,再列可选改进。', + }, + ], +}); +``` + +`builtinSkills` 不会添加默认 skills。可复用行为应放进项目 skill 文件、 +inline skills 或 agent 定义。Skill 文件是带 frontmatter 的 Markdown。使用 +`skillDirs` 加载技能文件,使用 `agentDirs` 加载 worker/subagent 定义。可用 +`allowed-tools` 限定 Skill 调用期间的工具,例如 `read(*), grep(*), bash(cargo test*)`。 + +`search_skills` 用于查找相关 skill: + +```ts +await session.tool('search_skills', { + query: 'release blockers', + limit: 5, +}); +``` + +带 `allowed-tools` frontmatter 的 Markdown skill file 和 inline skill 都能被发现。skill 管理现在由 SDK 或文件系统完成,不再作为模型可见的管理工具暴露。 + +当 skill 通过 `Skill` 工具被调用时,`allowed-tools` 是 fail-secure 的:省略 +frontmatter 不会授予任何工具。请显式声明最小可用工具集。普通 session 的工具调用 +默认不会被 active skill 限制,除非显式打开 +`enforceActiveSkillToolRestrictions` / `enforce_active_skill_tool_restrictions` +兼容旧行为。 diff --git a/website/docs/v6/zh/guide/tasks.mdx b/website/docs/v6/zh/guide/tasks.mdx new file mode 100644 index 00000000..317d95a9 --- /dev/null +++ b/website/docs/v6/zh/guide/tasks.mdx @@ -0,0 +1,174 @@ +--- +title: '任务' +description: '使用 task、parallel_task 与 subagent 进行手动和自动委派' +--- + +# 任务 + +常规多智能体路径是 `task` 与 `parallel_task` 这一组核心工具。它们隔离子运行上下文,并向父 agent 返回紧凑结果,而不是完整 transcript。 + +同一个委派核心也驱动自动 subagent 委派。需要运行时主动为高置信工作启动 +specialist 子 agent 时启用它;如果只想让自动委派串行执行,可以用 +`autoParallel: false` 关闭自动并行 fan-out。 + +## 内置 Subagents + +| Agent | 适用场景 | +| ----------------------------- | -------------------------------------------------------------- | +| `explore` | 只读代码搜索、文件检查和结构发现。 | +| `plan` | 只读实现计划和架构分析。 | +| `general` / `general-purpose` | 多步骤实现工作,可读写并执行命令。 | +| `verification` | 聚焦检查、复现、回归验证和对抗测试。 | +| `review` | 以 findings 为先的代码审查,关注正确性、回归、安全与可维护性。 | + +可以显式提及它们,例如 `@review`、`@agent-plan`、`use the verification subagent` 或 `delegate to general-purpose`。 + +## 手动委派 + +让父 agent 委派一个有边界的子任务: + +```text +Use task to ask an explore agent to inspect the auth module. +Return files inspected, findings, risks, and confidence. +``` + +宿主已经知道任务边界时,可以直接调用 SDK helper: + +```ts +const task = await session.task({ + agent: 'explore', + description: 'Inspect auth module', + prompt: 'Return files inspected, findings, risks, and confidence.', +}); +if (task.exitCode !== 0) throw new Error(task.output); +console.log(task.output); +``` + +子 agent 应返回紧凑契约: + +- summary +- files inspected or changed +- evidence references +- risks and unknowns +- confidence + +父 agent 不应吞入完整子 transcript。 + +## 并行委派 + +当工作彼此独立时,使用 `parallel_task` 或 `session.tasks(...)` 并发执行: + +```text +Run parallel_task with three lanes: +1. inspect provider config parsing +2. inspect Node SDK declarations +3. inspect release scripts + +Merge the results into one release-readiness report. +``` + +```ts +const batch = await session.tasks([ + { + agent: 'explore', + description: 'Inspect config', + prompt: 'Check provider parsing.', + }, + { + agent: 'verification', + description: 'Verify SDK', + prompt: 'Check SDK declarations.', + }, +]); +if (batch.exitCode !== 0) throw new Error(batch.output); +console.log(batch.output); +``` + +`session.task(...)` 和 `session.tasks(...)` 返回来自 `task` / `parallel_task` +工具的 `ToolResult`。读取 `output` 获取紧凑的子任务摘要,并在信任结果前检查 +`exitCode`。session options 中的 `maxParallelTasks` 与 ACL 中的 +`max_parallel_tasks` 会限制 sibling fan-out。 + +## 自动委派 + +自动委派默认需要显式启用。运行时会把当前请求与内置/自定义 agent 描述进行评分,并在置信度足够时启动最多 `maxTasks` 个子运行。 + +```ts +const session = agent.session('/repo', { + autoDelegation: { enabled: true, minConfidence: 0.72, maxTasks: 4 }, + maxParallelTasks: 8, + autoParallel: false, +}); +``` + +```text +auto_delegation { + enabled = true + auto_parallel = false + min_confidence = 0.72 + max_tasks = 4 +} +``` + +`autoParallel: false` / `auto_parallel = false` 是自动并行子 agent fan-out 的全局开关。手动 `parallel_task` 和 `session.tasks(...)` 仍然可用。 + +## Agent 目录 + +通过 `agentDirs`、`agent_dirs` 或 A3S 内置目录加载自定义 agent 定义: + +```ts +const session = agent.session('/repo', { agentDirs: ['./.a3s/agents'] }); +const loaded = session.registerAgentDir('./more-agents'); +``` + +A3S 会扫描配置的 `agent_dirs`、项目/用户 `.a3s/agents`,以及 Claude 兼容的 `.claude/agents` 迁移路径。新项目优先使用 `.a3s/agents`。 + +Markdown agent 文件支持 frontmatter: + +```markdown +--- +name: docs-auditor +description: Use proactively after documentation changes +tools: Read, Grep, Glob +disallowedTools: + - Write + - Bash(rm:*) +--- + +Audit docs for drift, broken examples, and unclear migration notes. +``` + +`tools` 字段是 allowlist。`disallowedTools` 是 denylist,且优先级高于 allowlist。模型路由字段不属于这个兼容层。 + +## Worker Agents + +通过 `workerAgents` 或 `registerWorkerAgent()` 注册一次性 worker agents: + +```ts +const session = agent.session('/repo', { + workerAgents: [ + { + name: 'frontend-worker', + description: 'Small verified frontend fixes', + kind: 'implementer', + model: 'provider/model-id', + maxSteps: 24, + confirmationInheritance: 'auto_approve', + }, + ], +}); +``` + +### 确认继承 + +通过 `confirmationInheritance` 控制子运行如何处理 Ask 决策: + +- `'auto_approve'`(默认):子运行自动批准所有 Ask 决策 +- `'deny_on_ask'`:子运行遇到 Ask 时立即失败 +- `'inherit_parent'`:子运行继承父级的确认策略 + +旧生命周期控制面 API 已移除;需要 UI 状态时,应用应消费 streaming events、run replay,以及 Node `cancelRun(runId)`。 + +## 可编程编排 + +本页的所有内容都是模型驱动的:`task` / `parallel_task`、`session.task(...)` / `session.tasks(...)` 以及自动委派让 LLM 决定何时以及如何 fan-out。当宿主已经知道工作的形态、并希望它确定可复现时,改用 `session.parallel(...)`、`session.pipeline(...)` 和 `session.parallelResumable(...)` 以编程方式表达。开发者表达的 fan-out、无屏障 pipeline 以及可恢复/可迁移工作流见 [编排](/guide/orchestration)。 diff --git a/website/docs/v6/zh/guide/teams.mdx b/website/docs/v6/zh/guide/teams.mdx new file mode 100644 index 00000000..078689d3 --- /dev/null +++ b/website/docs/v6/zh/guide/teams.mdx @@ -0,0 +1,126 @@ +--- +title: '团队' +description: '用 agent 定义、task 工具和自动委派构建团队' +--- + +# 团队 + +团队是一种驾驭层模式:命名 agent 定义加上 `task` 与 `parallel_task` 委派核心。A3S Code 不再暴露单独的 team runner API;父 session 仍负责合成、策略和最终验证。 + +## 推荐形态 + +1. 把长期角色放在 `.a3s/agents`,或配置额外 `agentDirs`。 +2. 每个角色保持聚焦:description、prompt、allowed tools、denied tools。 +3. 需要运行时选择高置信 subagent 时启用 `autoDelegation`。 +4. 宿主已知 lane 时直接调用 `session.task(...)` 或 `session.tasks(...)`。 +5. 父 agent 合并子摘要、证据引用和风险。 + +```ts +const session = agent.session('/repo', { + agentDirs: ['./.a3s/agents'], + planningMode: 'auto', + autoDelegation: { enabled: true, maxTasks: 4 }, + maxParallelTasks: 8, + autoParallel: false, +}); + +await session.send(` +Build a release-readiness team: +- explorer: find risky changed areas +- tester: identify missing verification +- security: review side-effect paths + +Use independent subagents where useful and return a single prioritized report. +`); +``` + +## 自定义 Agent 文件 + +Markdown agent 文件使用 Claude 兼容 frontmatter,但 A3S 原生位置是 `.a3s/agents`: + +```markdown +--- +name: release-reviewer +description: Use proactively after release or CI changes +tools: Read, Grep, Glob, Bash(cargo test*) +disallowedTools: + - Write + - Bash(git push*) +--- + +Review release blockers first, then risks, then follow-up work. +``` + +A3S 也读取 `.claude/agents` 作为迁移来源。新项目优先使用 `.a3s/agents`。 + +## 内置团队角色 + +无需创建文件即可使用: + +- `explore`:只读仓库探索 +- `plan`:只读实现计划 +- `general` / `general-purpose`:多步骤实现 +- `verification`:检查、复现和回归验证 +- `review`:findings-first 代码审查 + +## 手动 Lane + +宿主已经知道 lane 时,可直接调用 SDK helper: + +```ts +const result = await session.tasks([ + { + agent: 'explore', + description: 'Changed files', + prompt: 'Find risky changed files.', + }, + { + agent: 'verification', + description: 'Test gaps', + prompt: 'Find missing verification.', + }, + { + agent: 'review', + description: 'Regression review', + prompt: 'Review correctness risks.', + }, +]); + +if (result.exitCode !== 0) throw new Error(result.output); +console.log(result.output); +``` + +`session.tasks(...)` 是 `parallel_task` 的宿主侧封装;它返回 `ToolResult`, +而不是 `StepOutcome[]`。当你需要每条 lane 都有独立结构化 outcome 时,使用 +`session.parallel(...)`。 + +当团队的 lane 结构固定、应当可复现且可恢复而非由模型选择时,使用 [编排](/guide/orchestration) 中的可编程组合子(`session.parallel` / `session.pipeline` / `session.parallelResumable`)。 + +## Worker Agents + +当角色由宿主动态构造而不是存储在磁盘上时,通过 `workerAgents` 或 `registerWorkerAgent()` 注册一次性 worker agents: + +```ts +const session = agent.session('/repo', { + workerAgents: [ + { + name: 'frontend-worker', + description: 'Small verified frontend fixes', + kind: 'implementer', + model: 'provider/model-id', + maxSteps: 24, + confirmationInheritance: 'auto_approve', + }, + ], +}); +``` + +通过 `confirmationInheritance` 控制子运行如何处理 Ask 决策: + +- `'auto_approve'`(默认):子运行自动批准所有 Ask 决策 +- `'deny_on_ask'`:子运行遇到 Ask 时立即失败 +- `'inherit_parent'`:子运行继承父级的确认策略 + +## 运行状态 + +应用应通过 streaming events 和 run replay 快照展示状态,Node 侧需要取消时使用 `cancelRun(runId)`。 diff --git a/website/docs/v6/zh/guide/telemetry.mdx b/website/docs/v6/zh/guide/telemetry.mdx new file mode 100644 index 00000000..1aa39c61 --- /dev/null +++ b/website/docs/v6/zh/guide/telemetry.mdx @@ -0,0 +1,26 @@ +--- +title: 'Telemetry' +description: '追踪事件、验证摘要与运行时可观测性' +--- + +# Telemetry + +A3S Code 通过事件、trace、验证报告和结果元数据暴露运行证据。 + +```ts +const result = await session.send('运行发布检查'); + +console.log(result.promptTokens); +console.log(result.completionTokens); +console.log(result.totalTokens); +console.log(result.toolCallsCount); +console.log(result.verificationStatus); +console.log(result.verificationSummaryText); + +console.log(session.traceEvents()); +console.log(session.verificationReports()); +``` + +流式输出返回版本化 `AgentEvent` 信封。`version`、`type`、`payload` 和可选 +`metadata` 是稳定、无损的字段;文本、工具名、工具输出、错误、token 总数和验证摘要 +是便捷投影。未来未知的 `type` 及其 payload 仍会完整提供给宿主。 diff --git a/website/docs/v6/zh/guide/tools.mdx b/website/docs/v6/zh/guide/tools.mdx new file mode 100644 index 00000000..f7ffec6c --- /dev/null +++ b/website/docs/v6/zh/guide/tools.mdx @@ -0,0 +1,215 @@ +--- +title: '工具' +description: '工具选择、直接工具调用与验证证据' +--- + +# 工具 + +A3S Code 会保留工具注册表。`toolNames()` 返回当前 session 的工具表面。这个表面由 +workspace capability 与 session 级集成共同组装;因此非本地 workspace 可以有意隐藏它无法提供服务的工具。 + +## 工具表面 + +| 层级 | 工具 | 注册规则 | +| ------------------------ | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| Workspace-gated builtins | `read`、`write`、`edit`、`patch`、`grep`、`glob`、`ls`、`bash`、`git` | 仅在 `WorkspaceServices` 声明具备所需 capability 时注册。例如 browser 或 S3 workspace 可以暴露文件工具,同时隐藏 `bash` 或本地 git。 | +| Runtime builtins | `web_fetch`、`web_search`、`batch`、`program` | 由 core tool executor 注册。`batch` 和 `program` 接收当前 scoped invoker,因此内部工具既受 session surface 限制,也继承调用方的 governance scope。 | +| Session bootstrap tools | `task`、`parallel_task`、`generate_object`、`search_skills`、`Skill` | 构建 `AgentSession` 时加入。关闭 manual delegation 后会移除委派工具;`generate_object` 需要已配置的 LLM client;skill 工具使用有效 skill registry。 | +| Dynamic integrations | `mcp____` 与宿主注册工具 | 从 MCP manager 或宿主代码发现后加入。 | + +当某个工作流依赖特定工具可见时,在测试或应用诊断中使用 `toolNames()` / +`toolDefinitions()` 验证。工具可见性不是安全授权。`send`、`run`、`stream` +中的模型选择工具调用会经过 active-skill restriction、permission policy、 +confirmation、hooks、budget、queue/timeout、cancellation、递归调用保护、output +sanitization、artifact limit 与 workspace path check。`session.tool(...)` 这类 +direct SDK call 使用另一条显式 policy,见下文。 +`activeTools()` 回答的是另一个问题:当前 active operation 中哪些工具调用正在运行。 + +## 统一 Invocation 内核 + +Runtime 会为每次调用标记 origin: + +| Origin | 示例 | Permission / confirmation policy | +| ----------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| Agent | 模型在 `send` 或 `stream` 中发出 tool call | 应用完整的模型侧 policy 与 HITL。 | +| Nested | `batch`、`program` 或其他 orchestrator 的内部调用 | 继承调用方的 governed invoker 与 invocation stack。 | +| Host direct | `session.tool(...)`、typed read/write/git helper、`session.program(...)` 与直接 task helper | Trusted control plane:宿主已经选择该调用,因此跳过模型侧 permission/HITL。 | + +三种 origin 都经过同一个 invocation kernel。Pre-hook 可以阻止调用;budget check +在 side effect 前执行;queue/timeout、cancellation、递归保护、post-hook 与 +security-provider output sanitization 仍然有效。安装 scoped invoker 后,nested call +不能回退到 raw registry。 + +Host-direct policy 不是面向终端用户的授权系统。Embedding application 必须先完成 +用户身份认证和授权,再把请求翻译成 direct SDK helper。关闭 session 会通过 session +cancellation scope 中止正在执行的 host-direct tool。 + +```ts +const files = await session.glob('src/**/*.rs'); +const hits = await session.grep('PermissionPolicy'); +const status = await session.git('status'); +const output = await session.bash('cargo test -p a3s-code-core'); +const raw = await session.tool('read', { file_path: 'README.md' }); +const schemas = session.toolDefinitions(); +const hitLines = hits.split('\n').filter(Boolean).length; + +console.log(files.length, hitLines, output.length, schemas.length); +console.log(status.output); +console.log(raw.output); +``` + +直接工具调用在 session 工作区下执行,应视为宿主侧特权操作。它们不更新 transcript, +因此不占用 session 的 single-flight conversation lease。`session.tool(...)`、 +`session.program(...)`、`session.git(...)`、`session.writeFile(...)`、 +`session.ls(...)`、`session.editFile(...)` 和 `session.patchFile(...)` 返回 +`ToolResult`,读取 `output`、`exitCode` 和可选的 `metadataJson`。类型化 +read/search/shell helper 返回更简单的值:`readFile`、`grep`、`bash` 返回字符串, +`glob` 返回字符串数组。长输出应在进入 prompt 前先摘要。 + +## 结构化输出:`generate_object` + +`generate_object` 工具会让配置的 LLM 生成 JSON 对象,对响应执行 JSON Schema +校验,并且只在零退出码结果中返回校验后的对象。它有两种使用方式: + +1. **智能体自主调用**:LLM 在工具列表中看到 `generate_object`,在需要结构化输出时自行决定调用。 +2. **直接调用**:应用通过 `session.tool('generate_object', ...)` 绕过模型驱动的工具选择步骤;工具内部仍会调用配置的 LLM。 + +```ts +const result = await session.tool('generate_object', { + schema: { + type: 'object', + required: ['sentiment', 'confidence'], + properties: { + sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] }, + confidence: { type: 'number', minimum: 0, maximum: 1 }, + }, + }, + prompt: '分类: "这个产品太棒了!"', + schema_name: 'sentiment', + mode: 'tool', + max_repair_attempts: 2, +}); + +if (result.exitCode !== 0) { + throw new Error(result.output); +} + +const { object } = JSON.parse(result.output); +// object = { sentiment: "positive", confidence: 0.95 } +``` + +### 参数 + +| 参数 | 类型 | 必填 | 说明 | +| --------------------- | ------- | ---- | --------------------------------------------------------------------- | +| `schema` | object | 是 | 用于校验输出的 JSON Schema | +| `prompt` | string | 是 | 描述要生成或提取的内容 | +| `schema_name` | string | 否 | 短名称(默认 "result") | +| `schema_description` | string | 否 | 合成工具的描述 | +| `system` | string | 否 | 可选系统提示 | +| `mode` | string | 否 | `"auto"` / `"strict"` / `"json"` / `"tool"` / `"prompt"`(默认 auto) | +| `max_repair_attempts` | integer | 否 | 0–5(默认 2) | + +### 模式 + +- **tool**:注入一个合成工具,其参数就是 schema。这是当前跨 provider 的默认路径。 +- **prompt**:将 schema 指令追加到 prompt 中。它适合作为仅 prompt 的回退路径,但更依赖模型遵循指令。 +- **auto**:当前解析为 `tool`。 +- **strict** / **json**:API 接受这两个值,但当前运行路径会解析为 `tool`,因为 provider 级 `response_format` 尚未在这里启用。 + +### 流式 + +通过 `session.stream()` 调用时,部分对象以 `tool_output_delta` 事件发出: + +```ts +const stream = await session.stream('提取所有发票...'); + +while (true) { + const { value: ev, done } = await stream.next(); + if (done) break; + if (!ev) continue; + + if (ev.type === 'tool_output_delta' && ev.toolName === 'generate_object') { + const { object_partial } = JSON.parse(ev.text); + renderProgress(object_partial); + } +} +``` + +### 修复重试 + +如果 LLM 输出未通过 schema 校验,工具会自动将校验错误反馈给模型并重试。这能处理缺少必填字段、枚举值错误等边界情况,无需应用层重试逻辑。 + +委派子任务可以直接使用 SDK helper,它们底层仍是同一组核心工具: + +```ts +await session.task({ + agent: 'explore', + description: '查找认证文件', + prompt: '检查认证相关文件,并返回紧凑证据列表。', +}); + +await session.tasks([ + { agent: 'explore', description: '查找测试', prompt: '定位认证测试。' }, + { + agent: 'verification', + description: '检查风险', + prompt: '审查认证边界情况。', + }, +]); +``` + +自动 subagent 委派也使用同一组核心工具。`autoParallel: false` 只关闭自动并行 fan-out,不会移除 `parallel_task` 或 `session.tasks(...)`。 + +## Programmatic Tool Calling + +PTC 不是单次 direct tool call。`program` 工具会在内嵌 QuickJS VM 中运行一个受限 JavaScript 脚本;脚本定义 `async function run(ctx, inputs)`,用一个有边界的程序替代多轮模型工具调用。 + +不要让模型反复消耗工具回合: + +```text +grep -> read -> grep -> read -> summarize +``` + +可以让模型请求 `program` 运行一个脚本: + +```js +// search-auth.js +export default async function run(ctx, inputs) { + const hits = await ctx.grep(inputs.query, { glob: '*.rs' }); + const files = await ctx.glob('crates/**/*.rs'); + const snippets = []; + + for (const file of files.slice(0, 20)) { + const content = await ctx.readFile(file); + if (content.includes(inputs.query)) { + snippets.push({ file, preview: content.slice(0, 1200) }); + } + } + + return { + summary: `找到 ${snippets.length} 个与 ${inputs.query} 相关的候选文件`, + evidence: snippets, + rawSearch: hits, + }; +} +``` + +SDK 的 `session.program(...)` 支持内联 `source`,也支持 workspace 相对路径 `.js` 或 `.mjs` 文件: + +```js +await session.program({ + path: 'scripts/ptc/search-auth.js', + inputs: { query: 'PermissionPolicy' }, + allowedTools: ['grep', 'glob', 'read'], + limits: { + timeoutMs: 30000, + maxToolCalls: 30, + maxOutputBytes: 65536, + }, +}); +``` + +`session.program(...)` 等价于 `session.tool('program', { type: 'script', language: 'javascript', ... })`,只是使用 SDK 原生命名。如果省略 `allowedTools` / `allowed_tools`,脚本可以调用除 `program` 之外的所有已注册工具。需要更小能力面时,再显式传入 allow-list。 + +QuickJS VM 不获得文件系统、网络、子进程或环境变量权限。脚本唯一有用的能力来自 `ctx`,这些方法会回到 A3S Code 的受控工具执行路径。PTC 会返回可读的 `ToolResult.output`,结构化数据位于 `ToolResult.metadataJson`。原始大输出不应反复塞进 prompt;应先总结发现、证据引用、风险和建议下一步。 diff --git a/website/docs/v6/zh/guide/tui.mdx b/website/docs/v6/zh/guide/tui.mdx new file mode 100644 index 00000000..884a07cb --- /dev/null +++ b/website/docs/v6/zh/guide/tui.mdx @@ -0,0 +1,276 @@ +--- +title: 'A3S Code TUI' +description: '安装、配置并使用 a3s code 终端应用' +--- + +# A3S Code TUI + +`a3s code` 是 A3S Code 的交互式终端应用。它由 +[`a3s` CLI](https://github.com/A3S-Lab/Cli) 发布,嵌入 +`a3s-code-core` 运行时,并使用 [`a3s-tui`](https://github.com/A3S-Lab/TUI) +渲染运行时事件流。 + +需要现成的终端编程 agent 时使用 TUI;要构建自己的 harness、IDE 扩展、 +server worker、workflow runner 或产品 UI 时使用 SDK 包。 + +## Surface Map + +| Surface | 仓库 | 职责 | +| --------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `a3s-code-core` | [A3S-Lab/Code](https://github.com/A3S-Lab/Code) | Rust 运行时 crate,以及 Node.js 和 Python SDK。 | +| `a3s code` | [A3S-Lab/Cli](https://github.com/A3S-Lab/Cli) | 驱动 A3S Code session 的终端编程 agent 应用。 | +| A3S Use | [A3S-Lab/Use](https://github.com/A3S-Lab/Use) | 独立发布的 Browser、原生 Office、内置 OCR、可选 Office 兼容层,以及通过标准 MCP 与 Skill 投影到 Code 的签名外部应用能力。 | +| `a3s-tui` | [A3S-Lab/TUI](https://github.com/A3S-Lab/TUI) | CLI 使用的终端 UI 框架,不是 agent runtime。 | +| A3S monorepo | [A3S-Lab/a3s](https://github.com/A3S-Lab/a3s) | 产品文档、发布编排、submodule pin 与相关 crates。 | + +## 安装 + +先安装 CLI,然后在希望 agent 检查的 workspace 中运行 `a3s code`: + +```bash +brew install A3S-Lab/tap/a3s + +# 或从 crates.io 安装 +cargo install a3s + +# 或从 CLI 仓库安装 +cargo install --git https://github.com/A3S-Lab/Cli +``` + +```bash +a3s code +a3s code resume +a3s code resume +a3s code update +``` + +顶层 `a3s update` 命令也会被 CLI 接受,并路由到同一个 updater。 + +## 配置 + +TUI 按以下顺序发现配置: + +1. `A3S_CONFIG_FILE` +2. 从当前目录向上查找的 `.a3s/config.acl` +3. `~/.a3s/config.acl` + +TUI 使用 `.a3s/config.acl` 作为项目本地配置。SDK 示例经常使用 +`agent.acl`,因为 embedders 会通过 `Agent.create("agent.acl")` 显式传入 +配置路径。二者都是 ACL 文件;关键区别在于由谁发现文件。 +可选 A3S OS endpoint 使用 `os = "https://..."` 配置。 + +`/ide` 与 `/config` 共用同一套全屏编辑器。文件树使用终端安全的文件类型与目录 +符号、语义化图标颜色和对齐的展开标记;编辑器标题、文件 metadata 与带分隔线的 +行号 gutter 复用同一图标来源,不依赖 emoji 或 Nerd Font。 + +不要把真实 key、私有 provider URL、tenant 标识或用户本地路径提交到仓库。 +公开模板应通过环境变量解析凭据: + +```text +default_model = "provider/model-id" +os = env("A3S_OS_URL") + +providers "provider" { + apiKey = env("PROVIDER_API_KEY") + baseUrl = env("PROVIDER_BASE_URL") + + models "model-id" { + tool_call = true + limit = { + context = 128000 + output = 4096 + } + } +} +``` + +## 会话恢复 + +Core session 快照会自动保存到 +`/.a3s/tui/sessions/v1/sessions`,TUI 自己维护的逐 session 状态保存在 +`/.a3s/tui/session-state/v1`。退出后,CLI 会打印完整的 +`a3s code resume ` 命令;启用颜色输出时,该命令会高亮显示。不带 id 的 +`a3s code resume` 会恢复当前 workspace 中最新的 saved session。 + +恢复时会保留该 session 的模型及 credential source、effort profile、执行模式 +(`default`、`plan` 或 `auto`)和语法高亮主题。如果退出中断了 durable `/goal`,启动时 +会显示 `Resume goal` 与 `Leave paused`:前者从下一次 goal iteration 继续,且不改变已恢复的 +执行模式;后者进入 session 并保持 goal 暂停,之后可用 `/goal resume` 继续。 + +## 文件系统优先工作流 + +TUI 面向 workspace。它在仓库中启动时,可以加载 SDK session 也会使用的 +filesystem-first 约定: + +| 路径 | TUI 中的作用 | +| --------------------------- | ----------------------------------------------------------- | +| `AGENTS.md` | 加载到上下文中的项目说明。 | +| `.a3s/config.acl` | 项目本地模型、provider、skill、storage 和 delegation 策略。 | +| `.a3s/agents/` | 可供 `task`、`parallel_task` 和自动委派使用的 worker role。 | +| `.a3s/skills/` 与 `skills/` | 暴露给 session 的可复用项目 skills。 | +| `.a3s/kb/` | TUI knowledge workflows 使用的项目知识库。 | + +这些文件让行为可审查,但不会绕过运行时边界。权限策略、确认提示、 +workspace 检查、工具可见性、响应契约与验证仍然通过 A3S Code 执行路径。 + +## 通过 A3S Use 使用 Browser、Office 与 OCR + +A3S Use 是独立发布的 first-use 组件。终端接管前,`a3s code` 会复用健康安装;网络和 +自动准备策略允许时,会安装经过校验的 release。`--offline`、`A3S_OFFLINE=1` 和 +`A3S_NO_AUTO_INSTALL=1` 始终禁止该修改。准备失败不会阻止 Code 启动,并会通过 `/use` +保持可诊断。runtime 与模型资源也可以显式提前准备: + +```bash +a3s install use --source release +a3s install use/browser + +# 可选的 OfficeCLI 兼容 provider;原生 Office 已内置在 Use 中。 +a3s install use/office + +# 安装或修复固定版本的本地 PP-OCRv6 模型。 +a3s install use/ocr +a3s use ocr doctor --json +``` + +当 Use 主程序 ready 后,Code 会读取它带版本的 capability registry, +并同时监听 generation 与内容 revision。之后 provider 变化,以及 extension 的安装、升级、 +启用或禁用,都会把对应 MCP 与已校验的 `SKILL.md` 热插拔到当前 session。如果启动时缺少 +首次准备被禁用或失败,则需要在显式安装后重启 Code 一次;之后的 capability 变化不需要重启。 + +`/use` 与 `/use status` 会显示 Use binary 路径/版本、registry 收敛状态、provider +readiness、MCP 连接与工具数量,以及 Skill 的校验/加载状态。`/use repair` 只打印非破坏性的 +修复指引,绝不会自动执行安装或改变 extension 状态。 + +主 coding model 有意看不到原始 `mcp__use_*` definitions;它通过 `task` 看到专用的 +`use` worker,并把应用操作委派给该 worker。Use worker 只能看到 `mcp__use_*`,不能使用 +shell、workspace、无关 MCP 或递归委派工具。受管 Skill 只提供领域指引,不能扩大权限。 +用户可以直接说“用 Browser 检查这个页面”“用 Office 更新这个工作簿”或“对这张扫描图做 +OCR”,无需手写工具名。能力不可用时,worker 会返回 typed failure,不会换用其他工具兜底。 +默认 Browser surface 包含 doctor 和有界安装工具;缺少 managed Browser 时,只能在父 +TUI 确认后由 Use worker 请求安装。 + +| 能力 | Code surface | Provider 与确认行为 | +| ----------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Browser | `mcp__use_browser__*` | 使用已发现的 browser,或通过需父级 HITL 的 mutation 请求 managed provider。封闭会话内的只读检查可直接执行;导航、网络/open-world 读取、输入、点击与提交需要 HITL。 | +| 原生 Office | `mcp__use_office__*` | 使用带 revision/conflict 检查的有界原生 Office package kernel。读取是 closed-world;修改与破坏性保存需要 HITL。 | +| 内置 OCR | `mcp__use_ocr__*` | 通过 ONNX Runtime 在本地运行固定版本的 `PP-OCRv6_small` 检测与识别模型。doctor 与提取均为 closed-world read-only 工具,source bytes 不会离开设备。 | + +MCP behavior metadata 只能提升风险,不能降低父级策略。只有同时声明 +`readOnlyHint=true`、`openWorldHint=false` 且不是 destructive 的操作才无额外确认。 +缺少 annotations、open-world、mutation、destructive 或 submit 风险都会向父 TUI 请求 +确认;父级 deny 始终有效。Office mutation 绝不会被自动重试,尤其 +`use.office.outcome_unknown` 表示变更可能已经落地,worker 必须保留证据并停止。 + +Code 也不会把 MCP 结果压扁成纯文本:`outputSchema`、`structuredContent`、图片、 +text/blob resources、协议 metadata 和带 SHA-256 的有界 artifacts 都会进入正常的 +transcript/tool-result 路径。 + +## Runtime Event Stream + +TUI 将 `AgentSession::stream()` 视为事实来源。运行时事件驱动 transcript 文本、 +tool-call 进度、planning 状态、approval prompt、memory view、git/file panel、 +verification evidence 与可 replay 的 run state。 + +`Ctrl+T` 打开完整、实时的语义会话 transcript,其中包括用户与 +助手消息、plan、每个工具的完整生命周期与完整输出、subagent 状态,以及当前仍在 +流式生成的 Markdown 尾部。 + +这个分层对产品构建者很重要:TUI 只是 runtime 上方的一个有主张的客户端。 +SDK embedders 可以基于同样的 session、tool、event、persistence 与 +verification API 构建不同控制面。 + +## Slash Commands + +内置 slash commands 包括: + +| 命令 | 作用 | +| -------------------------------------- | ------------------------------------------------------------------------------------------- | +| `/model` | 在已配置模型和已登录账号模型之间切换。 | +| `/init` | 分析 workspace 并生成 `AGENTS.md`。 | +| `/config` | 在编辑器中打开当前生效的 ACL config。 | +| `/use` / `/use status` / `/use repair` | 查看实时 A3S Use capability 状态,或打印不会自动执行的显式修复指引。 | +| `/theme` | 切换代码高亮主题。 | +| `/flow` | 选择或草拟 workflow asset,用于本地开发与可选宿主集成。 | +| `/agent` | 选择 agent definition,用于本地开发与可选宿主集成。 | +| `/mcp` | 选择 MCP server asset,用于本地开发与可选宿主集成。 | +| `/skill` | 选择 skill asset,用于本地开发与可选宿主集成。 | +| `/okf` | 选择 OKF package,用于本地开发与知识包管理。 | +| `/login` 与 `/logout` | 登录或退出 `os` endpoint 对应的 A3S OS account。 | +| `Open view` | 点击内联按钮,在原生窗口中打开最近的 validated local report 或 trusted runtime view。 | +| `/plugin` 与 `/reload` | 启用、禁用并重新扫描 skills/plugins。 | +| `/ide` | 打开 workspace 文件树和代码查看器。 | +| `/memory` | 浏览长期记忆。 | +| `/kb` | 把文本、文件或目录加入项目知识库。 | +| `/ctx` | 搜索 past sessions、附加结果,或保存到 memory。 | +| `/effort` | 调整对所有 provider 生效的 host budgets,并在 provider 支持时传递 native reasoning effort。 | +| `/compact` | 总结并压缩对话上下文。 | +| `/goal ` | 为 session 设置持续目标。 | +| `/goal resume` | 继续 session 恢复时保留为 paused 的 durable goal。 | +| `/loop` | 自动继续执行任务,直到完成或停止。 | +| `/sleep` | 把今天的工作整理进长期记忆。 | +| `/help` | 显示 commands 与 shortcuts。 | +| `/fork` | 从当前点分支出新的 saved session。 | +| `/clear` | 重置 conversation。 | +| `/auto` | 切换到 auto-approve mode。 | +| `/update` | 将 CLI 升级到最新 release。 | +| `/exit` | 退出 `a3s code`。 | + +部分命令只能在 session 空闲时执行,因为它们会改变 conversation、model、 +context 或 process state。 + +## Effort 与 DeepResearch + +| Level | Thinking budget | Tool rounds | Continuations | Parallel tasks | +| ----------- | --------------: | ----------: | ------------: | -------------: | +| `low` | 2,048 | 240 | 4 | 4 | +| `medium` | 8,192 | 800 | 8 | 8 | +| `high` | 16,384 | 1,200 | 12 | 12 | +| `xhigh` | 32,768 | 1,800 | 16 | 16 | +| `max` | 65,536 | 2,400 | 24 | 24 | +| `ultracode` | 65,536 | 3,200 | 32 | 32 | + +`low` 到 `max` 保持 Codex 原生 `reasoning.effort` 映射,并关闭运行时自动委派; +`task` 与 `parallel_task` 仍可被明确调用。只有 `ultracode` 会启用自动委派、 +自动规划与动态工作流指引。仅用于生成最终回答的 synthesis continuation 不会再次 +启动子 Agent。 + +DeepResearch 当前先收集 bounded web seed,再根据复杂度选择 direct fast path +或本地 `parallel_task` tracks 与 bounded follow-up rounds。在 Function-as-a-Service +support 就绪前,A3S OS Runtime tool-call fan-out 保持禁用。 + +在 TUI 输入 `? ` 可启动 DeepResearch。默认情况下,它允许 Direct Web 和 +delegated web tools;query 中包含 `no web`、`do not use web` 或 `不要联网` 时, +这些 web paths 会被禁用,同时保留本地 evidence collection 与 follow-up depth。 + +每次 run 的 artifacts 位于: + +```text +.a3s/research//report.md +.a3s/research//index.html +``` + +completed report 必须能够追溯到本次 run 收集的 sources。若 evidence collection 或 +synthesis 无法满足这个条件,A3S 会写入明确标注、low-confidence 的 DeepResearch +Recovery Report,而不是把 unsupported conclusion 伪装成 completed report。本地 HTML +由 loopback-only、no-auth viewer 提供,因此 signed out 时也可以通过 `Open view` 打开。 + +Headless command 使用相同 workflow: + +```bash +a3s code deepresearch [--local|--os] [--local-only|--web] +``` + +`--local` 只表示本地编排,仍允许 web evidence;`--local-only`(也接受 `--offline`) +会强制禁用网络,`--web` 会显式启用 web + workspace evidence。自然语言中的 `no web` +仅作为 compatibility fallback。`--os` 当前禁用。headless command 的所有阶段共同消耗 +一个 60 秒 absolute deadline,其中 host-direct 最多使用 20 秒;失败后仅用剩余预算执行 +最多一轮 fallback,并为取消与 artifact 收尾保留最后 3 秒。若最终只能生成 recovery +report,artifacts 仍会落盘,但 command 会以 error 结束。 + +## 相关页面 + +1. [文件系统优先](/guide/filesystem-first) +2. [会话](/guide/sessions) +3. [命令](/guide/commands) +4. [工具](/guide/tools) +5. [安全](/guide/security) diff --git a/website/docs/v6/zh/guide/verification.mdx b/website/docs/v6/zh/guide/verification.mdx new file mode 100644 index 00000000..549e3ccb --- /dev/null +++ b/website/docs/v6/zh/guide/verification.mdx @@ -0,0 +1,158 @@ +--- +title: '验证' +description: '用验证命令和报告证明一个回合已完成,而不是轻信模型的声明' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# 验证 + +运行时将"完成"视为必须被**证明**的事实,而不仅仅是被声明的结果。当模型说某个任务已完成时,这句话本身毫无价值。验证把声明转化为证据:你声明一组*必须*成功的命令,运行时执行它们,结果会携带一份你可以检视、据以拦截或呈现给用户的报告。 + +验证是会话级的。Rust 核心运行每条命令,记录其退出状态和输出,并将每份报告汇总为一份随回合结果一同返回的摘要。 + +## 运行验证命令 + +一条验证命令就是一个小而具名的检查:一个 `id`、一个 `kind`、一段可读的 `description`,以及要运行的 `command`。当某个失败应被视为硬失败而非警告时,将该检查标记为 `required`。 + + + + +```ts +const report = await session.verifyCommands('release-readiness', [ + { + id: 'build', + kind: 'build', + description: 'Project compiles', + command: 'cargo build --all-features', + required: true, + timeoutMs: 120000, + }, + { + id: 'tests', + kind: 'test', + description: 'Unit tests pass', + command: 'cargo test', + required: true, + }, +]); + +console.log(report); +``` + + + + +```python +report = session.verify_commands('release-readiness', [ + { + "id": "build", + "kind": "build", + "description": "Project compiles", + "command": "cargo build --all-features", + "required": True, + "timeout_ms": 120000, + }, + { + "id": "tests", + "kind": "test", + "description": "Unit tests pass", + "command": "cargo test", + "required": True, + }, +]) + +print(report) +``` + + + + +`subject`(此处为 `release-readiness`)为这一批检查命名,使同一会话内的多次验证在报告中保持彼此独立。 + +## 读取回合结束后的摘要 + +每个回合的 `send()` 结果同样携带只读的验证字段,因此你无需单独发起一次验证调用即可据结果拦截。用这些字段判断该回合是否真正完成了它所声称的工作。 + + + + +```ts +const result = await session.send('Apply the fix and run the checks'); + +console.log(result.verificationStatus); +console.log(result.pendingVerificationCount); +console.log(result.failedVerificationCount); +console.log(result.verificationReportCount); +console.log(result.verificationSummaryText); + +if (result.failedVerificationCount > 0) { + throw new Error('Turn reported done but verification failed'); +} +``` + + + + +```python +result = session.send('Apply the fix and run the checks') + +print(result.verification_status) +print(result.pending_verification_count) +print(result.failed_verification_count) +print(result.verification_report_count) +print(result.verification_summary_text) + +if result.failed_verification_count > 0: + raise RuntimeError('Turn reported done but verification failed') +``` + + + + +## 检视报告与摘要 + +除了逐回合的字段之外,会话还暴露完整的报告集合、一份结构化摘要、可用的预设,以及一份可读的概要。该概要是向人展示某回合*为何*通过或失败的最快方式。 + + + + +```ts +import { formatVerificationSummary } from '@a3s-lab/code'; + +const reports = session.verificationReports(); +const summary = session.verificationSummary(); +const presets = session.verificationPresets(); + +// Either the session helper or the standalone formatter yields readable text. +console.log(session.verificationSummaryText()); +console.log(formatVerificationSummary(summary)); +``` + + + + +```python +reports = session.verification_reports() +summary = session.verification_summary() +presets = session.verification_presets() + +# The session helper returns a ready-to-print human-readable digest. +print(session.verification_summary_text()) +``` + + + + +`verificationPresets()` 返回根据 workspace 文件推断出的检查模板,例如 +`Cargo.toml`、`package.json`、`pyproject.toml` 和 `go.mod`。请把它们当作起点: +在用来拦截发布或用户可见自动化之前,应审查命令、超时和 required 标记是否适合该项目。 + +## 为何重要 + +没有验证,一次智能体运行止于模型的一面之词。有了验证,运行止于可观测的证据:编译通过的构建、跑通的测试套件、保持沉默的代码检查器。摘要文本提供审计轨迹;结果上的计数让你能在自动化中以失败为默认(fail closed)。 + +## 相关 + +- [遥测](/guide/telemetry) —— 将追踪事件和验证报告作为运行时证据进行检视。 +- [限制](/guide/limits) —— 在验证运行之前限定一个回合能完成多少工作量。 diff --git a/website/docs/v6/zh/guide/workspace-backends.mdx b/website/docs/v6/zh/guide/workspace-backends.mdx new file mode 100644 index 00000000..feae3f82 --- /dev/null +++ b/website/docs/v6/zh/guide/workspace-backends.mdx @@ -0,0 +1,226 @@ +--- +title: 'Workspace 后端' +description: '让 A3S Code session 运行在本地文件、S3 兼容对象存储和 remote git 服务之上。' +--- + +import { Tab, Tabs } from '@rspress/core/theme'; + +# Workspace 后端 + +Workspace backend 决定内置 workspace 工具从哪里读写文件。默认后端是以 session +workspace 为根目录的本地文件系统。SDK 也暴露了显式本地后端、S3 兼容对象存储后端, +以及可选的 HTTP/JSON remote git provider。 + +当宿主负责 workspace 放置时使用这组能力:本地开发、浏览器或容器 workspace、对象存储 +workspace、托管 session 等。 + +## 能力矩阵 + +| Backend | 文件工具 | 搜索工具 | Shell 与本地 Git | +| ---------------------------------- | -------------------------------------- | --------------------------------------------------- | ----------------------------------------- | +| 默认本地 workspace | `read`、`write`、`edit`、`patch`、`ls` | `grep`、`glob` | `bash`、`git` | +| `LocalWorkspaceBackend` | 与默认本地 workspace 相同 | 与默认本地 workspace 相同 | 与默认本地 workspace 相同 | +| `S3WorkspaceBackend` | `read`、`write`、`edit`、`patch`、`ls` | 设置 `searchEnabled` 后可使用降级版 `grep` / `glob` | 不注册 | +| `S3WorkspaceBackend` + `remoteGit` | S3 文件工具 | 可选降级版 S3 搜索 | 通过 remote git 提供 `git`,不提供 `bash` | + +对象存储不能执行本地进程。不要承诺 S3 workspace 上可以直接跑 shell,除非宿主通过 +MCP 或 A3S Box 额外提供隔离后的执行能力。 + +## 本地后端 + +显式本地后端适合宿主希望本地与远程 session 都走同一个 option surface 的场景。 + + + + +```ts +import { Agent, LocalWorkspaceBackend } from '@a3s-lab/code'; + +const agent = await Agent.create('agent.acl'); +const session = agent.session('/repo', { + workspaceBackend: new LocalWorkspaceBackend('/repo'), +}); +``` + + + + +```python +from a3s_code import Agent, LocalWorkspaceBackend, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.workspace_backend = LocalWorkspaceBackend("/repo") +session = agent.session("/repo", opts) +``` + + + + +## S3 后端 + +`S3WorkspaceBackend` 会把内置文件工具指向任意 S3 兼容服务,包括 AWS S3、MinIO、 +RustFS、Cloudflare R2 和 Backblaze B2。 + + + + +```ts +import { Agent, S3WorkspaceBackend } from '@a3s-lab/code'; + +const backend = new S3WorkspaceBackend({ + endpoint: process.env.WORKSPACE_S3_ENDPOINT, + region: process.env.WORKSPACE_S3_REGION ?? 'us-east-1', + accessKeyId: process.env.S3_ACCESS_KEY_ID!, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, + bucket: process.env.WORKSPACE_S3_BUCKET!, + prefix: 'sessions/example', + forcePathStyle: true, + searchEnabled: false, +}); + +const agent = await Agent.create('agent.acl'); +const session = agent.session('s3://workspace-bucket/sessions/example', { + workspaceBackend: backend, +}); +``` + + + + +```python +import os + +from a3s_code import Agent, S3WorkspaceBackend, SessionOptions + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.workspace_backend = S3WorkspaceBackend( + bucket=os.environ["WORKSPACE_S3_BUCKET"], + prefix="sessions/example", + access_key_id=os.environ["S3_ACCESS_KEY_ID"], + secret_access_key=os.environ["S3_SECRET_ACCESS_KEY"], + endpoint=os.environ.get("WORKSPACE_S3_ENDPOINT"), + region=os.environ.get("WORKSPACE_S3_REGION", "us-east-1"), + force_path_style=True, + search_enabled=False, +) +session = agent.session("s3://workspace-bucket/sessions/example", opts) +``` + + + + +S3 搜索是显式 opt-in。启用后,`grep` / `glob` 会降级为对象列表和有上限的下载。 +当 bucket 较大或 endpoint 会限流时,应配置 `maxObjectsScanned`、 +`maxGrepBytesPerObject` 和 `searchConcurrency`。 + +### S3 选项 + +| Node.js 选项 | Python 选项 | 必填 | 作用 | +| ----------------------- | --------------------------- | ---- | -------------------------------------------------------- | +| `bucket` | `bucket` | 是 | 存储 workspace 对象的 S3 bucket。 | +| `prefix` | `prefix` | 是 | bucket 内的逻辑 workspace 根;使用 `""` 表示 bucket 根。 | +| `accessKeyId` | `access_key_id` | 是 | Access key id,通常从宿主环境读取。 | +| `secretAccessKey` | `secret_access_key` | 是 | Secret access key,通常从宿主环境或密钥管理系统读取。 | +| `endpoint` | `endpoint` | 否 | 自定义 S3 兼容 endpoint;AWS S3 默认 endpoint 可省略。 | +| `region` | `region` | 否 | Region;省略时默认为 `us-east-1`。 | +| `sessionToken` | `session_token` | 否 | 使用临时凭据时的 STS session token。 | +| `forcePathStyle` | `force_path_style` | 否 | MinIO、RustFS 和多数非 AWS endpoint 通常设为 `true`。 | +| `maxReadBytes` | `max_read_bytes` | 否 | 单次 read 的大小上限;默认 10 MiB。 | +| `searchEnabled` | `search_enabled` | 否 | 启用降级版 S3 `grep` / `glob`;默认 false。 | +| `maxObjectsScanned` | `max_objects_scanned` | 否 | 单次 search 扫描对象数上限;仅 search 启用时使用。 | +| `maxGrepBytesPerObject` | `max_grep_bytes_per_object` | 否 | `grep` 的单对象下载上限;仅 search 启用时使用。 | +| `searchConcurrency` | `search_concurrency` | 否 | `grep` 时并发下载对象数;仅 search 启用时使用。 | + +## Remote Git + +`remoteGit` 会在 `workspaceBackend` 之上挂载 HTTP/JSON git provider。它用于没有本地 +`.git` 目录的非本地 workspace。 + +`remoteGit` 必须和 `workspaceBackend` 一起传;单独传入会被拒绝。 + + + + +```ts +import { Agent, S3WorkspaceBackend } from '@a3s-lab/code'; + +const backend = new S3WorkspaceBackend({ + endpoint: process.env.WORKSPACE_S3_ENDPOINT, + region: process.env.WORKSPACE_S3_REGION ?? 'us-east-1', + accessKeyId: process.env.S3_ACCESS_KEY_ID!, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, + bucket: process.env.WORKSPACE_S3_BUCKET!, + prefix: 'sessions/example', + forcePathStyle: true, +}); + +const agent = await Agent.create('agent.acl'); +const session = agent.session('s3://workspace-bucket/sessions/example', { + workspaceBackend: backend, + remoteGit: { + baseUrl: process.env.REMOTE_GIT_BASE_URL!, + repoId: 'sessions/example', + bearerToken: process.env.REMOTE_GIT_TOKEN, + }, +}); +``` + + + + +```python +import os + +from a3s_code import ( + Agent, + RemoteGitBackendConfig, + S3WorkspaceBackend, + SessionOptions, +) + +agent = Agent.create("agent.acl") +opts = SessionOptions() +opts.workspace_backend = S3WorkspaceBackend( + bucket=os.environ["WORKSPACE_S3_BUCKET"], + prefix="sessions/example", + access_key_id=os.environ["S3_ACCESS_KEY_ID"], + secret_access_key=os.environ["S3_SECRET_ACCESS_KEY"], + endpoint=os.environ.get("WORKSPACE_S3_ENDPOINT"), + region=os.environ.get("WORKSPACE_S3_REGION", "us-east-1"), + force_path_style=True, +) +opts.remote_git = RemoteGitBackendConfig( + base_url=os.environ["REMOTE_GIT_BASE_URL"], + repo_id="sessions/example", + bearer_token=os.environ["REMOTE_GIT_TOKEN"], +) +session = agent.session("s3://workspace-bucket/sessions/example", opts) +``` + + + + +不要把 remote git 凭据写入 `agent.acl` 或 Agent 目录。应由宿主通过环境变量或密钥管理系统注入。 + +### Remote Git 选项 + +| Node.js 选项 | Python 选项 | 必填 | 作用 | +| ------------------ | -------------------- | -------- | ----------------------------------------------------------- | +| `baseUrl` | `base_url` | 是 | Remote git 服务 base URL,不带末尾斜杠。 | +| `repoId` | `repo_id` | 是 | 与 remote git 服务约定的不透明仓库 id。 | +| `bearerToken` | `bearer_token` | 生产环境 | Remote git 服务的 bearer 凭据;只应在受信任开发环境中省略。 | +| `clientCertPem` | `client_cert_pem` | 否 | mTLS 客户端证书路径;必须与客户端 key 成对设置。 | +| `clientKeyPem` | `client_key_pem` | 否 | mTLS 客户端 key 路径;必须与证书成对设置。 | +| `requestTimeoutMs` | `request_timeout_ms` | 否 | 单次 HTTP 调用超时,单位毫秒;默认 30000。 | +| `maxDiffBytes` | `max_diff_bytes` | 否 | `diff` 响应字节数客户端上限;默认 1 MiB。 | +| `maxLogEntries` | `max_log_entries` | 否 | `log` 条目数客户端上限;默认 200。 | + +## 选择后端 + +- 普通开发机和 CI checkout 使用默认本地 workspace。 +- 宿主希望总是传 typed backend object 时,使用 `LocalWorkspaceBackend`。 +- workspace 状态必须落在对象存储中时,使用 `S3WorkspaceBackend`。 +- 非本地 workspace 仍需要内置 `git` 工具时,追加 `remoteGit`。 +- 当前 backend 不能直接运行 shell 时,通过 MCP 或 A3S Box 提供执行能力。 diff --git a/website/docs/v6/zh/index.mdx b/website/docs/v6/zh/index.mdx new file mode 100644 index 00000000..471fc6fe --- /dev/null +++ b/website/docs/v6/zh/index.mdx @@ -0,0 +1,7 @@ +--- +pageType: home +title: A3S Code +description: 可治理的编码 Agent 运行时,让工具、策略、事件与持久化证据保持显式。 +sidebar: false +outline: false +--- diff --git a/website/package-lock.json b/website/package-lock.json new file mode 100644 index 00000000..3dc949aa --- /dev/null +++ b/website/package-lock.json @@ -0,0 +1,3536 @@ +{ + "name": "a3s-code-website", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "a3s-code-website", + "version": "0.0.0", + "dependencies": { + "@rspress/core": "^2.0.17" + }, + "devDependencies": { + "@rslint/core": "^0.5.3", + "@types/node": "^22.8.1", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "prettier": "^3.8.3", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "typescript": "^6.0.3" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@rsbuild/core": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.1.8.tgz", + "integrity": "sha512-Y70LMcCZspVoQ7Oip1W2Agu5wVhWZ2x3cYl4s9GLQG4VYphBud53DY/jkrqkyQF8ASYZDDDrW2KWNFj0kNLLuA==", + "license": "MIT", + "dependencies": { + "@rspack/core": "~2.1.5", + "@swc/helpers": "^0.5.23" + }, + "bin": { + "rsbuild": "bin/rsbuild.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "core-js": ">= 3.0.0" + }, + "peerDependenciesMeta": { + "core-js": { + "optional": true + } + } + }, + "node_modules/@rsbuild/plugin-react": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-2.1.0.tgz", + "integrity": "sha512-RQTIAWB/CwPjoWt9iAl+8HixeQVgZ7kEIBrWPCixfITyHdiD84h0YpUTpEUuz6kGHw1KXT9mHZ3Rwy6WG7aRDA==", + "license": "MIT", + "dependencies": { + "@rspack/plugin-react-refresh": "^2.0.2", + "react-refresh": "^0.18.0" + }, + "peerDependencies": { + "@rsbuild/core": "^2.0.0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + } + } + }, + "node_modules/@rslint/core": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@rslint/core/-/core-0.5.3.tgz", + "integrity": "sha512-7s7Yb9L0mwI2sl06G5HTI7ENd1iJiGYFVWbHjeJbXa7bLM5rPwkgWsYMRZo4kYQIYqpqD4Oqraor+zawEcunkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "4.0.4", + "tinyglobby": "0.2.15" + }, + "bin": { + "rslint": "bin/rslint.cjs" + }, + "optionalDependencies": { + "@rslint/darwin-arm64": "0.5.3", + "@rslint/darwin-x64": "0.5.3", + "@rslint/linux-arm64": "0.5.3", + "@rslint/linux-x64": "0.5.3", + "@rslint/win32-arm64": "0.5.3", + "@rslint/win32-x64": "0.5.3" + }, + "peerDependencies": { + "jiti": "^2.0.0" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/@rslint/darwin-arm64": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@rslint/darwin-arm64/-/darwin-arm64-0.5.3.tgz", + "integrity": "sha512-g/VSIw1/slkvQsNRbHpImAip/sCjEaqE/Z5ZE6plL7GXdeztWckSevu+J5v07JmILvebJ7ZUgFvMw/ydTdxofw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rslint/darwin-x64": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@rslint/darwin-x64/-/darwin-x64-0.5.3.tgz", + "integrity": "sha512-+29fPLRjkNVQW7DAtgNkQH6rYg1CdjJYHhBhRHT8TJWU8+N3GrhWjbxEvW9XThkABpDqsxAGS86wCE060QuRpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rslint/linux-arm64": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@rslint/linux-arm64/-/linux-arm64-0.5.3.tgz", + "integrity": "sha512-gpvnMVVP0wPe0T9qeITkaBB9WldxQPnJP8u314cG4sDyGV/qISKvUOPPk4AM3YG0f4UU7FA9gUySLptHLcDOFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rslint/linux-x64": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@rslint/linux-x64/-/linux-x64-0.5.3.tgz", + "integrity": "sha512-/ZzTWlZwi+Ff74tbnGYgLUc7YTDVsNuaL3EeZFuFIYk02Lge9UCCJywMExoLVTdhrZh7fRWu6nlS6sf1/okb8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rslint/win32-arm64": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@rslint/win32-arm64/-/win32-arm64-0.5.3.tgz", + "integrity": "sha512-l5T5VeKQKjLMpUqahU/UDRV0UQcjvxN+r6bOSl+do0xCsYXpfEiI6HlE33//+VilJ120t2SI5e8Hnidaha3XEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rslint/win32-x64": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@rslint/win32-x64/-/win32-x64-0.5.3.tgz", + "integrity": "sha512-aPbtXTNeM9A5pA3krRzEaHGA6RLLkG5k8naNWGIfQnwfjMCk6vzjTyL6Wceg6Oq+qjTDnBLzU3RqZhpXwPeJ7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.1.5.tgz", + "integrity": "sha512-lF3ZLeeyV0AN3BL0m2jAmNZD5pP9IHsQ8gUXN7mo0g4xsW6nf6hsN7o9CnEHECz5uUZb+EoWsuWzAAmnA9Ip8Q==", + "license": "MIT", + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "2.1.5", + "@rspack/binding-darwin-x64": "2.1.5", + "@rspack/binding-linux-arm64-gnu": "2.1.5", + "@rspack/binding-linux-arm64-musl": "2.1.5", + "@rspack/binding-linux-riscv64-gnu": "2.1.5", + "@rspack/binding-linux-riscv64-musl": "2.1.5", + "@rspack/binding-linux-x64-gnu": "2.1.5", + "@rspack/binding-linux-x64-musl": "2.1.5", + "@rspack/binding-wasm32-wasi": "2.1.5", + "@rspack/binding-win32-arm64-msvc": "2.1.5", + "@rspack/binding-win32-ia32-msvc": "2.1.5", + "@rspack/binding-win32-x64-msvc": "2.1.5" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.1.5.tgz", + "integrity": "sha512-XLAN6YSU36qkciJtV9DY9z57pdHOjKy/f1HyoqpZenRg8p46jnXPziV45lfrTZpG+FF6g/jtTUc4dKbeyoWqPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.1.5.tgz", + "integrity": "sha512-KLW7PV86jyCOyqJSqrkZdvYUWYCFX/Q4LsGmVc8tyDA8jBYLMHJDewC/lNf9ot3rU2SoLDZKhDUh7q7dX0FRmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.1.5.tgz", + "integrity": "sha512-KOooAC8L+Ljx5sY7ZuMeaXCQ310FNWaPWXcYQJpFPApF3qyLeSfuOftUGwxcxeo5U0mS5OY3zxp7/5CaHWKYjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.1.5.tgz", + "integrity": "sha512-0GFc07gT71+uRbHtejDecBfSL0fs7dQAwCtXYieXboACauL7UvS3Hwr6v8A91aGtAcvLotnCaIa3CWWYwtYlYQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-riscv64-gnu": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-2.1.5.tgz", + "integrity": "sha512-isQQDp3fBzPSZpLVFzPqVXIVA4I9b9mKs58TfUgOzDP/1g1582YSV3iFopgFogvEliihXDuuXvM6aAkP+w8Z+Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-riscv64-musl": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-2.1.5.tgz", + "integrity": "sha512-/SP6VknY4kH60BYplI2FDNAJCo4U4DUszLxhKsgVlgA5ImgHC8Ew2AsKgidk7ceiYV9OcKzYuWYe9tVpysBYVA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.1.5.tgz", + "integrity": "sha512-/GJmS+buA4pilT10gs5mfAhAZxSHXpDD8veZ3QpYvNUuoU9gvempAq9TgjbyNN/vc5pf76GdXPXKFMiehudYSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.1.5.tgz", + "integrity": "sha512-iwS3t75nZ2TnUjB05KtvpvjpdkOy/nLxbaOnwjbdnNZZXpUaMadLbllGAWayHPGCMsL2yKBEhVEESZpR7tK3SA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.1.5.tgz", + "integrity": "sha512-jOhW69t1H/jcRaSMB5U8jVzWP18j0YQIlo0nT8W+KvNVScJeyOze4FFc+5RIS7VmuT8/jid7hUyydZOfm6UgqA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "1.1.6" + } + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.1.5.tgz", + "integrity": "sha512-OLfMXmtFBcrWr9BRGKXJYrWgYR0JSaoAWr3EVFDjKGi/YR5aUwQSGc3AJPBph9g0YsdSqRuUhtQGtCk84DeeEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.1.5.tgz", + "integrity": "sha512-p4OSmnj21+AY8vpIADveLEBXfXJRXonOTYim2VLVWV4TbXfhYNMLiX3qESHCdNnn6lVJM0q6zxxzEUfTyAKydw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.1.5.tgz", + "integrity": "sha512-YCkLLDGMkHsw5CpcsFtFhzCu+JZEsNrcH9O1GpWMkyxR2ku2Fq1i2wEHYSm26HyW+PJ3+Fv347MPWMFZIz9q2g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/core": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.1.5.tgz", + "integrity": "sha512-YHjL7xVXycAWsjJtF39cRZ2u+ddiNFxY+FcNgEBe6Y8OaLeUfMfjba3gK+B1Oj32UcKD6BmXGsPfu9p+OII/Yw==", + "license": "MIT", + "dependencies": { + "@rspack/binding": "2.1.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@module-federation/runtime-tools": "^0.24.1 || ^2.0.0", + "@swc/helpers": "^0.5.23" + }, + "peerDependenciesMeta": { + "@module-federation/runtime-tools": { + "optional": true + }, + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/plugin-react-refresh": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-2.0.2.tgz", + "integrity": "sha512-dGNZiCxQxgAUI9sah7gd8u+O7OJZRCmqtEJNDOd8xW5RqcieC86F7p5qcShyw6onH5pKf57evpr2VjGbaFGkZg==", + "license": "MIT", + "peerDependencies": { + "@rspack/core": "^2.0.0", + "react-refresh": ">=0.10.0 <1.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + } + } + }, + "node_modules/@rspress/core": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.18.tgz", + "integrity": "sha512-DBpsr/6XAItQkZPL5FSjthLGuzdG9ks/7EOxkqaaXp93uAT54LO0PD5Dkoy6ydup1PnvkpHyiHpUyDcnHHTVhg==", + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.1.1", + "@mdx-js/react": "^3.1.1", + "@rsbuild/core": "^2.1.5", + "@rsbuild/plugin-react": "~2.1.0", + "@rspress/shared": "2.0.18", + "@shikijs/rehype": "^4.2.0", + "@types/unist": "^3.0.3", + "@unhead/react": "^2.1.15", + "body-scroll-lock": "4.0.0-beta.0", + "clsx": "2.1.1", + "copy-to-clipboard": "^3.3.3", + "flexsearch": "0.8.212", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.3.6", + "mdast-util-mdx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.1", + "medium-zoom": "1.1.0", + "nprogress": "^0.2.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-lazy-with-preload": "^2.2.1", + "react-reconciler": "0.33.0", + "react-render-to-markdown": "19.1.0", + "react-router-dom": "^7.18.1", + "rehype-external-links": "^3.0.0", + "rehype-raw": "^7.0.0", + "remark-cjk-friendly": "^2.3.1", + "remark-cjk-friendly-gfm-strikethrough": "^2.3.1", + "remark-gfm": "^4.0.1", + "remark-mdx": "^3.1.1", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "scroll-into-view-if-needed": "^3.1.0", + "shiki": "^4.2.0", + "unified": "^11.0.5", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-children": "^3.0.0" + }, + "bin": { + "rspress": "bin/rspress.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rspress/shared": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.18.tgz", + "integrity": "sha512-GJswqJQCPSxvBt5r+gJzz8Em8EEK/sOmCQjTpemTvldvxr1Lva85BGVnSQZOgofnM0nrjt18kn4mmnuuSstbpA==", + "license": "MIT", + "dependencies": { + "@rsbuild/core": "^2.1.5", + "@shikijs/rehype": "^4.2.0", + "unified": "^11.0.5" + } + }, + "node_modules/@shikijs/core": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.1.tgz", + "integrity": "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.3.1", + "@shikijs/types": "4.3.1", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.1.tgz", + "integrity": "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.1.tgz", + "integrity": "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.1.tgz", + "integrity": "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.1.tgz", + "integrity": "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/rehype": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.3.1.tgz", + "integrity": "sha512-oshrlfUF3VPUJfnp5K1lLwsS/SRBKrIxONpdWebSKZXdBE3UsZnxgqpvRUA8UsofS7vmjFOCAHIT71ECbmOxTw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1", + "@types/hast": "^3.0.4", + "hast-util-to-string": "^3.0.1", + "shiki": "4.3.1", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.1.tgz", + "integrity": "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.1.tgz", + "integrity": "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@unhead/react": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.16.tgz", + "integrity": "sha512-FhKVVHAsHbiD24yNAklQIAbjGmcVLTJjKFx1YEhOzGmiZaH2q6CzbwxboJYK6oasepio8Qap9UqHhudJ98wTbA==", + "license": "MIT", + "dependencies": { + "unhead": "2.1.16" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "react": ">=18.3.1" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/body-scroll-lock": { + "version": "4.0.0-beta.0", + "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz", + "integrity": "sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==", + "license": "MIT" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flexsearch": { + "version": "0.8.212", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.8.212.tgz", + "integrity": "sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/ts-thomas" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=GEVR88FC9BWRW" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/flexsearch" + }, + { + "type": "patreon", + "url": "https://patreon.com/user?u=96245532" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/ts-thomas" + } + ], + "license": "Apache-2.0" + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", + "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown-cjk-friendly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown-cjk-friendly/-/mdast-util-to-markdown-cjk-friendly-1.0.0.tgz", + "integrity": "sha512-BoaAm8mlJ+LAYz0Qs532Y3ciTuQYgBUPZcSFbvC/ZKmEMAKgulw84YvQK1gI34t/vL2euSfuaWlqczkTBgamkw==", + "license": "MIT", + "dependencies": { + "mdast-util-to-markdown": "^2.1.2", + "micromark-extension-cjk-friendly-util": "3.0.1", + "micromark-util-symbol": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/mdast": "*" + }, + "peerDependenciesMeta": { + "@types/mdast": { + "optional": true + } + } + }, + "node_modules/mdast-util-to-markdown-cjk-friendly-gfm-strikethrough": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown-cjk-friendly-gfm-strikethrough/-/mdast-util-to-markdown-cjk-friendly-gfm-strikethrough-1.0.0.tgz", + "integrity": "sha512-1ePVfB4P/vz3xSsm6H3D32r6VYGErxclnuLLFK02/2ReF+UdEKm7caulK6Vm0LBIp5gPRtB2Z1OYDznCkX3k2w==", + "license": "MIT", + "dependencies": { + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-to-markdown": "^2.1.2", + "micromark-extension-cjk-friendly-util": "3.0.1", + "micromark-util-symbol": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/mdast": "*" + }, + "peerDependenciesMeta": { + "@types/mdast": { + "optional": true + } + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/medium-zoom": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.1.0.tgz", + "integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==", + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-cjk-friendly": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-cjk-friendly/-/micromark-extension-cjk-friendly-2.0.1.tgz", + "integrity": "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.1.0", + "micromark-extension-cjk-friendly-util": "3.0.1", + "micromark-util-chunked": "^2.0.1", + "micromark-util-resolve-all": "^2.0.1", + "micromark-util-symbol": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "micromark": "^4.0.0", + "micromark-util-types": "^2.0.0" + }, + "peerDependenciesMeta": { + "micromark-util-types": { + "optional": true + } + } + }, + "node_modules/micromark-extension-cjk-friendly-gfm-strikethrough": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-cjk-friendly-gfm-strikethrough/-/micromark-extension-cjk-friendly-gfm-strikethrough-2.0.1.tgz", + "integrity": "sha512-wVC0zwjJNqQeX+bb07YTPu/CvSAyCTafyYb7sMhX1r62/Lw5M/df3JyYaANyp8g15c1ypJRFSsookTqA1IDsUg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.1.0", + "get-east-asian-width": "^1.4.0", + "micromark-extension-cjk-friendly-util": "3.0.1", + "micromark-util-character": "^2.1.1", + "micromark-util-chunked": "^2.0.1", + "micromark-util-resolve-all": "^2.0.1", + "micromark-util-symbol": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "micromark": "^4.0.0", + "micromark-util-types": "^2.0.0" + }, + "peerDependenciesMeta": { + "micromark-util-types": { + "optional": true + } + } + }, + "node_modules/micromark-extension-cjk-friendly-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-cjk-friendly-util/-/micromark-extension-cjk-friendly-util-3.0.1.tgz", + "integrity": "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.4.0", + "micromark-util-character": "^2.1.1", + "micromark-util-symbol": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependenciesMeta": { + "micromark-util-types": { + "optional": true + } + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-lazy-with-preload": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-lazy-with-preload/-/react-lazy-with-preload-2.2.1.tgz", + "integrity": "sha512-ONSb8gizLE5jFpdHAclZ6EAAKuFX2JydnFXPPPjoUImZlLjGtKzyBS8SJgJq7CpLgsGKh9QCZdugJyEEOVC16Q==", + "license": "MIT" + }, + "node_modules/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-render-to-markdown": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-render-to-markdown/-/react-render-to-markdown-19.1.0.tgz", + "integrity": "sha512-dF9b3tO41ezqdmHP8X92kbHbMexJ6iC7iHw4ykC8fwiO7DgpFc9PhMoKlI+BcPzRxGcWgQSdrixVB9RykhjJpQ==", + "license": "MIT", + "dependencies": { + "react-reconciler": "0.33.0" + }, + "peerDependencies": { + "react": ">=19" + } + }, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-cjk-friendly": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/remark-cjk-friendly/-/remark-cjk-friendly-2.3.1.tgz", + "integrity": "sha512-f+pKZRxCRwNEGFBKNRAZAqU91GIK1SAo3ZyFHWRUgC9zcxRR0BXKd6YwqgSsxtW0rNpUDtONj7H5nje2WL3fcA==", + "license": "MIT", + "dependencies": { + "mdast-util-to-markdown-cjk-friendly": "1.0.0", + "micromark-extension-cjk-friendly": "2.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/mdast": "^4.0.0", + "unified": "^11.0.0" + }, + "peerDependenciesMeta": { + "@types/mdast": { + "optional": true + } + } + }, + "node_modules/remark-cjk-friendly-gfm-strikethrough": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.3.1.tgz", + "integrity": "sha512-JE3TGgouk/sy92SemNMEUhO5mNP4on04cmzOV3s3R5Dbk160ewmpM4tgPiinKKvoJ5UW2fTu7FOYsjVbusSA9w==", + "license": "MIT", + "dependencies": { + "mdast-util-to-markdown-cjk-friendly-gfm-strikethrough": "1.0.0", + "micromark-extension-cjk-friendly-gfm-strikethrough": "2.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/mdast": "^4.0.0", + "unified": "^11.0.0" + }, + "peerDependenciesMeta": { + "@types/mdast": { + "optional": true + } + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shiki": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.1.tgz", + "integrity": "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.3.1", + "@shikijs/engine-javascript": "4.3.1", + "@shikijs/engine-oniguruma": "4.3.1", + "@shikijs/langs": "4.3.1", + "@shikijs/themes": "4.3.1", + "@shikijs/types": "4.3.1", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unhead": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.16.tgz", + "integrity": "sha512-cD2Q4a6SQHhW9/bPp17NT4GJ1yS0DSLe75WEHKQ8bKa/wjB6cIki3KeL86hhllNBcpv8i6bxdwtwQunneXPqKQ==", + "license": "MIT", + "dependencies": { + "hookable": "^6.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", + "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..bdd71ad1 --- /dev/null +++ b/website/package.json @@ -0,0 +1,30 @@ +{ + "name": "a3s-code-website", + "version": "0.0.0", + "description": "Official multilingual website and documentation for A3S Code.", + "private": true, + "type": "module", + "scripts": { + "build": "rspress build", + "check:site": "node scripts/check-built-site.mjs", + "dev": "rspress dev", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "rslint && tsc --noEmit && node scripts/check-language-parity.mjs", + "normalize:docs": "node scripts/normalize-imported-docs.mjs", + "preview": "rspress preview" + }, + "dependencies": { + "@rspress/core": "^2.0.17" + }, + "devDependencies": { + "@rslint/core": "^0.5.3", + "@types/node": "^22.8.1", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "prettier": "^3.8.3", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "typescript": "^6.0.3" + } +} diff --git a/website/rslint.config.ts b/website/rslint.config.ts new file mode 100644 index 00000000..951771a3 --- /dev/null +++ b/website/rslint.config.ts @@ -0,0 +1,14 @@ +import { + defineConfig, + js, + ts, + reactPlugin, + reactHooksPlugin, +} from '@rslint/core'; + +export default defineConfig([ + js.configs.recommended, + ts.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); diff --git a/website/rspress.config.ts b/website/rspress.config.ts new file mode 100644 index 00000000..18d7ad28 --- /dev/null +++ b/website/rspress.config.ts @@ -0,0 +1,83 @@ +import * as path from 'node:path'; +import { defineConfig } from '@rspress/core'; + +const base = process.env.DOCS_BASE ?? '/Code/'; +const siteOrigin = process.env.DOCS_ORIGIN ?? 'https://a3s-lab.github.io'; + +export default defineConfig({ + root: path.join(__dirname, 'docs'), + base, + siteOrigin, + title: 'A3S Code', + description: + 'A governed coding-agent runtime with explicit tools, policy, events, and durable evidence.', + lang: 'zh', + icon: '/favicon.svg', + logo: '/a3s-code-mark.svg', + logoText: 'A3S Code', + outDir: 'doc_build', + llms: true, + multiVersion: { + default: 'v6', + versions: ['v6'], + }, + locales: [ + { + lang: 'zh', + label: '简体中文', + title: 'A3S Code', + description: + '可治理的编码 Agent 运行时,让工具、策略、事件与持久化证据保持显式。', + }, + { + lang: 'en', + label: 'English', + title: 'A3S Code', + description: + 'A governed coding-agent runtime with explicit tools, policy, events, and durable evidence.', + }, + ], + head: [ + ['meta', { name: 'theme-color', content: '#0b0b0d' }], + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { property: 'og:site_name', content: 'A3S Code' }], + [ + 'meta', + { + property: 'og:image', + content: `${siteOrigin}${base}social-card.svg`, + }, + ], + ['meta', { name: 'twitter:card', content: 'summary_large_image' }], + (route) => [ + 'link', + { + rel: 'canonical', + href: `${siteOrigin}${base.replace(/\/$/, '')}${route.routePath}`, + }, + ], + ], + themeConfig: { + darkMode: 'force-dark', + search: true, + localeRedirect: 'never', + enableContentAnimation: true, + editLink: { + docRepoBaseUrl: 'https://github.com/A3S-Lab/Code/tree/main/website/docs', + }, + lastUpdated: { + author: true, + }, + llmsUI: { + placement: 'outline', + viewOptions: ['markdownLink', 'chatgpt', 'claude'], + }, + socialLinks: [ + { + icon: 'github', + mode: 'link', + content: 'https://github.com/A3S-Lab/Code', + }, + ], + }, +}); diff --git a/website/scripts/check-built-site.mjs b/website/scripts/check-built-site.mjs new file mode 100644 index 00000000..2e291fe0 --- /dev/null +++ b/website/scripts/check-built-site.mjs @@ -0,0 +1,104 @@ +import { access, readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const websiteRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const outputRoot = path.join(websiteRoot, 'doc_build'); +const base = '/Code/'; + +const requiredFiles = [ + 'index.html', + 'en/index.html', + 'guide/index.html', + 'en/guide/index.html', + 'api/index.html', + 'en/api/index.html', + 'llms.txt', + 'llms-full.txt', + 'en/llms.txt', + 'en/llms-full.txt', + 'a3s-code-mark.svg', + 'social-card.svg', +]; + +async function collectHtmlFiles(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const absolutePath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...(await collectHtmlFiles(absolutePath))); + } else if (entry.name.endsWith('.html')) { + files.push(absolutePath); + } + } + + return files; +} + +for (const file of requiredFiles) { + await access(path.join(outputRoot, file)); +} + +const brokenReferences = []; +const htmlFiles = await collectHtmlFiles(outputRoot); +const referencePattern = /(?:href|src)="([^"]+)"/g; + +for (const htmlFile of htmlFiles) { + const html = await readFile(htmlFile, 'utf8'); + + for (const [, rawReference] of html.matchAll(referencePattern)) { + if ( + rawReference.startsWith('#') || + rawReference.startsWith('data:') || + rawReference.startsWith('mailto:') || + /^[a-z]+:\/\//i.test(rawReference) + ) { + continue; + } + + if (rawReference.startsWith('/') && !rawReference.startsWith(base)) { + brokenReferences.push( + `${path.relative(outputRoot, htmlFile)} -> ${rawReference} (outside ${base})`, + ); + continue; + } + + if (!rawReference.startsWith(base)) { + continue; + } + + const withoutBase = rawReference + .slice(base.length) + .split(/[?#]/, 1)[0] + .replace(/\/+/g, '/'); + const outputPath = + withoutBase === '' || withoutBase.endsWith('/') + ? path.join(outputRoot, withoutBase, 'index.html') + : path.join(outputRoot, withoutBase); + + try { + await access(outputPath); + } catch { + brokenReferences.push( + `${path.relative(outputRoot, htmlFile)} -> ${rawReference}`, + ); + } + } +} + +if (brokenReferences.length > 0) { + throw new Error( + `Built-site reference check failed:\n${brokenReferences + .map((reference) => ` - ${reference}`) + .join('\n')}`, + ); +} + +console.log( + `Built-site references verified across ${htmlFiles.length} HTML pages.`, +); diff --git a/website/scripts/check-language-parity.mjs b/website/scripts/check-language-parity.mjs new file mode 100644 index 00000000..c9618edf --- /dev/null +++ b/website/scripts/check-language-parity.mjs @@ -0,0 +1,68 @@ +import { readdir } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const websiteRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const docsRoot = path.join(websiteRoot, 'docs'); +const versions = ['v6']; +const languages = ['zh', 'en']; + +async function collectMarkdownFiles(directory, prefix = '') { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const relativePath = path.posix.join(prefix, entry.name); + const absolutePath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + files.push(...(await collectMarkdownFiles(absolutePath, relativePath))); + } else if (/\.(md|mdx)$/.test(entry.name)) { + files.push(relativePath); + } + } + + return files.sort(); +} + +for (const version of versions) { + const filesByLanguage = new Map(); + + for (const language of languages) { + const languageRoot = path.join(docsRoot, version, language); + filesByLanguage.set( + language, + new Set(await collectMarkdownFiles(languageRoot)), + ); + } + + const allFiles = new Set( + [...filesByLanguage.values()].flatMap((files) => [...files]), + ); + const missing = []; + + for (const file of [...allFiles].sort()) { + for (const language of languages) { + if (!filesByLanguage.get(language)?.has(file)) { + missing.push(`${version}/${language}/${file}`); + } + } + } + + if (missing.length > 0) { + throw new Error( + `Language parity check failed. Missing files:\n${missing + .map((file) => ` - ${file}`) + .join('\n')}`, + ); + } + + console.log( + `Language parity verified for ${version}: ${allFiles.size} pages in ${languages.join( + ', ', + )}.`, + ); +} diff --git a/website/scripts/normalize-imported-docs.mjs b/website/scripts/normalize-imported-docs.mjs new file mode 100644 index 00000000..95c0b7c5 --- /dev/null +++ b/website/scripts/normalize-imported-docs.mjs @@ -0,0 +1,61 @@ +import { readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)); +const websiteDirectory = path.resolve(scriptDirectory, '..'); +const docsDirectory = path.join(websiteDirectory, 'docs', 'v6'); + +async function* walk(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + yield* walk(entryPath); + } else { + yield entryPath; + } + } +} + +function normalizeContent(content) { + return content + .replace( + "import { Tab, Tabs } from 'fumadocs-ui/components/tabs';", + "import { Tab, Tabs } from '@rspress/core/theme';", + ) + .replace( + //g, + '', + ) + .replace(//g, '') + .replaceAll('/cn/docs/code/', '/guide/') + .replaceAll('/cn/docs/code)', '/guide/)') + .replaceAll('/docs/code/', '/guide/') + .replaceAll('/docs/code)', '/guide/)'); +} + +let changed = 0; +let removed = 0; + +for await (const filePath of walk(docsDirectory)) { + if (path.basename(filePath) === 'meta.json') { + await rm(filePath); + removed += 1; + continue; + } + + if (!filePath.endsWith('.md') && !filePath.endsWith('.mdx')) { + continue; + } + + const before = await readFile(filePath, 'utf8'); + const after = normalizeContent(before); + if (after !== before) { + await writeFile(filePath, after); + changed += 1; + } +} + +console.log( + `Normalized ${changed} documentation files and removed ${removed} legacy metadata files.`, +); diff --git a/website/theme/components/HomeLayout.tsx b/website/theme/components/HomeLayout.tsx new file mode 100644 index 00000000..5d85a83b --- /dev/null +++ b/website/theme/components/HomeLayout.tsx @@ -0,0 +1,867 @@ +import { useState } from 'react'; +import type { CSSProperties } from 'react'; +import { useLang, useSite, useVersion, withBase } from '@rspress/core/runtime'; + +type Locale = 'zh' | 'en'; + +type Localized = { + zh: string; + en: string; +}; + +type Feature = { + index: string; + title: Localized; + body: Localized; + tags: string[]; +}; + +const installCommands = [ + { + id: 'terminal', + label: 'Terminal', + command: 'brew install A3S-Lab/tap/a3s\ncd /path/to/project\na3s code', + }, + { + id: 'rust', + label: 'Rust', + command: 'cargo add a3s-code-core', + }, + { + id: 'node', + label: 'Node.js', + command: 'npm install @a3s-lab/code', + }, + { + id: 'python', + label: 'Python', + command: 'python -m pip install a3s-code', + }, +] as const; + +const governanceFeatures: Feature[] = [ + { + index: '01', + title: { + zh: '治理每一次副作用', + en: 'Govern every side effect', + }, + body: { + zh: '参数校验、能力声明、权限、人工确认、Hook、预算、安全提供者与取消,共享同一条工具调用路径。', + en: 'Validation, capabilities, permissions, confirmation, hooks, budgets, security providers, and cancellation share one invocation path.', + }, + tags: ['policy', 'HITL', 'sandbox'], + }, + { + index: '02', + title: { + zh: '让上下文保持有界', + en: 'Keep context bounded', + }, + body: { + zh: '读取、搜索、命令输出、Git 与网页证据使用范围或游标;大型结果进入带预览、大小与哈希的有界 Artifact。', + en: 'Reads, searches, command output, Git, and web evidence use ranges or cursors. Large results become bounded artifacts with previews, sizes, and hashes.', + }, + tags: ['cursor', 'artifact', 'hash'], + }, + { + index: '03', + title: { + zh: '拥有自己的 UI', + en: 'Own the UI', + }, + body: { + zh: 'Core 发出 AgentEvent;SDK 流与持久化 Run 使用无损 EventEnvelopeV1。宿主可以选择呈现方式,而不必分叉 Agent Loop。', + en: 'Core emits AgentEvent while SDK streams and persisted runs use lossless EventEnvelopeV1. Hosts choose presentation without forking the agent loop.', + }, + tags: ['AgentEvent', 'EventEnvelopeV1'], + }, + { + index: '04', + title: { + zh: '从证据恢复', + en: 'Resume from evidence', + }, + body: { + zh: 'SessionSnapshotV1 可以把会话、Run、Artifact、Trace、验证报告与子任务记录原子提交为一个 Generation。', + en: 'SessionSnapshotV1 can atomically commit sessions, runs, artifacts, traces, verification reports, and child-task records as one generation.', + }, + tags: ['snapshot', 'replay', 'verification'], + }, +]; + +const capabilityCards = [ + { + className: 'a3s-bento-card--wide a3s-bento-card--policy', + eyebrow: { zh: '调用内核', en: 'INVOCATION KERNEL' }, + title: { + zh: '工具可用,不等于工具越权', + en: 'Available never means ungoverned', + }, + body: { + zh: '文件、搜索、Shell、Git、Web、Batch、QuickJS、结构化生成与委派,都在 Workspace 能力和宿主策略允许时才暴露。', + en: 'Files, search, shell, Git, web, batch, QuickJS, structured generation, and delegation are exposed only when workspace capability and host policy allow.', + }, + tags: ['files', 'shell', 'git', 'web', 'program', 'task'], + }, + { + className: 'a3s-bento-card--models', + eyebrow: { zh: '模型适配', en: 'MODEL ADAPTERS' }, + title: { + zh: '统一生命周期,不锁定提供商', + en: 'One lifecycle, no provider lock-in', + }, + body: { + zh: 'Anthropic、智谱、OpenAI-compatible API,或宿主注入的 LlmClient。', + en: 'Anthropic, Zhipu, OpenAI-compatible APIs, or a host-injected LlmClient.', + }, + tags: ['streaming', 'tools', 'structured output'], + }, + { + className: 'a3s-bento-card--state', + eyebrow: { zh: '持久状态', en: 'DURABLE STATE' }, + title: { + zh: 'Run、Trace、Artifact 与 Snapshot', + en: 'Runs, traces, artifacts, and snapshots', + }, + body: { + zh: '原子快照、事件回放、验证证据、Checkpoint,以及可选的 State Graph / Flow 投影。', + en: 'Atomic snapshots, event replay, verification evidence, checkpoints, and optional State Graph / Flow projection.', + }, + tags: ['atomic', 'replayable', 'auditable'], + }, + { + className: 'a3s-bento-card--extend', + eyebrow: { zh: '扩展边界', en: 'EXTENSION BOUNDARIES' }, + title: { + zh: '用显式契约扩展运行时', + en: 'Extend through explicit contracts', + }, + body: { + zh: 'MCP、Skills、ContextProvider、MemoryStore、SessionStore、自定义工具与 Workspace 服务保持可替换。', + en: 'MCP, Skills, ContextProvider, MemoryStore, SessionStore, custom tools, and workspace services stay replaceable.', + }, + tags: ['MCP', 'Skills', 'traits'], + }, + { + className: 'a3s-bento-card--wide a3s-bento-card--workspace', + eyebrow: { zh: '工作区', en: 'WORKSPACE' }, + title: { + zh: '代码智能与工具遵守同一个 Workspace', + en: 'Code intelligence and tools share one workspace boundary', + }, + body: { + zh: '符号、定义、引用、实现、诊断与修订信息由宿主选择的 Workspace 提供;不具备本地能力的后端不会向模型声明本地 Bash 或 Git。', + en: 'Symbols, definitions, references, implementations, diagnostics, and revisions come from the host-selected workspace. A backend without local capability never advertises local Bash or Git.', + }, + tags: ['symbols', 'diagnostics', 'local / S3 / remote'], + }, +]; + +const surfaces = [ + { + key: 'terminal', + name: 'Terminal', + packageName: 'a3s code', + href: 'https://github.com/A3S-Lab/CLI', + description: { + zh: '现成的交互式编码产品,渲染推理、工具、审批、任务进度与 Diff。', + en: 'The ready interactive coding product for reasoning, tools, approvals, task progress, and diffs.', + }, + command: 'brew install A3S-Lab/tap/a3s', + }, + { + key: 'rust', + name: 'Rust', + packageName: 'a3s-code-core', + href: 'https://crates.io/crates/a3s-code-core', + description: { + zh: '完整的异步运行时 API 与公共扩展 Trait。', + en: 'The complete async runtime API and public extension traits.', + }, + command: 'cargo add a3s-code-core', + }, + { + key: 'node', + name: 'Node.js', + packageName: '@a3s-lab/code', + href: 'https://www.npmjs.com/package/@a3s-lab/code', + description: { + zh: '基于 N-API 的原生绑定,覆盖生命周期、事件流、工具、Store、编排与 MCP。', + en: 'Native N-API bindings for lifecycle, streams, tools, stores, orchestration, and MCP.', + }, + command: 'npm install @a3s-lab/code', + }, + { + key: 'python', + name: 'Python', + packageName: 'a3s-code', + href: 'https://pypi.org/project/a3s-code/', + description: { + zh: '基于 PyO3 的原生包,提供同步与异步应用 API。', + en: 'A native PyO3 package with synchronous and asynchronous application APIs.', + }, + command: 'python -m pip install a3s-code', + }, +]; + +const runtimeLayers = [ + { + id: 'surfaces', + code: 'L01 / SURFACES', + title: { zh: '产品入口', en: 'Product surfaces' }, + body: { + zh: 'Terminal、Rust、Node.js 与 Python 进入同一套执行语义;呈现方式可以不同,Runtime Contract 保持一致。', + en: 'Terminal, Rust, Node.js, and Python enter the same execution semantics. Presentation varies while the runtime contract stays consistent.', + }, + tags: ['a3s code', 'Rust', 'Node.js', 'Python'], + }, + { + id: 'session', + code: 'L02 / AGENT API', + title: { zh: 'Agent 与 Session', en: 'Agent and session' }, + body: { + zh: 'Agent 持有解析后的配置与共享能力;AgentSession 把它们绑定到一个 Workspace 和一段对话生命周期。', + en: 'Agent owns resolved configuration and shared capabilities. AgentSession binds them to one workspace and conversation lifecycle.', + }, + tags: ['Agent', 'AgentSession', 'lifecycle'], + }, + { + id: 'context', + code: 'L03 / INTELLIGENCE', + title: { zh: '上下文、记忆与模型', en: 'Context, memory, and models' }, + body: { + zh: 'ContextAssembler 排序并预算输入;Memory 保留可复用事实;模型适配器统一流、工具调用、结构化输出与取消。', + en: 'ContextAssembler ranks and budgets inputs, memory retains reusable facts, and model adapters normalize streaming, tool calls, structured output, and cancellation.', + }, + tags: ['ContextAssembler', 'Memory', 'LlmClient'], + }, + { + id: 'governance', + code: 'L04 / GOVERNANCE', + title: { zh: '治理内核', en: 'Governance kernel' }, + body: { + zh: '每个副作用依次经过参数校验、能力检查、权限、人工确认、预算、安全提供者、沙箱与取消边界。', + en: 'Every side effect crosses argument validation, capability checks, permissions, human confirmation, budgets, security providers, sandboxing, and cancellation.', + }, + tags: ['validate', 'permission', 'confirm', 'budget'], + }, + { + id: 'tools', + code: 'L05 / WORKSPACE', + title: { zh: 'Workspace 与工具', en: 'Workspace and tools' }, + body: { + zh: '文件、搜索、Shell、Git、Web、代码智能、MCP、Skills 与委派只在 Workspace 能力和策略共同允许时注册。', + en: 'Files, search, shell, Git, web, code intelligence, MCP, Skills, and delegation register only when workspace capability and policy both allow.', + }, + tags: ['files', 'git', 'web', 'MCP', 'Skills'], + }, + { + id: 'evidence', + code: 'L06 / DURABILITY', + title: { zh: '事件与持久证据', en: 'Events and durable evidence' }, + body: { + zh: 'AgentEvent 与 EventEnvelopeV1 向产品公开生命周期;Run、Trace、Artifact、验证报告和 SessionSnapshotV1 支持审计与恢复。', + en: 'AgentEvent and EventEnvelopeV1 expose lifecycle to products. Runs, traces, artifacts, verification reports, and SessionSnapshotV1 support audit and recovery.', + }, + tags: ['EventEnvelopeV1', 'Run', 'Artifact', 'Snapshot'], + }, +] satisfies Array<{ + id: string; + code: string; + title: Localized; + body: Localized; + tags: string[]; +}>; + +const copy = { + zh: { + eyebrow: 'OPEN SOURCE · ASYNC RUST RUNTIME', + titleLead: '构建可治理的', + titleAccent: '编码 Agent', + subtitle: + 'A3S Code 把 Agent Loop、Workspace 工具、模型适配、策略决策、版本化事件与持久化证据放在显式契约之后。', + docs: '开始使用', + github: '查看 GitHub', + copy: '复制', + copied: '已复制', + turn: '一次受治理的 Turn', + proposal: '模型提出工具调用', + governed: '统一治理边界', + result: '结果成为事件与证据', + context: '上下文 + 记忆', + model: '模型适配器', + guard: '校验 → 权限 → 确认 → 预算 → 沙箱', + evidence: 'Run · Trace · Artifact · Snapshot', + surfacesLabel: '一个运行时,四种产品表面', + whyEyebrow: 'WHY A3S CODE', + whyTitle: '可组合,也可问责。', + whyBody: + '运行时把执行语义放在一个可观察、可替换、可持久化的边界里;UI、身份、凭据与部署策略仍由宿主拥有。', + architectureEyebrow: 'VISIBLE BY DESIGN', + architectureTitle: '责任链不是黑盒。', + architectureBody: + 'AgentSession 绑定 Workspace 与会话;模型只提出调用。每个副作用经过同一条治理路径,再以版本化事件和耐久证据向宿主公开。', + architectureAlt: + 'A3S Code 的受治理 Agent 运行时架构:模型、策略、工具、事件与持久化快照之间的显式流转。', + capabilitiesEyebrow: 'RUNTIME CAPABILITIES', + capabilitiesTitle: '能力丰富,授权明确。', + capabilitiesBody: + 'Core 默认保持可嵌入。云存储、服务端和遥测是可选能力;自动压缩、目标、委派、沙箱、持久化与图投影都需要宿主显式配置。', + surfacesEyebrow: 'CHOOSE YOUR SURFACE', + surfacesTitle: '同一套执行语义,进入你的技术栈。', + surfacesBody: + '直接运行终端产品,或通过 Rust、Node.js、Python 把同一个 Runtime 嵌入自己的 IDE、Runner、服务与产品界面。', + boundariesEyebrow: 'EXPLICIT BOUNDARIES', + boundariesTitle: 'Core 管执行,宿主管信任。', + boundaryItems: [ + 'Core 是可嵌入运行时,不是托管 Agent 服务,也不是终端组件库。', + '独立的 A3S CLI 负责交互式 TUI、账户适配与呈现策略。', + '身份、凭据、部署和直接宿主工具的信任决策始终属于宿主。', + ], + boundaryLink: '阅读架构与边界', + ctaTitle: '从一个可观察的 Turn 开始。', + ctaBody: + '运行 a3s code,或把 a3s-code-core 嵌入你的产品。工具、策略、事件和证据从第一天起就是显式的。', + ctaPrimary: '阅读快速开始', + ctaSecondary: '查看 API 契约', + footer: + 'MIT licensed. Built in Rust. Designed for governed agent products.', + }, + en: { + eyebrow: 'OPEN SOURCE · ASYNC RUST RUNTIME', + titleLead: 'Build governed', + titleAccent: 'coding agents', + subtitle: + 'A3S Code keeps the agent loop, workspace tools, model adapters, policy decisions, versioned events, and durable evidence behind explicit contracts.', + docs: 'Get started', + github: 'View on GitHub', + copy: 'Copy', + copied: 'Copied', + turn: 'One governed turn', + proposal: 'The model proposes a tool call', + governed: 'One governance boundary', + result: 'Results become events and evidence', + context: 'context + memory', + model: 'model adapter', + guard: 'validation → permission → confirmation → budget → sandbox', + evidence: 'Run · Trace · Artifact · Snapshot', + surfacesLabel: 'One runtime, four product surfaces', + whyEyebrow: 'WHY A3S CODE', + whyTitle: 'Composable and accountable.', + whyBody: + 'The runtime puts execution semantics behind one observable, replaceable, durable boundary. The host still owns UI, identity, credentials, and deployment policy.', + architectureEyebrow: 'VISIBLE BY DESIGN', + architectureTitle: 'The chain of responsibility is not a black box.', + architectureBody: + 'AgentSession binds a workspace and conversation; the model only proposes calls. Every side effect crosses the same governance path, then returns as versioned events and durable evidence.', + architectureAlt: + 'A3S Code governed agent runtime architecture showing the explicit flow between model, policy, tools, events, and durable snapshots.', + capabilitiesEyebrow: 'RUNTIME CAPABILITIES', + capabilitiesTitle: 'Rich capability. Explicit authority.', + capabilitiesBody: + 'Core stays embeddable by default. Cloud storage, serving, and telemetry are opt-in; compaction, goals, delegation, sandboxing, persistence, and graph projection require host configuration.', + surfacesEyebrow: 'CHOOSE YOUR SURFACE', + surfacesTitle: 'One execution model, in your stack.', + surfacesBody: + 'Run the terminal product or embed the same runtime in an IDE, runner, service, or product UI through Rust, Node.js, and Python.', + boundariesEyebrow: 'EXPLICIT BOUNDARIES', + boundariesTitle: 'Core owns execution. The host owns trust.', + boundaryItems: [ + 'Core is an embeddable runtime, not a hosted agent service or terminal widget library.', + 'The separate A3S CLI owns the interactive TUI, account adapters, and presentation policy.', + 'Identity, credentials, deployment, and trust decisions for direct host tools remain host-owned.', + ], + boundaryLink: 'Read architecture and boundaries', + ctaTitle: 'Start with one observable turn.', + ctaBody: + 'Run a3s code or embed a3s-code-core. Tools, policy, events, and evidence are explicit from day one.', + ctaPrimary: 'Read the quick start', + ctaSecondary: 'Explore the API contract', + footer: + 'MIT licensed. Built in Rust. Designed for governed agent products.', + }, +}; + +function localeValue(value: Localized, locale: Locale) { + return value[locale]; +} + +function ArrowIcon() { + return ( + + ); +} + +function GitHubIcon() { + return ( + + ); +} + +function InstallSwitcher({ + locale, + labels, +}: { + locale: Locale; + labels: (typeof copy)[Locale]; +}) { + const [activeId, setActiveId] = + useState<(typeof installCommands)[number]['id']>('terminal'); + const [copied, setCopied] = useState(false); + const active = + installCommands.find((item) => item.id === activeId) ?? installCommands[0]; + + async function copyActiveCommand() { + await navigator.clipboard.writeText(active.command); + setCopied(true); + window.setTimeout(() => setCopied(false), 1600); + } + + return ( +
+
+ {installCommands.map((item) => ( + + ))} +
+
+
+          {active.command}
+        
+ +
+ +
+ ); +} + +function RuntimeDiagram({ labels }: { labels: (typeof copy)[Locale] }) { + return ( +
+