Skip to content

feat(workflow): persist provider observations - #157

Closed
Waishnav wants to merge 1 commit into
codex/observability-contractsfrom
codex/workflow-provider-observations
Closed

feat(workflow): persist provider observations#157
Waishnav wants to merge 1 commit into
codex/observability-contractsfrom
codex/workflow-provider-observations

Conversation

@Waishnav

@Waishnav Waishnav commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Provider runtimes need a way to report activity and usage without coupling adapters to SQLite. The workflow journal now accepts provider observations, appends activity immediately, coalesces usage snapshots on a five-second cadence, flushes pending usage when a call finishes or fails, and records the provider result as final usage. This layer depends on the observability contracts below it.

Summary by CodeRabbit

  • New Features

    • Added provider-neutral reporting for agent activity observations.
    • Added token-usage reporting for local and workflow agent runs.
    • Workflow journals now record agent observations and final usage on completed calls.
    • Added throttling and final flushing for usage updates during execution.
  • Bug Fixes

    • Preserved usage data when processing structured provider responses.
    • Observation callback errors no longer interrupt agent execution.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds provider-neutral observation callbacks and token-usage reporting. Workflow execution aggregates and throttles usage observations, persists them in the journal, and records final usage for successful or failed agent calls.

Changes

Observation and usage reporting

Layer / File(s) Summary
Runtime observation and usage contracts
src/local-agent-runtime.ts
Local agent inputs accept observation callbacks. Local agent results expose token usage. Callback errors do not fail provider execution.
Workflow accounting and journal persistence
src/workflow-api.ts
Workflow execution forwards observations, aggregates token usage, throttles usage observations to five seconds, flushes pending usage, and records usage on completion or failure.
Provider wiring and integration validation
src/workflow-worker.ts, src/workflow-engine.test.ts
The worker forwards observation callbacks and provider usage. The integration test verifies persisted observations and final usage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Provider
  participant WorkflowWorker
  participant WorkflowAPI
  participant WorkflowJournal
  Provider->>WorkflowWorker: Emit observations and return usage
  WorkflowWorker->>WorkflowAPI: Forward observations and usage
  WorkflowAPI->>WorkflowJournal: Append observations
  WorkflowAPI->>WorkflowJournal: Complete agent call with aggregated usage
Loading

Possibly related PRs

Poem

A rabbit watched the tokens flow,
Through journal paths both fast and slow.
Observations hopped in line,
Final usage marked the sign.
Callbacks failed, yet runs stayed bright. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: persisting provider observations in the workflow journal.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/workflow-provider-observations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR carries provider activity and token-usage observations through the local runtime and workflow worker, persists observations in the workflow journal, and attaches final usage to completed agent calls.

  • Adds provider-neutral observation callbacks and usage fields to runtime and workflow contracts.
  • Throttles usage-observation persistence while immediately journaling activity events.
  • Flushes pending usage on success and failure and records final usage on completed calls.
  • Adds an integration-style workflow test covering activity, usage observations, and final usage.

Confidence Score: 4/5

The PR should not merge until observation persistence failures are prevented from converting provider results into failures or leaving agent calls unfinished.

The new synchronous observation callback performs fallible journal writes without the best-effort error boundary used by the runtime, and the same unguarded flush precedes failure finalization.

Files Needing Attention: src/workflow-api.ts

Important Files Changed

Filename Overview
src/workflow-api.ts Adds observation persistence and final-usage handling, but unguarded observation writes can fail provider calls and prevent journal finalization.
src/local-agent-runtime.ts Extends runtime inputs and results with observations and usage while providing a best-effort callback helper.
src/workflow-worker.ts Correctly forwards observation callbacks and provider usage between the workflow API and local runtime.
src/workflow-engine.test.ts Covers successful observation and usage persistence but does not exercise journal-write failures.

Sequence Diagram

sequenceDiagram
  participant E as Workflow API
  participant J as Workflow Journal
  participant P as Provider
  E->>J: startAgentCall
  E->>P: run(onObservation)
  P-->>E: activity / usage observation
  E->>J: appendAgentObservation
  P-->>E: provider result + usage
  E->>J: flush pending usage
  E->>J: completeAgentCall(finalUsage)
  Note over E,J: An append exception currently escapes and can interrupt completion/failure finalization
Loading

Reviews (1): Last reviewed commit: "feat(workflow): persist provider observa..." | Re-trigger Greptile

