0Pricing
AI Engineering Academy · レッスン

実行ステップ間の状態管理

複数のコード実行ステップにわたって変数、データフレーム、インポート済みライブラリを保持し、エージェントが以前の計算を再実行せずに過去の結果を利用できるようにします。

「実行ステップ間の状態管理」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

The Statelessness Problem

Each Docker container execution starts with a fresh Python interpreter. Variables defined in iteration 1 do not exist in iteration 2. This statelessness forces the agent to re-compute or re-load everything from scratch on every execution step — unless you implement an explicit state management strategy to persist and restore state across iterations. Without it, multi-step analyses are impossible.

# Iteration 1 - works fine
df = pd.read_csv('data.csv')  # df is in memory
df_cleaned = df.dropna()
print('Rows after cleaning:', len(df_cleaned))

# Iteration 2 - NEW container, df_cleaned is GONE
result = df_cleaned.groupby('category').sum()  # NameError: df_cleaned is not defined
print(result)  # This will fail!

File-Based State Persistence

The simplest and most portable approach is to save state to files. At the end of each code block, the agent saves DataFrames, dictionaries, or other objects to files in the workspace directory. The next iteration loads them back. Parquet is ideal for DataFrames, JSON for dictionaries, and pickle for arbitrary Python objects (though pickle from untrusted code is a security risk).

import pandas as pd
import json
from pathlib import Path

WORKSPACE = Path('/workspace')

# Iteration 1: process and SAVE
df = pd.read_csv(WORKSPACE / 'raw_data.csv')
df_cleaned = df.dropna().reset_index(drop=True)
df_cleaned.to_parquet(WORKSPACE / 'cleaned.parquet')  # save for next iteration

stats = {'rows': len(df_cleaned), 'columns': list(df_cleaned.columns)}
with open(WORKSPACE / 'stats.json', 'w') as f:
    json.dump(stats, f)

print('Saved cleaned data:', len(df_cleaned), 'rows')

# Iteration 2: LOAD and continue
df_cleaned = pd.read_parquet(WORKSPACE / 'cleaned.parquet')  # restore state
with open(WORKSPACE / 'stats.json') as f:
    stats = json.load(f)
result = df_cleaned.groupby('category')['value'].sum()
print(result)

Teaching the Agent File-Based State

You cannot just implement file-based state in your infrastructure — the LLM must also know about the convention and consistently use it. Include explicit instructions in the system prompt: after computing a result that will be needed later, always save it with a descriptive filename, and at the start of each iteration, always load the files that were produced in previous steps before proceeding.

STATE_MANAGEMENT_INSTRUCTIONS = '''
State persistence rules:
- You have a persistent workspace at /workspace/
- After computing any result you will need later, SAVE it to /workspace/
  - DataFrames: use .to_parquet('/workspace/name.parquet')
  - Dicts/lists: use json.dump to /workspace/name.json
  - Text: write to /workspace/name.txt
- At the start of each code block, LOAD the files you need from previous steps
- Use clear, descriptive filenames like 'cleaned_data.parquet', not 'tmp1.parquet'
- When listing files, use: import os; print(os.listdir('/workspace/'))
'''

Kernel-Based State with Persistent Python Processes

An alternative to file-based state is to keep a persistent Python process (like a Jupyter kernel) running between iterations and inject each code block into it with exec(). Variables defined in one iteration remain accessible in the next. This approach is faster and more natural, but requires running a persistent process per agent session rather than ephemeral containers.

import jupyter_client

class PersistentKernel:
    def __init__(self):
        km, self.kc = jupyter_client.manager.start_new_kernel(kernel_name='python3')
        self.kc.wait_for_ready(timeout=30)
        print('Kernel started')

    def execute(self, code: str, timeout=60) -> tuple[str, str]:
        msg_id = self.kc.execute(code)
        outputs, errors = [], []
        
        while True:
            msg = self.kc.get_iopub_msg(timeout=timeout)
            if msg['msg_type'] == 'stream':
                if msg['content']['name'] == 'stdout':
                    outputs.append(msg['content']['text'])
                else:
                    errors.append(msg['content']['text'])
            if msg['msg_type'] == 'status' and msg['content']['execution_state'] == 'idle':
                break
        
        return ''.join(outputs), ''.join(errors)

    def shutdown(self):
        self.kc.shutdown()

# Variables persist across execute() calls!
kernel = PersistentKernel()
kernel.execute('x = 42')  # x is now defined in kernel
output, _ = kernel.execute('print(x)')  # prints 42 - state persists!
kernel.shutdown()

Managing State With a State Object

For the agent orchestrator layer (outside the sandbox), maintain an explicit state object that tracks the current status of the agent's work: what files have been created, what has been computed, what step the agent is on, and what the final goal is. Pass a summary of this state object to the LLM in each iteration so it always knows where it is in the overall task.

from dataclasses import dataclass, field
from typing import Any

@dataclass
class ExecutionState:
    task: str
    iteration: int = 0
    workspace_files: list[str] = field(default_factory=list)
    computed_values: dict[str, Any] = field(default_factory=dict)
    completed_steps: list[str] = field(default_factory=list)
    last_output: str = ''

    def to_context_summary(self) -> str:
        return f'''Current task: {self.task}
Iteration: {self.iteration}
Completed steps: {', '.join(self.completed_steps) or 'None yet'}
Workspace files: {', '.join(self.workspace_files) or 'None yet'}
Key values: {self.computed_values}
Last output: {self.last_output[:500]}'''

    def after_execution(self, output: str, step_name: str):
        import os
        self.workspace_files = os.listdir('/workspace')
        self.completed_steps.append(step_name)
        self.last_output = output
        self.iteration += 1

Injecting State Context into Prompts

Each time you call the LLM for the next code block, inject the current state context into the user message. This gives the LLM accurate information about what files are available, what has been computed, and what still needs to be done. Without this context, the LLM may try to re-create files that already exist or skip steps that are already complete.

def build_iteration_prompt(task: str, state: ExecutionState, last_observation: str) -> str:
    return f'''ORIGINAL TASK: {task}

CURRENT STATE:
{state.to_context_summary()}

LAST EXECUTION OUTPUT:
{last_observation}

What is the next step? Write Python code to continue. 
Remember:
- Load data from workspace files if needed
- Save any results you will need in future steps
- If the task is complete, say TASK COMPLETE and summarize the result'''

# Use in the main loop
for step_num in range(max_iterations):
    context = build_iteration_prompt(task, state, last_observation)
    response = llm.complete(messages + [{'role': 'user', 'content': context}])
    code = extract_code_block(response)
    if not code:
        break  # done
    output, err = execute_in_sandbox(code)
    state.after_execution(output, step_name=f'step_{step_num}')
    last_observation = format_observation(output, err)

Handling Large Intermediate Data

Data analysis agents often produce large intermediate datasets that are expensive to reload on every iteration. Apply lazy loading: only load the data you need for the current step. Use columnar formats like Parquet that support efficient column-selective reads. For truly large datasets (100MB+), keep a persistent kernel so the DataFrame lives in memory across iterations instead of being serialized and deserialized each time.

# Good: load only needed columns
df = pd.read_parquet('/workspace/full_data.parquet', columns=['date', 'revenue', 'region'])

# Bad: load everything even if you only need 2 columns
# df = pd.read_parquet('/workspace/full_data.parquet')  # loads 50 columns you don't need

# Good: filter early before loading full dataset
df = pd.read_parquet('/workspace/full_data.parquet', filters=[('region', '=', 'EMEA')])

# For very large files, tell the LLM about data shape upfront
df_info = {'shape': (1_000_000, 50), 'size_mb': 850, 'columns': [...]}
# Include df_info in state so LLM plans accordingly

Checkpointing Long-Running Tasks

For tasks that span many iterations, implement checkpointing: periodically save the full agent state (completed steps, workspace files, current progress) to a durable store so the task can be resumed after an interruption. This is especially important for long data analysis jobs that might take 30+ minutes and could be interrupted by network issues, API errors, or server restarts.

