Skip to content
Open
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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agent-sequencer",
"version": "0.1.2",
"version": "0.1.3",
"description": "Claude Code skill + MCP server that lets a Python script drive an AI agent through strictly-defined workflows and long-running tasks.",
"author": {
"name": "OPENSPHERE Inc.",
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ as conversation context degrades over long-running tasks.
against a JSON Schema with automatic retry on violation; interruptions and post-compact
desyncs recover via deterministic replay of a JSONL event log; `--watch` hot-reloads
program edits during development.
- **Volatile per-step memo for sub-agent IPC** — the `sequencer_memo_*` tools expose a
small in-memory KV that is cleared on every `sequencer_next` call. Parallel sub-agents
can stash intermediate JSON without pushing it through the orchestrator's context.
Cross-step retention is structurally impossible, which keeps the resume contract
trivially consistent.

For details, see [`SKILL.md`](skills/agent-sequencer/SKILL.md) (driving rules)
and the [program author's guide](skills/agent-sequencer/docs/authoring-programs.md).
Expand Down Expand Up @@ -194,7 +199,11 @@ Add the following to the allow list in `.claude/settings.local.json` (or similar
"mcp__agent-sequencer__sequencer_next",
"mcp__agent-sequencer__sequencer_resume",
"mcp__agent-sequencer__sequencer_close",
"mcp__agent-sequencer__sequencer_list"
"mcp__agent-sequencer__sequencer_list",
"mcp__agent-sequencer__sequencer_memo_set",
"mcp__agent-sequencer__sequencer_memo_get",
"mcp__agent-sequencer__sequencer_memo_keys",
"mcp__agent-sequencer__sequencer_memo_delete"
]
}
```
Expand Down Expand Up @@ -242,6 +251,8 @@ agent-sequencer/
|---|---|---|
| `AGENT_SEQUENCER_PROGRAMS_DIR` | Additional program search path appended as the lowest-priority fallback (used by plugins to ship bundled programs) | (unset) |
| `AGENT_SEQUENCER_STATE_DIR` | Directory for JSONL event logs | `~/.claude/sequencer/state/` |
| `AGENT_SEQUENCER_MEMO_VALUE_LIMIT` | Per-value byte ceiling for `sequencer_memo_*` (UTF-8 JSON encoding) | `1048576` (1 MiB) |
| `AGENT_SEQUENCER_MEMO_INSTANCE_LIMIT` | Per-instance total byte ceiling for `sequencer_memo_*` | `67108864` (64 MiB) |

Programs are searched in the following order (first match wins):

Expand Down
13 changes: 12 additions & 1 deletion README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
- **長時間ワークフローの安定実行** — JSON Schema で各ステップの応答を厳密検証し、
違反は自動リトライ。interrupt / compact 後も JSONL の決定論的再生で完全復旧。
`--watch` でプログラム編集をホットリロード
- **サブエージェント間 IPC のための揮発性ステップ内メモ** —
`sequencer_memo_*` ツールは小さなインメモリ KV を提供し、`sequencer_next`
ごとに自動クリアされる。並列に動くサブエージェントが中間 JSON をオーケスト
レーターのコンテキストに乗せずにやり取りできる。ステップ越え保持は構造的
に不可能なため、resume との整合性は自動的に保たれる

詳しくは [`SKILL.md`](skills/agent-sequencer/SKILL.md)(駆動ルール)と
[`docs/authoring-programs_ja.md`](skills/agent-sequencer/docs/authoring-programs_ja.md)
Expand Down Expand Up @@ -187,7 +192,11 @@ instance_id=abc123 を resume して続きから進めてください
"mcp__agent-sequencer__sequencer_next",
"mcp__agent-sequencer__sequencer_resume",
"mcp__agent-sequencer__sequencer_close",
"mcp__agent-sequencer__sequencer_list"
"mcp__agent-sequencer__sequencer_list",
"mcp__agent-sequencer__sequencer_memo_set",
"mcp__agent-sequencer__sequencer_memo_get",
"mcp__agent-sequencer__sequencer_memo_keys",
"mcp__agent-sequencer__sequencer_memo_delete"
]
}
```
Expand Down Expand Up @@ -235,6 +244,8 @@ agent-sequencer/
|---|---|---|
| `AGENT_SEQUENCER_PROGRAMS_DIR` | 追加のプログラム探索パス(最低優先度のフォールバック。プラグインが同梱プログラムを公開するために使用) | (未設定) |
| `AGENT_SEQUENCER_STATE_DIR` | JSONL イベントログの配置ディレクトリ | `~/.claude/sequencer/state/` |
| `AGENT_SEQUENCER_MEMO_VALUE_LIMIT` | `sequencer_memo_*` の 1 値あたりのバイト上限(UTF-8 JSON 表現) | `1048576`(1 MiB) |
| `AGENT_SEQUENCER_MEMO_INSTANCE_LIMIT` | `sequencer_memo_*` の 1 インスタンス合計バイト上限 | `67108864`(64 MiB) |

プログラム探索は次の順(先勝ち):

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "agent-sequencer"
version = "0.1.2"
version = "0.1.3"
description = "MCP skill + server that lets a Python script drive an AI agent through strictly-defined workflows and long-running tasks."
readme = "README.md"
license = { text = "MIT" }
Expand Down
4 changes: 4 additions & 0 deletions skills/agent-sequencer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ For details, see [SKILL.md](SKILL.md) (the 9 driving rules).
| `sequencer_resume` | Restore from JSONL |
| `sequencer_close` | Release (recommended path) |
| `sequencer_list` | List active instances |
| `sequencer_memo_set` | Store a JSON value in the per-instance memo. Cleared on the next `sequencer_next` call |
| `sequencer_memo_get` | Read a value from the per-instance memo |
| `sequencer_memo_keys` | List keys in the per-instance memo, optionally prefix-filtered |
| `sequencer_memo_delete` | Delete a key from the per-instance memo |

## Program search paths (first-match wins)

Expand Down
4 changes: 4 additions & 0 deletions skills/agent-sequencer/README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ AI エージェントを駆動する MCP スキル。
| `sequencer_resume` | JSONL からの復元 |
| `sequencer_close` | 解放(推奨経路) |
| `sequencer_list` | アクティブなインスタンスの一覧 |
| `sequencer_memo_set` | インスタンスごとのメモに JSON 値を保存(次の `sequencer_next` でクリア) |
| `sequencer_memo_get` | インスタンスごとのメモから値を取得 |
| `sequencer_memo_keys` | インスタンスごとのメモのキー一覧(任意で prefix フィルタ) |
| `sequencer_memo_delete` | インスタンスごとのメモからキーを削除 |

## プログラム探索パス(先勝ち)

Expand Down
1 change: 1 addition & 0 deletions skills/agent-sequencer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ For how to write new programs, see [`docs/authoring-programs.md`](docs/authoring
- `sequencer_resume` — Restore from JSONL.
- `sequencer_close` — Release (recommended path).
- `sequencer_list` — List active instances.
- `sequencer_memo_set` / `_get` / `_keys` / `_delete` — Volatile per-instance KV scoped to the current Instruction (cleared on every `sequencer_next`). Use it to let parallel sub-agents share intermediate JSON without round-tripping through the orchestrator's context. Cross-step retention is structurally impossible; for state that must outlive a step, use files.

For details including setup and usage, see [`README.md`](README.md).
58 changes: 58 additions & 0 deletions skills/agent-sequencer/docs/authoring-programs.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,64 @@ The JSONL `header.source_hash` is compared with the current source's hash; if th
resume is rejected with a `ProgramChanged` error. This is the mechanism for detecting
"the prerequisite for deterministic replay has been broken," and there is no way around it.

## 9.5 Volatile memo store (sub-agent IPC)

The MCP server exposes four `sequencer_memo_*` tools backed by a small
in-memory KV store. They are intended for **sub-agent IPC during the
execution of a single Instruction**: parallel sub-agents launched by
the orchestrator can stash intermediate JSON results without sending
them back through the orchestrator's context.

### 9.5.1 Lifecycle (memorize this)

- The memo bucket for an instance is **cleared at the top of every
`sequencer_next` call**, before the program advances. There is no way
to keep a memo entry alive across two Instructions.
- The bucket is also dropped on `sequencer_close` and on TTL archive.
- Nothing is persisted to disk, nothing is replayed on `sequencer_resume`.

The combined effect is that the memo is always empty at the start of
any Instruction's execution, both in fresh runs and in resumed runs.
This is what makes the memo compatible with deterministic replay
without any author-side discipline.

### 9.5.2 Authoring rules

- **Do not call any `sequencer_memo_*` tool from inside `run(ctx)`.**
Doing so would constitute I/O inside the program and break
determinism. `ctx` deliberately does not expose a memo API.
- Inside `Instruction.text`, instruct sub-agents to use the memo
themselves. Pass the orchestrator's known `instance_id` and a key
naming scheme. A typical template:

```
Sub-agents must write their per-finding result to the memo:
tool: sequencer_memo_set
instance_id: <orchestrator's instance_id>
key: round{N}/triage/<finding-id>
value: {triage JSON}

An aggregator sub-agent reads memo keys with prefix
"round{N}/triage/" and produces the final triage table.
```

- Choose hierarchical keys (`round1/triage/C-1`) so that
`sequencer_memo_keys(prefix=...)` can be used to drive an aggregator
sub-agent.
- For data that must outlive the current step (across rounds, across
resume), use **files** (write paths to disk, return the path in the
Instruction result). The memo is not a substitute for files.

### 9.5.3 Quotas

- Per-value byte ceiling: 1 MiB by default
(`AGENT_SEQUENCER_MEMO_VALUE_LIMIT`).
- Per-instance total byte ceiling: 64 MiB by default
(`AGENT_SEQUENCER_MEMO_INSTANCE_LIMIT`).

Both limits count the UTF-8 byte size of the JSON encoding of the
value.

## 10. Bundling programs (recommended)

When a program depends on external skills, agent definitions, or scripts,
Expand Down
56 changes: 56 additions & 0 deletions skills/agent-sequencer/docs/authoring-programs_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,62 @@ JSONL の `header.source_hash` は現在のソースのハッシュと比較さ
一致しない場合は resume が `ProgramChanged` エラーで拒否されます。
これは「決定論的再生の前提が崩れた」ことを検知するための仕組みであり、回避手段はありません。

## 9.5 揮発性メモストア(サブエージェント間 IPC)

MCP サーバは小さなインメモリ KV を背後に持つ 4 つの `sequencer_memo_*` ツール
を公開しています。これは **1 つの Instruction の実行中に並列で動くサブエージェ
ント間で中間データをやり取りするため**のもので、サブエージェントが書き込んだ
中間 JSON をオーケストレーターのコンテキストを経由せずに別のサブエージェント
に渡すために使います。

### 9.5.1 ライフサイクル(要暗記)

- インスタンスのメモバケットは **`sequencer_next` 呼び出しの先頭で必ずクリア
される**(プログラムが進行する直前)。1 つの Instruction を跨いでメモ項目
を保持する手段はありません。
- `sequencer_close` および TTL アーカイブ時にもバケットは破棄されます。
- ディスクには永続化されず、`sequencer_resume` でも復元されません。

これらの効果により、メモは新規実行でも resume 後でも、Instruction 実行開始時
点で常に空です。プログラム作者が特別な配慮をしなくても、決定論的再生と整合
する仕組みになっています。

### 9.5.2 作成ルール

- **`run(ctx)` の中から `sequencer_memo_*` ツールを呼んではいけません。**
プログラム内からの呼び出しは I/O であり決定論性を壊します。`ctx` は意図的
にメモ API を公開していません。
- `Instruction.text` の中で、サブエージェント自身にメモを使うよう指示します。
オーケストレーター側で既知の `instance_id` とキー命名規則を渡します。例:

```
サブエージェントは指摘ごとの結果をメモに書き込んでください:
tool: sequencer_memo_set
instance_id: <オーケストレーターの instance_id>
key: round{N}/triage/<finding-id>
value: {triage の JSON}

集約サブエージェントは
`sequencer_memo_keys(prefix="round{N}/triage/")` でキー一覧を取り、
最終的な triage テーブルを生成します。
```

- 階層キー(`round1/triage/C-1` 等)を採用し、
`sequencer_memo_keys(prefix=...)` で集約サブエージェントを駆動できる
ようにしてください。
- 現ステップを跨いで残す必要があるデータ(ラウンド跨ぎ、resume 跨ぎ)は
**ファイル**を使ってください(パスをディスクに書き出し、Instruction の
result でパスを返す)。メモはファイルの代替ではありません。

### 9.5.3 クォータ

- 1 値あたりのバイト上限: デフォルト 1 MiB
(`AGENT_SEQUENCER_MEMO_VALUE_LIMIT`)。
- 1 インスタンス合計のバイト上限: デフォルト 64 MiB
(`AGENT_SEQUENCER_MEMO_INSTANCE_LIMIT`)。

いずれも値の JSON エンコード結果の UTF-8 バイト数を計上します。

## 10. プログラムのバンドル化(推奨)

プログラムが外部のスキル、エージェント定義、スクリプトに依存する場合、
Expand Down
2 changes: 1 addition & 1 deletion src/agent_sequencer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
See documents/agent-sequencer/implementation-plan.md for design details.
"""

__version__ = "0.1.2"
__version__ = "0.1.3"
87 changes: 80 additions & 7 deletions src/agent_sequencer/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,22 @@
sequencer_list_programs / sequencer_start / sequencer_resume).

Environment variables:
AGENT_SEQUENCER_PROGRAMS_DIR Additional program search path appended as
the lowest-priority fallback (used by
plugins to ship bundled programs that user
/ project paths can override). Excluded
from the search list when unset.
AGENT_SEQUENCER_STATE_DIR Directory where JSONL event logs are stored.
Defaults to ~/.claude/sequencer/state/.
AGENT_SEQUENCER_PROGRAMS_DIR Additional program search path
appended as the lowest-priority
fallback (used by plugins to ship
bundled programs that user /
project paths can override).
Excluded from the search list when
unset.
AGENT_SEQUENCER_STATE_DIR Directory where JSONL event logs
are stored. Defaults to
~/.claude/sequencer/state/.
AGENT_SEQUENCER_MEMO_VALUE_LIMIT Per-value byte ceiling for the
sequencer_memo_* tools. Defaults
to 1 MiB.
AGENT_SEQUENCER_MEMO_INSTANCE_LIMIT Per-instance total byte ceiling
for the sequencer_memo_* tools.
Defaults to 64 MiB.
"""

from __future__ import annotations
Expand All @@ -26,6 +35,10 @@
import sys
from pathlib import Path

from .memo import (
DEFAULT_INSTANCE_LIMIT as MEMO_DEFAULT_INSTANCE_LIMIT,
DEFAULT_VALUE_LIMIT as MEMO_DEFAULT_VALUE_LIMIT,
)
from .persistence import prune_old_logs
from .tools import build_server

Expand Down Expand Up @@ -55,6 +68,13 @@ def main() -> None:
state_dir = _resolve_state_dir()
logger.info("State directory: %s", state_dir)

memo_value_limit, memo_instance_limit = _resolve_memo_limits(logger)
logger.info(
"Memo quotas: per-value=%d bytes, per-instance=%d bytes",
memo_value_limit,
memo_instance_limit,
)

# Prune JSONL files older than the disk TTL once at startup.
state_dir.mkdir(parents=True, exist_ok=True)
pruned = prune_old_logs(state_dir)
Expand All @@ -65,6 +85,8 @@ def main() -> None:
search_paths=search_paths,
state_dir=state_dir,
watch=args.watch,
memo_value_limit=memo_value_limit,
memo_instance_limit=memo_instance_limit,
)
mcp.run(transport="stdio")

Expand Down Expand Up @@ -135,5 +157,56 @@ def _resolve_state_dir() -> Path:
return Path.home() / ".claude" / "sequencer" / "state"


def _resolve_memo_limits(
logger: logging.Logger,
) -> tuple[int, int]:
"""Resolve memo quotas from AGENT_SEQUENCER_MEMO_* env vars.

Falls back to the defaults exported from agent_sequencer.memo when
the variables are unset or contain a non-positive integer; in the
latter case a warning is logged so misconfigurations are visible.
"""
return (
_read_positive_int_env(
"AGENT_SEQUENCER_MEMO_VALUE_LIMIT",
MEMO_DEFAULT_VALUE_LIMIT,
logger,
),
_read_positive_int_env(
"AGENT_SEQUENCER_MEMO_INSTANCE_LIMIT",
MEMO_DEFAULT_INSTANCE_LIMIT,
logger,
),
)


def _read_positive_int_env(
name: str, default: int, logger: logging.Logger
) -> int:
"""Read a positive integer from an env var, falling back on errors."""
raw = os.environ.get(name)
if raw is None or raw == "":
return default
try:
value = int(raw)
except ValueError:
logger.warning(
"Invalid %s=%r (not an integer), using default %d",
name,
raw,
default,
)
return default
if value <= 0:
logger.warning(
"Invalid %s=%d (must be positive), using default %d",
name,
value,
default,
)
return default
return value


if __name__ == "__main__":
main()
Loading
Loading