Comment thread src/workflow-api.ts
Comment on lines +367 to +371
deps.journal.appendAgentObservation({
runId: deps.runId,
callIndex: index,
observation: { kind: "usage", usage },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Observation failures escape callback

When the journal throws while appending an activity or usage observation, the exception escapes the provider callback and discards an otherwise successful provider result. The same unguarded write in flushUsage runs before failAgentCall, so a flush failure leaves the agent-call record unfinished.

@Waishnav

Waishnav commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/local-agent-runtime.ts`:
- Around line 39-40: Update the observation dispatch using onObservation so
returned promises are handled and rejected callbacks are suppressed, while
preserving handling of synchronous throws and the existing optional-callback
behavior.
- Around line 50-51: Update the live runtime adapters for Codex, Claude,
Opencode, Pi, and ACP to invoke notifyLocalAgentObservation for activity and
usage events during execution, rather than only forwarding onObservation to the
API. Ensure emitted usage uses LocalAgentTokenUsage and is persisted by the
worker, while preserving the existing finalResponse, items, and usage() results.

In `@src/workflow-api.ts`:
- Around line 458-471: Update the schema-enforcement flow around
enforceAgentSchema and the retry-exhaustion path before failAgentCall to merge
usage from each completed latestProviderResult into the usage journal, ensuring
the latest provider usage is flushed even when enforcement throws. Add a
retry-exhaustion test where the provider returns usage without emitting a usage
observation, and verify that usage is persisted.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 895c0708-343a-4b17-abff-bb1537aa82f1

📥 Commits

Reviewing files that changed from the base of the PR and between 518ba2c and 6563974.

📒 Files selected for processing (4)
  • src/local-agent-runtime.ts
  • src/workflow-api.ts
  • src/workflow-engine.test.ts
  • src/workflow-worker.ts

Comment on lines +39 to +40
/** Receives provider-neutral activity and usage observations while running. */
onObservation?: (observation: LocalAgentObservation) => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/local-agent-runtime.ts --items all --type interface,function,class
rg -n -C 5 'onObservation|notifyLocalAgentObservation' \
  src/local-agent-runtime.ts src/local-agent-runtime.test.ts

Repository: Waishnav/devspace

Length of output: 2904


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
let unhandled
process.on('unhandledRejection', (reason) => {
  unhandled = reason
  process.exitCode = 1
})

function notifyLocalAgentObservation(onObservation, observation) {
  try {
    onObservation?.(observation)
  } catch {
    // Observability is best effort and must not turn a provider result into a failure.
  }
}

async function rejected() {
  return Promise.reject(new Error('observer rejected'))
}

notifyLocalAgentObservation(rejected, { event: 'x' })
setTimeout(() => {
  console.log(unhandled ? `unhandled rejection: ${unhandled.message}` : 'no unhandled rejection')
}, 10)
JS

Repository: Waishnav/devspace

Length of output: 194


Suppress rejected asynchronous observer callbacks.

input.onObservation?.(observation) can return a Promise. The current try/catch only handles synchronous throws, so an async observer rejection escapes unhandled. Catch observer promises after invoking the callback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/local-agent-runtime.ts` around lines 39 - 40, Update the observation
dispatch using onObservation so returned promises are handled and rejected
callbacks are suppressed, while preserving handling of synchronous throws and
the existing optional-callback behavior.

Comment on lines +50 to +51
/** Latest provider-reported usage, when available. */
usage?: LocalAgentTokenUsage;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/local-agent-runtime.ts --items all --type class,function
rg -n -C 6 'notifyLocalAgentObservation|onObservation|usage:' src --glob '*local-agent*.ts'

Repository: Waishnav/devspace

Length of output: 5270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== local-agent files =="
git ls-files 'src/**/local-agent*.ts'

echo
echo "== outline src/local-agent-runtime.ts =="
ast-grep outline src/local-agent-runtime.ts --items all --type class,function || true

echo
echo "== relevant src/local-agent-runtime.ts sections =="
sed -n '1,190p' src/local-agent-runtime.ts | cat -n

echo
echo "== local-agent observations =="
sed -n '1,220p' src/local-agent-observations.ts | cat -n

echo
echo "== imports/usages of notifyLocalAgentObservation and onObservation =="
rg -n -C 4 'notifyLocalAgentObservation|onObservation|LocalAgentObservation|usageAsObservation|normalizeLocalAgentTokenUsage|LocalAgentRunResult' src --glob '*.ts'

Repository: Waishnav/devspace

Length of output: 25055


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== local-agent adapters relevant sections =="
sed -n '60,270p' src/local-agent-adapters.ts | cat -n
sed -n '400,490p' src/local-agent-adapters.ts | cat -n

echo
echo "== workflow API persistObservation section =="
sed -n '320,490p' src/workflow-api.ts | cat -n

echo
echo "== references to notifyLocalAgentObservation in src =="
rg -n 'notifyLocalAgentObservation' src --glob '*.ts'

echo
echo "== imports in local-agent runtime and adapters =="
python3 - <<'PY'
from pathlib import Path
for path in [Path("src/local-agent-runtime.ts"), Path("src/local-agent-adapters.ts")]:
    text = path.read_text()
    print(f"== imports in {path} ==")
    import re
    for m in re.finditer(r"^(?:import\s+(?:type\s+)?(?:.*?\n)*?from ['\"]([^'\"]+)['\"])|(^export\s+\{(?:.*?\n)*?from ['\"]([^'\"]+)['\"])", text, re.M):
        print(text[m.start():m.start()+400].split('\n')[0])
PY

Repository: Waishnav/devspace

Length of output: 19853


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== all adapters and their result statements =="
python3 - <<'PY'
from pathlib import Path
p=Path('src/local-agent-adapters.ts')
text=p.read_text()
lines=text.splitlines()
for i,l in enumerate(lines,1):
    if 'class ' in l and 'Adapter' in l:
        print(f'== {p}:{i} ==')
        c1='async run'
        c2='  async run'
        if (c1 in l or c2 in l):
            start=max(1,i-3)
            # find start of run by walking backwards to class
            for j in range(i-1,0,-1):
                if lines[j-1].startswith('class '):
                    start=j
                    break
            end=None
            brace=0
            inBody=False
            for k in range(i-1, len(lines)):
                if ('async run' in lines[k] or (k>i and lines[k].strip().startswith('async run'))) and not inBody:
                    inBody=True
                if inBody:
                    brace += lines[k].count('{') - lines[k].count('}')
                    if brace == 0 and ('return {' in lines[k] or 'return {' in l for l in [lines[k]]):
                        end=k
                        break
            if end is None:
                for j in range(k+1, min(len(lines), i+80)):
                    if 'onObservation' in lines[j] or 'notifyLocalAgent' in lines[j]:
                        end=j
                        break
                if end is None:
                    end=min(len(lines), k+120)
            print('\n'.join(f'{n}: {lines[n]}' for n in range(start, end+1)))
            print()
            break
PY

echo
echo "== inspect all adapter run return blocks =="
python3 - <<'PY'
from pathlib import Path
import re
text=Path('src/local-agent-adapters.ts').read_text()
for name, pattern in [
    ('Codex', r'(?m)^class CodexLocalAgentAdapter[\s\S]*?return \{[\s\S]*?\}\s*\n'),
    ('Claude', r'(?m)^class ClaudeLocalAgentAdapter[\s\S]*?return \{[\s\S]*?\}\s*\n'),
    ('OpenCode', r'(?m)^class OpencodeLocalAgentAdapter[\s\S]*?return \{[\s\S]*?\}\s*\n'),
    ('AcpLocal', r'(?m)^class AcpLocalAgentAdapter[\s\S]*?return \{[\s\S]*?\}\s*\n'),
    ('PiRpc', r'(?m)^class PiRpcLocalAgentAdapter[\s\S]*?return \{[\s\S]*?\}\s*\n'),
]:
    m=re.search(pattern, text)
    print(f'== {name} ==')
    print(m.group(0) if m else "no match")
PY

echo
echo "== workflow API non-schema final path =="
sed -n '160,185p' src/workflow-api.ts | cat -n

Repository: Waishnav/devspace

Length of output: 12002


Emit local-agent observations from the live adapters.

The packaged providers currently only pass onObservation through to the API, but notifyLocalAgentObservation is never called and the runtime adapters return only finalResponse, items, and usage() at the end. Add activity/usage emissions for the supported path, including Codex, Claude, Opencode, Pi, and the ACP adapters, so the worker can persist them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/local-agent-runtime.ts` around lines 50 - 51, Update the live runtime
adapters for Codex, Claude, Opencode, Pi, and ACP to invoke
notifyLocalAgentObservation for activity and usage events during execution,
rather than only forwarding onObservation to the API. Ensure emitted usage uses
LocalAgentTokenUsage and is persisted by the worker, while preserving the
existing finalResponse, items, and usage() results.

Source: Coding guidelines

Comment thread src/workflow-api.ts
Comment on lines +458 to +471
let latestProviderResult: WorkflowProviderRunResult | undefined;
const enforced = await enforceAgentSchema({
schema: agentOpts.schema,
prompt: providerPrompt,
provider,
run: (p, options) =>
deps.runProvider({
run: async (p, options) => {
latestProviderResult = await deps.runProvider({
...providerBase,
prompt: p,
providerSessionId: options.providerSessionId,
...(options.mode === "native" ? { schema: agentOpts.schema } : {}),
}),
});
return latestProviderResult;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Persist provider-result usage when schema enforcement fails.

Lines 464-470 retain latestProviderResult but do not merge its usage before schema enforcement can throw. On retry exhaustion, execution goes to Line 563, where flushUsage() persists only callback-derived usage. failAgentCall has no usage field.

If a provider reports usage only in its result, failed schema calls lose that usage. Merge result usage for each completed attempt and journal the latest usage before failAgentCall. Add a retry-exhaustion test that returns usage without emitting a usage observation.

Also applies to: 563-563

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow-api.ts` around lines 458 - 471, Update the schema-enforcement
flow around enforceAgentSchema and the retry-exhaustion path before
failAgentCall to merge usage from each completed latestProviderResult into the
usage journal, ensuring the latest provider usage is flushed even when
enforcement throws. Add a retry-exhaustion test where the provider returns usage
without emitting a usage observation, and verify that usage is persisted.

@Waishnav Waishnav closed this Aug 9, 2026
@Waishnav

Waishnav commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Closing this stack in favor of the alternative observability implementation. Review found lifecycle gaps in the persistence path, especially that observation/journal write failures can escape the best-effort telemetry boundary and interfere with provider-call finalization. The overall callback design had good ideas, but this version is not the one we are carrying forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant