Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/).

## Unreleased

### GASP

- **Plugin logs in the run's record (opt-in).** `GaspRecorder::extension()` (install on the agent: it learns each run's id) and `GaspRecorder::plugin_log_layer()` (add to the `tracing` subscriber) record a rutis plugin's diagnostics — the yoagent-rutis bridge's `log`, target `yoagent_rutis::plugin` — as `observation.created` on the run they belong to, through the summarizer, with `{source: "plugin", level}` metadata. Only lines whose `run_id` is one of the recorder's runs are recorded. The `gasp` feature now also enables `tracing-subscriber` (registry only). Test: `tests/gasp_plugin_log_test.rs`.

### yoagent-rutis

- **pi extensions** (`plugins/pi/pi-extensions-adapter.ts`): the tools and tool policies of [pi](https://github.com/earendil-works/pi) coding-agent extensions, loaded unchanged with pi's own loader, as one yoagent handler following pi 1.1.0's semantics. `registerTool` → tools (pi's activation and first-registration-wins, `prepareArguments` and validation before the policies, only the judged arguments run), `setActiveTools` → an enforced allowlist, `tool_call` → `before_tool` (yoagent's built-ins under pi's names, arguments translated both ways, relative paths resolved where the policies look; an override of a built-in is enforced; `terminate` stops the run), `tool_result` → `after_tool` (a failing handler withholds the result), `input` → `on_input`, `before_agent_start` additions → a turn note. Fails closed: a load error, a failing `session_start`, an unfired deciding event (`context`, `message_end`, ...; unless `allowUnmapped`) or a pi tool named like a yoagent built-in (unless `withoutBuiltins`) refuses the load; other unmapped API (observer and session events, commands, renderers, providers, MCP servers) is reported, or refuses with `strict`. Example `pi_extensions` (any extension files; `--live` with DeepSeek; `--without`), test `pi_test`. `ctx.executeTool` runs pi tools as nested calls (pi's pipeline; never rejects); a deciding event registered mid-run stops that run; adapter warnings reach the host's logs. `pi.appendEntry` / `pi.sendMessage` are recorded in the adapter's in-memory session instead of throwing (pi tools that record their result this way no longer fail after the work). CI: a weekly `pi latest` job runs the adapter's tests against the newest pi.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Behind the `openapi` Cargo feature. `OpenApiToolAdapter` parses an OpenAPI 3.0 s

### GASP Bridge (`gasp.rs`, feature-gated)

Behind the `gasp` Cargo feature (dep: `yoagent-state`, the GASP reference runtime). `GaspRecorder` consumes the `AgentEvent` stream (via `recording_sender`, which also tees to a forward sender) and maps it onto `yoagent_state`'s `YoAgentStateSink`: AgentStart→run.started, assistant MessageEnd→model.called/finished pair, ToolExecutionStart/End→tool.called/finished, AgentEnd→run.finished + one git commit per run at stream close (scaffolding committed at init so clones restore). Stale open runs are closed as "interrupted" on open; a dropped sender finishes the run with the derived outcome; InputRejected → "rejected". Recording failures stop recording but the forward tee keeps flowing (error surfaces only via the returned handle — await it); events are forwarded BEFORE recording. `Ok(None)` when no AgentStart arrived. Zero loop changes. CI job `gasp-conformance` emits a repo via `examples/gasp_emit.rs` and runs the gasp conformance checker (7 checks) against it.
Behind the `gasp` Cargo feature (dep: `yoagent-state`, the GASP reference runtime). `GaspRecorder` consumes the `AgentEvent` stream (via `recording_sender`, which also tees to a forward sender) and maps it onto `yoagent_state`'s `YoAgentStateSink`: AgentStart→run.started, assistant MessageEnd→model.called/finished pair, ToolExecutionStart/End→tool.called/finished, AgentEnd→run.finished + one git commit per run at stream close (scaffolding committed at init so clones restore). Stale open runs are closed as "interrupted" on open; a dropped sender finishes the run with the derived outcome; InputRejected → "rejected". Recording failures stop recording but the forward tee keeps flowing (error surfaces only via the returned handle — await it); events are forwarded BEFORE recording. `Ok(None)` when no AgentStart arrived. Zero loop changes. **Plugin logs (opt-in):** `recorder.extension()` (`GaspRunLink`, an `Extension` whose `start_run` adds `run.run_id` to the open recording's `OpenRoute.runs`; no hooks) + `recorder.plugin_log_layer()` (`PluginLogLayer`, a `tracing_subscriber::Layer` — the `gasp` feature enables optional `tracing-subscriber` with `registry` only — that forwards events of target `PLUGIN_LOG_TARGET` = `yoagent_rutis::plugin` whose `run_id` field is in `runs` over an unbounded channel); `recording_sender` opens the route, `consume` `select!`s events and log lines, buffers lines until `AgentStart`, writes each as `record_observation` (`observed_in` the GASP run, title `plugin <level>`, summary through the summarizer, metadata `{source: "plugin", level}`), records lines already queued before recording `AgentEnd` (so they stay inside the run), and at stream end clears the route **only if it is still its own** (`OpenRoute.tx.same_channel(&Logs.own)`; a next `recording_sender` may already have replaced it) and drains queued lines. Lines with another or no `run_id` are never recorded. Test: `tests/gasp_plugin_log_test.rs` (own binary, global subscriber). CI job `gasp-conformance` emits a repo via `examples/gasp_emit.rs` and runs the gasp conformance checker (7 checks) against it.

