Skip to content

feat(agents): OCG-backed long-term agent memory + feedback links#133

Open
NicholasDCole wants to merge 14 commits into
mainfrom
feature/add_memory
Open

feat(agents): OCG-backed long-term agent memory + feedback links#133
NicholasDCole wants to merge 14 commits into
mainfrom
feature/add_memory

Conversation

@NicholasDCole

@NicholasDCole NicholasDCole commented Jul 13, 2026

Copy link
Copy Markdown

Ports the SDK side of the agentspan "OCG-backed agent memory" feature (agentspan-ai/agentspan#298) to the TypeScript/JavaScript SDK. The server-side compiler that consumes this wire contract lives in conductor-oss/conductor (agentspan-server: LongTermMemoryConfig.java, AgentConfig.longTermMemory / feedbackSink).

What was ported

  • OCGMemoryStore (src/agents/ocg-memory.ts) — HTTP adapter over the OCG BFF: search (POST /api/v1/memories/search), add (POST /api/v1/memories), delete, listAll, clear, and feedbackLinks (POST /api/v1/memories/{key}/feedback-links). Bearer auth via Authorization: Bearer <token>.
  • Agent params (src/agents/agent.ts) — semanticMemory (an OCGMemoryStore, or a SemanticMemory wrapping one), memorySummaryModel, feedbackSink.
  • Serializer emission (src/agents/serializer.ts) — the most important piece; the server only activates the feature when this is emitted:
    • longTermMemory: {ocgUrl, credential, agent, user, scope, maxResults, summaryModel}, camelCase, nulls omitted. credential is a server-resolvable secret NAME (default OCG_PUBLIC_KEY), never the raw client token. summaryModel falls back to the agent's own model. Emitted only for OCG-backed stores.
    • feedbackSink: {taskName: "<agent>_feedback_sink"}, emitted when the sink is set.
  • Runtime worker (src/agents/runtime.ts) — registers the <agent>_feedback_sink worker so the compiled server path can hand the distilled summary + signed good/bad capability URLs to the local feedbackSink for out-of-band human delivery. Best-effort; never fails the run. The capability URLs are never surfaced to the agent's LLM.
  • HelpersFeedbackEvent, MemorySummary, buildMemorySummarizer, exported via src/agents/index.ts.
  • Exampleexamples/agents/118-ocg-memory.ts, mirroring the Python 118_ocg_memory.py.

JS/TS-specific adaptations

  • Async store. JS fetch is async-only, so OCGMemoryStore methods return Promises and it does not implement the SDK's synchronous MemoryStore interface (that stays keyword-search in-memory). The serializer reads the store structurally (duck-typed on ocgUrl), so both a bare OCGMemoryStore and a SemanticMemory wrapping one are supported.
  • No client-side pre/post-run retrieval/save hooks. The Python runtime.py wraps server execution with client-side memory-injection and conversation-save hooks. In this SDK, AgentRuntime.run() delegates the entire agent loop to the server via POST /agent/start, and run() sends the same serialized config — so the server's compiled longTermMemory steps (retrieval pre-loop, distill/save/feedback post-loop) drive memory on both the deployed and run() paths. Re-implementing those hooks client-side would duplicate the server's work and, given the sync/async MemoryStore mismatch above, does not map cleanly — so it is omitted. The one runtime piece that does map cleanly (and is ported) is the feedback_sink worker registration.
  • FeedbackEvent fields are camelCase (memoryKey, goodUrl, ...); the worker maps the server's snake_case task input.
  • Naming follows this SDK's camelCase conventions (semanticMemory, memorySummaryModel, feedbackSink, maxResults).

Tests

New/updated jest tests (src/agents/__tests__/ocg-memory.test.ts, serializer.test.ts):

  • Store: url normalization, blank-arg validation, add posts value (not string_value/confidence) with agent/user, search folds the good/bad signal into content, feedbackLinks route, bearer header, non-2xx → AgentAPIError.
  • Serializer: OCG-backed → longTermMemory + feedbackSink; SemanticMemory-wrapped store; absent memory → no keys; summaryModel fallback; plain non-OCG memory emits nothing.
  • Runtime: feedback_sink worker rebuilds a FeedbackEvent and calls the sink; swallows sink failures; not registered without a sink.

Typecheck clean for all touched files. Lint clean (one pre-existing warning in untouched agent.ts).

🤖 Generated with Claude Code

Kowser and others added 14 commits July 9, 2026 12:41
…orts

Merge the Agentspan TypeScript SDK source (@conductor-oss/conductor-agent-sdk
0.3.0, agentspan@f8704fa3) into this package as src/agents/, published only
via subpath exports: ./agents, ./agents/testing, ./agents/vercel-ai,
./agents/langgraph, ./agents/langchain. The root export surface is
byte-identical to before (verified via compiler API), so the one name
collision (Action) never materializes.

Changes beyond the verbatim copy:
- agent-client.ts / worker.ts: import from ../sdk and ../open-api instead of
  the package name (the only coupling to the workflow layer)
- tool.ts: rename the createRequire binding to peerRequire (top-level
  `require` is reserved in this repo's CJS module flavor)
- lint conformance to this repo's strict+stylistic ruleset (~41 fixes:
  no-op arrows, guard-throws replacing non-null assertions,
  Reflect.deleteProperty, for-of); no-explicit-any and
  no-unsafe-function-type stay at upstream's own "warn" contract via a
  scoped eslint override
- tsup.config.ts replaces the inline package.json block: 6 entries,
  esm+cjs+dts; optional peers external; verify:dist gate loads every
  exports subpath in both formats
- package.json: dotenv to dependencies (import-time side effect in
  config.ts), 5 optional peerDependencies, sideEffects allowlists
  dist/agents/**, tsx/zod/zod-to-json-schema devDeps
- tsconfig: lib es2022 -> es2023 (stream.ts uses Array.findLast; superset,
  Node >= 18 supports it), package-name paths for in-repo resolution

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
38 of upstream's 41 unit/cli-bin suites move to src/agents/__tests__/, where
the existing test:unit glob and per-PR CI matrix pick them up with zero
workflow changes. Test-count parity with upstream vitest is exact:
761 = 860 - 99 in the three deferred files. Deferred:
- kitchen-sink-structural.test.ts (imports examples/kitchen-sink; lands with
  the examples in the next commit)
- validation/{algorithmic,event-audit}.test.ts (they test the validation/
  LLM-judge harness, whose migration is explicitly deferred)

Port mechanics: vitest imports -> @jest/globals, vi.* -> jest.*,
toBeTypeOf/toHaveBeenCalledOnce -> jest equivalents, vi.stubGlobal -> local
helpers/stub-global.ts (upstream never unstubs; per-file isolation contains
it), relative ../../src paths -> colocated paths, skills fixtures colocated
under __tests__/fixtures/. The two jest.mock("child_process") factories
hoist correctly under ts-jest (verified green).

cli-bin/ lands at repo root (the Go CLI walk-up probe expects <dir>/cli-bin)
with imports retargeted to src/agents/. deploy.ts gets a type-only fix for
its workflowName read (upstream never typechecked cli-bin; runtime shape
unchanged).

jest.config.mjs: package-name mappers to in-repo sources plus the standard
ESM .js-suffix strip for relative imports. eslint: upstream's warn contract
extended to cli-bin; stylistic-only relaxations scoped to the migrated tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
221 example files (top-level + quickstart + adk/langgraph/openai/vercel-ai
framework subdirs) land at examples/agents/, the 6 agent docs at docs/agents/
(sidestepping the README and api-reference path collisions). All imports and
install instructions rewritten from @conductor-oss/conductor-agent-sdk to
@io-orkes/conductor-javascript (/agents subpaths), including the runtime
error-message strings in src/agents wrappers — zero old-name references
remain outside git history.

Run story: top-level/quickstart examples resolve the package name straight to
src/agents via examples/agents/tsconfig.json paths (npx tsx, no install);
framework subdirs install their own deps (scripts/install-example-deps.sh,
nonexistent langchain/ entry dropped) and gain an explicit
file:../../.. SDK dep; langgraph/ also gains the @langchain deps it
silently inherited from upstream's deleted examples workspace manifest.

Also lands the deferred kitchen-sink-structural.test.ts (66 tests; unit suite
now 1500 green) and a type-only fix upstream never caught because examples
were never typechecked: schedules-api.ts runNow's first overload makes opts
optional, matching the documented 1-arg default usage.

Root tsconfig excludes examples/agents (framework subdir deps are not
installed at the root); eslint gains a scoped override for the four rules the
didactic files break en masse — upstream ignored examples/ in lint entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All 25 upstream e2e suites move to e2e/ (with fixtures/, _configs/, and the
loose harness scripts under e2e/tools/), run by jest.e2e.config.mjs:
testMatch e2e/**, 60s default timeout, maxWorkers 3 (upstream's maxForks 3),
jest-junit to results/junit-e2e.xml. New script: npm run test:agent-e2e
(--forceExit, matching the repo's other jest scripts — agent polling keeps
handles open after LLM suites). Deliberately invisible to
test/test:unit/test:integration globs — per-PR CI cost is unchanged
(verified: unit run still 74 suites).

Beyond the mechanical vitest-to-jest pass, six vitest-isms needed real
porting (verified by loading all 25 suites under jest — 196 tests enumerate
clean — and by running suites 1/2/9 against a live release-JAR server):
- expect(actual, message) two-arg form (265 sites, jest 30 rejects it) ->
  expectMsg helper in e2e/helpers.ts that prepends the diagnostic message
  to matcher failures
- describe(name, { timeout }, fn) options object (19 files) -> file-level
  jest.setTimeout
- it.skipIf(cond) (19 sites) -> itSkipIf helper
- it.fails -> it.failing (suite 7)
- it(name, { retry: 2 }, fn) (4 sites) -> file-level jest.retryTimes(2)
  (jest has no per-test retry; both files are LLM-variability suites)
- top-level await gates (suites 11/21/23; illegal in CJS) -> synchronous
  require for the langgraph availability probe, and fail-hard beforeAll
  health/scheduler gates (matches the TS-e2e fail-hard philosophy; the CI
  workflow health-gates the server before tests run)

Live-server proof (local V1): suites 1/2/9 against agentspan-server 0.4.0 —
11 tests pass; the only failures are server-side OpenAI 401s from the
missing local LLM key (the CI run with repo secrets is the full proof).

Unused-var noise (port artifacts + upstream) fixed with _-prefixes and
import trims; the stylistic eslint relaxations for migrated tests extend to
e2e/**. jest-junit output dirs (reports/, results/) are now gitignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of the Python SDK's implemented agent-e2e workflow. Kept verbatim:
pull_request + workflow_dispatch triggers, per-ref concurrency with
cancel-in-progress, pinned AGENTSPAN_VERSION 0.4.0 (server JAR from the
GitHub release, cached by version; linux CLI binary), temurin 21, hard
90s health gate that dumps server.log on failure, executed>0 junit guard
(defense-in-depth here — the TS suites fail hard when the server is down),
junit + HTML + server.log artifacts at 14-day retention, 45-min timeout,
job-level OPENAI_API_KEY/ANTHROPIC_API_KEY secrets (must be added to this
repo's settings before the first live run).

JS deltas: Node 22 + npm ci + npm run build + npm run test:agent-e2e as the
test step; Python setup retained only for mcp-testkit; HTML report via
npx tsx e2e/generate-report.ts; no CONDUCTOR_MP_START_METHOD (Python
multiprocessing concern, no JS analog).

Rehearsed locally: actionlint clean; health gate passed against the booted
0.4.0 JAR; guard correctly fails an all-skipped junit (0/196) and the
report generator renders the real junit output. The test:agent-e2e script
pins jest-junit's output to results/junit-e2e.xml via env vars — the
package.json jest-junit block (reports/jest-junit.xml) outranks reporter
options, and env vars outrank the block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README: new Durable AI Agents section (subpath quickstart mirroring
examples/agents/quickstart/01-basic-agent.ts, wrapper/testing subpaths,
docs + examples pointers) and a Documentation-table row.

AGENTS.md: src/agents feature area — repo layout additions (src/agents,
e2e/, cli-bin/, examples/agents, docs/agents), the subpath-only export rule
(root re-export ban and the OpenAPI Action collision that motivates it),
colocated-test and e2e conventions, example run story, AGENTSPAN_* config
surface, and the agent-e2e command.

CHANGELOG: additive-minor entry with a migration note for
@conductor-oss/conductor-agent-sdk users (specifier rewrite, /agents-prefixed
wrapper subpaths, optional peers, dotenv/sideEffects behavior).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
actions/setup-python's `cache: pip` hard-fails when the repo has no
requirements.txt or pyproject.toml to hash for the cache key — true for
this repo, where Python exists only to run mcp-testkit. Carried over
from the Python SDK's agent-e2e workflow, where a pyproject.toml exists.

First observed on PR #131: the job died in "Set up Python" before any
test ran, and the silently-empty guard correctly reported 0/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Stage-4 lint pass blanket-prefixed suite 10's diag and codeOutputs
locals as unused, but most scopes still reference the bare names inside
expectMsg messages and output assertions — ReferenceError at runtime.
ts-jest is transpile-only, so it surfaced only in the first secrets-
enabled CI run (6 failures, the run's only red).

Only genuinely-unused declarations keep the underscore (upstream parity;
upstream never linted e2e). Verified live: suite 10 vs local server JAR
0.4.0 with real keys — 8 passed, 1 skipped (Jupyter, needs kernel).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… agent lifecycle surface

Per-schedule pause/resume verbs differ by server family: OSS/embedded
Conductor maps them PUT-only, Orkes Conductor GET-only. SchedulerClient
now issues PUT first and retries via GET only on HTTP 405, stateless per
call, with the pause reason re-applied on the fallback URL (mirrors the
Python SDK's OrkesSchedulerClient). Implemented in the hand-written
wrapper via raw non-throwing client calls — the generated transport is
untouched and regeneration stays safe.

SchedulerClient also absorbs the typed agent-schedule lifecycle surface
(save/get/listForAgent/pause/resume/delete/runNow/previewNext/reconcile)
operating on Schedule/ScheduleInfo with wire-name prefixing and typed
errors via _translate. The models/mapping/errors relocate verbatim from
src/agents/schedule.ts to src/sdk/clients/agent/schedule.ts (Python
parity: conductor/client/ai/schedule.py); the agents module re-exports
them. Ctor widened to Client | PromiseLike<Client> so callers with async
client construction keep synchronous accessors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e SchedulerClient

BREAKING CHANGE (agent layer, no backward compatibility): the raw-fetch
ScheduleClient class and SchedulerFetcher interface are gone from the
./agents surface. SchedulerClient — which absorbed the same typed
lifecycle methods (save/get/listForAgent/pause/resume/delete/runNow/
previewNext/reconcile) in the previous commit — is re-exported from
./agents in their place; every method call site keeps its name and
signature. The scheduling e2e suite needed exactly two lines (import +
one type annotation); the schedules.* module facade, runtime
schedulesClient(), deploy({schedules}) reconciliation and
AgentClient.schedule() are unchanged.

AgentClient.schedules now hands its memoized Conductor-client promise to
SchedulerClient (single transport; the SchedulerFetcher raw-fetch path is
deleted, _rawRequestUntyped remains for /agent/* control-plane calls).
An absence test pins the deletion, mirroring the Python SDK's
test_agent_schedule_client_is_gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…posed via OrkesClients

Moves the agent control-plane client and the agent-flavored workflow
client from src/agents/ to src/sdk/clients/agent/ (names kept — nothing
on the root surface collides: Conductor's workflow client is
WorkflowExecutor). OrkesClients gains getAgentClient() and
getAgentWorkflowClient(); AgentClient's constructor accepts an optional
pre-built client ((AgentConfigOptions & {client?}) | AgentConfig) which
pre-seeds the memoized promise, so the factory path reuses one Conductor
client across workflow + schedule surfaces. The /agent/* raw transport
and JWT mint path are unchanged; decodeJwtExp stays exported.

Cycle guard: the moved files import createConductorClient and the agent
domain modules via deep paths, never the sdk barrel; madge reports no
new cycles (the AgentClient↔WorkflowClient type-only edge predates the
move). The ./agents subpath re-exports everything from the new homes —
its surface is unchanged. The enriched ConductorClient type is not
re-exported at root (the deprecated bare alias keeps that name).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CHANGELOG (Unreleased): OrkesClients agent getters, PUT-first/405→GET
pause-resume verb handling, absorbed typed schedule lifecycle, and the
ScheduleClient/SchedulerFetcher removal with its one-line migration.
Agent docs re-point the schedule surface at SchedulerClient and note the
WorkflowExecutor vs agent WorkflowClient distinction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port the agentspan "OCG-backed agent memory" feature (agentspan-ai/agentspan#298)
to the TypeScript SDK.

- OCGMemoryStore: async HTTP adapter over the OCG BFF (search / save /
  delete / list / feedback-links mint), using fetch.
- Agent params: semanticMemory, memorySummaryModel, feedbackSink.
- Serializer: emit longTermMemory {ocgUrl, credential, agent, user, scope,
  maxResults, summaryModel} + feedbackSink {taskName} — the wire contract the
  server-side compiler activates on (credential is a server-resolvable secret
  NAME, never the raw client token).
- Runtime: register the {agent}_feedback_sink worker so the compiled path can
  hand human good/bad capability links to the local sink (best-effort).
- FeedbackEvent / MemorySummary / buildMemorySummarizer helpers.
- Tests (jest) for the store, the serializer emission, and the feedback worker.
- Example examples/agents/118-ocg-memory.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.05302% with 38 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/agents/ocg-memory.ts 90.05% 38 Missing ⚠️
Flag Coverage Δ
integration-v4-sm 41.26% <9.32%> (-0.53%) ⬇️
integration-v5-sdkdev 43.63% <9.32%> (-0.65%) ⬇️
unit 73.71% <92.68%> (+0.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/agents/agent.ts 92.22% <100.00%> (+0.45%) ⬆️
src/agents/memory.ts 95.42% <100.00%> (+0.01%) ⬆️
src/agents/runtime.ts 67.60% <100.00%> (+0.84%) ⬆️
src/agents/serializer.ts 93.06% <100.00%> (+1.00%) ⬆️
src/agents/ocg-memory.ts 90.05% <90.05%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Base automatically changed from feature/combine-agentspan to main July 14, 2026 21:23
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.

2 participants