import json
import time

def checkpoint_state(state: ExecutionState, checkpoint_id: str, db):
    data = {
        'task': state.task,
        'iteration': state.iteration,
        'completed_steps': state.completed_steps,
        'workspace_files': state.workspace_files,
        'timestamp': time.time()
    }
    db.execute(
        'INSERT INTO agent_checkpoints (id, state) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET state=excluded.state',
        (checkpoint_id, json.dumps(data))
    )
    print(f'Checkpoint saved at iteration {state.iteration}')

def resume_from_checkpoint(checkpoint_id: str, db) -> ExecutionState | None:
    row = db.execute('SELECT state FROM agent_checkpoints WHERE id = ?', (checkpoint_id,)).fetchone()
    if not row:
        return None
    data = json.loads(row[0])
    state = ExecutionState(task=data['task'])
    state.iteration = data['iteration']
    state.completed_steps = data['completed_steps']
    print(f'Resumed from iteration {state.iteration}')
    return state

Cleaning Up State After Completion

Agent workspaces accumulate files and can grow large over time. Always implement a cleanup phase that runs when a task completes or fails: delete intermediate files (cleaned.parquet, tmp_output.csv) and keep only the final output files the user cares about. For cloud storage, set lifecycle policies that automatically delete workspace files after a retention period.

import shutil
from pathlib import Path

INTERMEDIATE_PATTERNS = ['*.parquet', 'tmp_*.csv', 'step_*.json', 'debug_*.txt']
FINAL_OUTPUT_PATTERNS = ['report.pdf', 'final_*.csv', 'summary.json']

def cleanup_workspace(workspace_dir: str, keep_final=True):
    workspace = Path(workspace_dir)
    final_outputs = []
    
    if keep_final:
        for pattern in FINAL_OUTPUT_PATTERNS:
            final_outputs.extend(workspace.glob(pattern))
        # Move final outputs to output directory
        output_dir = workspace.parent / 'outputs'
        output_dir.mkdir(exist_ok=True)
        for f in final_outputs:
            shutil.move(str(f), str(output_dir / f.name))
    
    # Delete the workspace
    shutil.rmtree(workspace_dir)
    print(f'Workspace cleaned up. Kept {len(final_outputs)} output files.')
    return [str(f) for f in final_outputs]

State Versioning for Debugging

When a code agent produces wrong results, you need to trace back through its execution history to find where it went wrong. Implement state versioning by saving a snapshot of the workspace after every iteration. This lets you replay the agent's execution, inspect intermediate states, and identify the exact iteration where the error occurred. Store only the diff (changed files) to save space.

Context Window vs External State

There is a fundamental tension in code agent design between putting state in the LLM context window (immediate but limited) and in external storage (unlimited but requires explicit management). The optimal strategy is: keep the current step's data in context, keep large intermediate results in files, keep the task goal and high-level plan in context, and keep raw data always on disk. The LLM manages the task; the filesystem manages the data.

Quick Check

Test your understanding of state management across code execution steps from this lesson.

Lesson Recap

In this lesson you learned: file-based state persistence using Parquet and JSON is the most portable way to share state between ephemeral container executions, persistent kernels eliminate the reload overhead by keeping a live Python process across iterations, and state context injection gives the LLM accurate knowledge of available files and completed steps at each iteration. Next up we build a full data analysis agent.

よくある質問

「実行ステップ間の状態管理」レッスンは無料ですか?

はい。「実行ステップ間の状態管理」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。

「実行ステップ間の状態管理」で何を学びますか?

複数のコード実行ステップにわたって変数、データフレーム、インポート済みライブラリを保持し、エージェントが以前の計算を再実行せずに過去の結果を利用できるようにします。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

AI Engineering Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「実行ステップ間の状態管理」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAI Engineering Academyレッスンでコードを書いて実行できますか?

はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. コード実行ループ
  2. DockerとRestrictedPythonによるサンドボックス化
  3. 実行ステップ間の状態管理
  4. データ分析エージェントを構築する
← AI Engineering Academyに戻る