### Decision Models (`decision/`, feature-gated)

Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ openapiv3 = { version = "2", optional = true }
serde_yaml_ng = { version = "0.10", optional = true }
# 0.4.2 floor: 0.4.1 declared MSRV 1.85 but required 1.88 (let-chain)
yoagent-state = { version = "0.5", optional = true }
# The GASP recorder's plugin-log layer (`gasp::GaspRecorder::plugin_log_layer`).
tracing-subscriber = { version = "0.3", optional = true, default-features = false, features = ["registry"] }

[features]
default = ["native"]
Expand All @@ -70,7 +72,7 @@ default = ["native"]
# (e.g. Cloudflare Workers); see docs for what remains available there.
native = ["tokio/process", "tokio/fs", "reqwest/default", "reqwest/socks"]
openapi = ["dep:openapiv3", "dep:serde_yaml_ng", "reqwest/query"]
gasp = ["dep:yoagent-state"]
gasp = ["dep:yoagent-state", "dep:tracing-subscriber"]
# Decision models (typed questions -> calibrated answers), e.g. TypeSafe Jev.
# Off by default; adds no dependencies. Nothing is sent until you pick a model.
decision = []
Expand Down
16 changes: 16 additions & 0 deletions docs/concepts/gasp.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ let run_id = record_handle.await??; // Option<RunId>: None if no AgentStart ar
| loop starts | `run.started` (the goal is stamped in the run commit's `Goal:` trailer) |
| each assistant turn | `model.called` / `model.finished` paired nodes (a retried provider attempt — an error `MessageEnd` followed by `ProviderRetry` — is not recorded) |
| each tool execution | `tool.called` / `tool.finished` (with success flag) |
| a plugin logs (opt-in, below) | `observation.created`, linked to the run, `{source: "plugin", level}` metadata |
| loop ends | `run.finished` with the outcome: `completed` / `truncated` / `error` / `aborted` (a cancelled run) / `refused`, `loop_aborted:<tool>` when loop detection stopped it, `rejected` when an input filter refused the prompt, `interrupted` for a run a crashed process left open |

The semantic log stores bounded one-line summaries — the **task string
Expand All @@ -62,6 +63,21 @@ handle — **always await it** — while event forwarding to your UI continues
uninterrupted. GASP repos are single-writer: don't share one repo between
live workers, and record one run at a time.

## Plugin logs (opt-in)

A rutis plugin's diagnostics — the [yoagent-rutis](https://github.com/yologdev/yoagent/tree/main/integrations/yoagent-rutis) bridge's `log`, such as a pi extension's "command `/todos` not available" or a DSH image that could not be read — can be part of the run's record. Install two things from the recorder:

```rust,ignore
use tracing_subscriber::layer::SubscriberExt;

tracing::subscriber::set_global_default(
tracing_subscriber::registry().with(recorder.plugin_log_layer()),
)?;
let agent = agent.with_extension(recorder.extension()); // learns each run's id
```

`tracing-subscriber` 0.3 must be a dependency of your own crate (the layer implements its `Layer` trait). The layer forwards lines of the target `yoagent_rutis::plugin` (`gasp::PLUGIN_LOG_TARGET`) whose `run_id` is one of this recorder's runs — another agent's plugins in the same process never land here, and a line without a run stays in your logs only. Each line goes through the summarizer, like tool output, and is recorded as an `observation.created` on the run. Installed with `with_tree_extension`, delegated runs' lines are recorded on the same run. Record every run of an agent that has the extension: a run started *without* a recording while one is open is attributed to that recording.

## Tested conformance

yoagent's CI emits an agent repo with a mock provider and runs the GASP
Expand Down
4 changes: 3 additions & 1 deletion integrations/yoagent-rutis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,9 @@ def apply(ctx, config):
the host's `tracing` output under the target `yoagent_rutis::plugin`,
where a terminal or service host shows it — a runtime process's own
stderr may go nowhere. Pass `{ run_id }`, or the hook argument itself, to
attribute the line to its run (the event's `run_id` field). It never
attribute the line to its run (the event's `run_id` field) — a host
recording runs with yoagent's GASP recorder can then keep the line in the
run's record (`GaspRecorder::plugin_log_layer`). It never
rejects (any message, extra context fields ignored). Fire-and-forget in
JavaScript; in Python `await yoagent.log(...)` — an un-awaited coroutine
is never sent. On a host without it (yoagent-rutis 0.1.0) rutis's
Expand Down
Loading
Loading