Skip to content

feat(ts): complete runtime parity and durable turn engine - #436

Open
sethjuarez wants to merge 9 commits into
microsoft:mainfrom
sethjuarez:sejuare-microsoft/typescript-runtime-parity
Open

feat(ts): complete runtime parity and durable turn engine#436
sethjuarez wants to merge 9 commits into
microsoft:mainfrom
sethjuarez:sejuare-microsoft/typescript-runtime-parity

Conversation

@sethjuarez

@sethjuarez sethjuarez commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

  • add the canonical durable TypeScript turn engine with ordered events, checkpoints, resume/reconciliation, retries, cancellation, context packing, post-commit handling, and shared engine-vector coverage
  • harden .prompty load/render/parse/prepare behavior with structured YAML references, request-local rich-input markers, concurrent preparation safety, prompt-declared tool activation, and durable denied-tool results
  • align OpenAI, Foundry, and Anthropic model discovery with shared capability vectors and add canonical generated streaming chunks while preserving legacy stream compatibility
  • cover structured output, streaming tool calls, usage, replay, recovery, and equal retry behavior for simple and tool-driven turns without editing generated model code

Validation

  • sequential builds: @prompty/core, @prompty/openai, @prompty/foundry, @prompty/anthropic
  • TypeScript lint/typecheck: all four workspaces green
  • core: 171 files, 1,578 passed, 17 skipped
  • OpenAI: 6 files, 102 passed, 10 skipped
  • Foundry: 4 passed / 2 skipped files, 50 passed, 12 skipped
  • Anthropic: 3 files, 34 passed
  • live OpenAI integration: 8 passed, 10 skipped (optional credentials/models unavailable)
  • live Anthropic integration: 5 passed
  • git diff --check origin/main...HEAD clean

Resolved cross-runtime follow-up

TypeScript follows the written turn-engine contract: permission-denied tools journal one failed tool_result_committed event and emit no tool execution events. PR #437 contains the corresponding Rust fix and explicit shared-vector assertion, resolving the prior cross-runtime gap. There is no changed-file overlap with this PR; once #437 lands on main, this branch only needs a merge-base update and core-suite rerun to consume the stricter vector.

The local provider .env remains ignored and uncommitted.

Typra optional-collection validation

Typra 0.4.3 was pinned locally, regenerated, and tested against the four discovery semantics. It did not preserve absent TypeScript ModelInfo modality collections: native save still materialized both omitted collections as empty arrays, and no TypeScript generated files changed. The existing createModelInfo normalization therefore remained required.

Typra 0.4.4 was then installed with the published integrity sha512-yKnxXyxwxrss9KSWvNI/zYKso9/8L6shViUwP96yaVjf+VJJcA+C2eeffjFJFgeiedHROE/Pdj6J3Tb6blVUQQ== and regenerated without hand-editing generated output. Native TypeScript proof passed:

  • omitted ModelInfo constructor and load() values remain absent from save()
  • explicit empty modality arrays survive constructor/load/save
  • required collections retain their empty-array defaults
  • focused discovery suites passed: core 9, OpenAI 10, Foundry 11, Anthropic 4

The fix is sufficient to simplify createModelInfo. @typra/emitter@0.4.6 (integrity sha512-8YAZaaJCE7blO5bDTtaTxJfMykIhED4PRlsyhnMrSMTmREJIaUadKcPjdfWLEeWvB3Fdy775G8EJweOuVzrn6A==) is the current minimum known to contain the verified TypeScript fix; broader emitter parity work remains active, so this PR does not preselect it as the final adoption version. The earlier 0.4.4 regeneration spanned 316 files across generated runtimes and overlapped PR #438's schema/generated stream baseline, while #438 was not yet merge-ready. To avoid accepting two conflicting generated baselines, the probe was restored to a clean, verified 0.4.2 worktree. After #438 lands, this branch will select the latest validated emitter, regenerate once from the combined schema, remove the now-unnecessary omission deletion, and rerun focused discovery plus full TypeScript/provider validation.

Canonical stream-failure dependency

PR #438 owns the canonical FailureChunk / StreamFailure schema, generated models, shared vectors, and provider/engine integration. Five handwritten TypeScript files overlap this PR (openai and anthropic processors, core pipeline, and two export surfaces), so this branch intentionally does not duplicate that work. After #438 lands, #436 must update its merge base and reconcile those surfaces by preserving its request-local rendering, canonical streaming compatibility, retry, and durability behavior while consuming #438's classified failure, partial-content, and reconciliation semantics. The combined branch must rerun stream-failure vectors, turn/recovery vectors, full workspace validation, live provider E2E, and CI.

sethjuarez and others added 6 commits August 3, 2026 23:01
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Parse YAML file references as structured data and keep canonical rich-input nonce state local to each render request.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Consume shared capability vectors, add Anthropic model listing, normalize provider discovery shapes, and expose canonical generated stream chunks without breaking legacy stream consumers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add generated-model-backed turn orchestration with ordered durable events, atomic checkpoints, cancellation, retries, resume, reconciliation, post-commit effects, and shared engine vector coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 4, 2026 07:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR advances the TypeScript runtime toward full cross-runtime parity by introducing a canonical, durable turn engine (with checkpointing/resume/reconciliation) and by standardizing provider streaming and model discovery through shared capability/vector datasets.

Changes:

  • Adds a durable, provider-neutral TypeScript turn engine with event journaling, checkpoints, resume/recovery, retries, cancellation boundaries, and post-commit handling.
  • Introduces canonical generated streaming chunks (TextChunk, ToolChunk, UsageChunk, ErrorChunk, ThinkingChunk) with provider processors emitting canonical chunks while preserving legacy stream compatibility.
  • Aligns provider model discovery (OpenAI/Foundry/Anthropic) to a shared capability enrichment dataset and adds shared “vector” tests for discovery and engine behavior.
Show a summary per file
File Description
runtime/typescript/packages/openai/tests/models.test.ts Updates expectations to treat modalities as omitted (undefined) when not provided/enriched.
runtime/typescript/packages/openai/tests/e2e.test.ts Adds E2E coverage for canonical streaming chunk emission (text/tool/usage/error) via processStream.
runtime/typescript/packages/openai/tests/discovery-vectors.test.ts Adds OpenAI discovery vector tests driven by shared spec vectors.
runtime/typescript/packages/openai/src/processor.ts Adds canonical processStream() yielding generated StreamChunks and adapts legacy streaming via legacyStreamGenerator.
runtime/typescript/packages/openai/src/models.ts Refactors model discovery to modelInfoFromWire() + shared enrichment (enrichModelInfo) and createModelInfo semantics.
runtime/typescript/packages/openai/src/index.ts Re-exports processStream and modelInfoFromWire as part of the OpenAI package public surface.
runtime/typescript/packages/foundry/tests/models.test.ts Updates expectations for modalities to be omitted when Azure APIs don’t return them.
runtime/typescript/packages/foundry/tests/discovery-vectors.test.ts Adds Foundry discovery vector tests (catalog + deployment shapes) driven by shared spec vectors.
runtime/typescript/packages/foundry/src/processor.ts Implements processStream() by delegating to OpenAI’s canonical stream processor.
runtime/typescript/packages/foundry/src/index.ts Re-exports model mapping helpers used by vector tests and other consumers.
runtime/typescript/packages/foundry/src/azure-models.ts Adds deploymentToModelInfo / catalogModelToModelInfo mapping using shared enrichment and preserving raw properties.
runtime/typescript/packages/core/tests/turn-engine-vectors.test.ts Adds shared vector-based tests validating canonical engine semantics (events, snapshots, ordering, portability).
runtime/typescript/packages/core/tests/turn-engine-recovery.test.ts Adds recovery/resume/reconciliation tests for durability failures, indeterminate effects, retry boundaries, and cancellation.
runtime/typescript/packages/core/tests/turn-engine-harness.ts Introduces deterministic harness ports (model/tools/durability/clock/ids) to drive engine tests.
runtime/typescript/packages/core/tests/spec-vectors.test.ts Removes nonce-pattern adaptation now that the TS runtime emits the canonical marker format.
runtime/typescript/packages/core/tests/resilience.test.ts Updates retry behavior expectations to retry “simple mode” turns when configured.
runtime/typescript/packages/core/tests/pipeline.test.ts Adds coverage for request-local rich-input markers, concurrent prepare isolation, cancellation-before-prepare, and stream completion event timing.
runtime/typescript/packages/core/tests/loader.test.ts Adds coverage for structured YAML/YML ${file:...} references while preserving JSON/text reference behavior.
runtime/typescript/packages/core/tests/discovery.test.ts Adds tests for shared model capability enrichment vectors (enrichModelInfo + createModelInfo).
runtime/typescript/packages/core/src/renderers/nunjucks.ts Moves rich-input marker preparation out of renderer into pipeline-managed input preparation.
runtime/typescript/packages/core/src/renderers/mustache.ts Same as nunjucks: renderers now render provided inputs without mutating/augmenting them.
runtime/typescript/packages/core/src/renderers/index.ts Narrows renderer exports to avoid exposing removed global nonce state helpers.
runtime/typescript/packages/core/src/renderers/common.ts Switches to canonical request-local rich-input marker format and removes shared/global nonce state.
runtime/typescript/packages/core/src/index.ts Exposes turn-engine APIs and canonical stream chunk types from @prompty/core.
runtime/typescript/packages/core/src/harness/turn-runner.ts Records denied-tool results as durable tool-result events to preserve ordering and replay behavior.
runtime/typescript/packages/core/src/core/turn-engine.ts Adds the canonical durable turn engine state machine (events, checkpoints, retries, cancellation, reconciliation, resume).
runtime/typescript/packages/core/src/core/turn-engine-ports.ts Adds runtime-local effect port interfaces and defaults used by the canonical turn engine.
runtime/typescript/packages/core/src/core/turn-engine-context.ts Adds ordered context assembly pipeline that produces immutable, deeply-frozen invocation snapshots.
runtime/typescript/packages/core/src/core/turn-engine-cancellation.ts Adds a runtime cancellation token that can bridge AbortSignal and supports synchronous/async cancellation handling.
runtime/typescript/packages/core/src/core/pipeline.ts Adds canonical processStream() and updates render/prepare/turn to use request-local rich markers, improved cancellation and retry/stream event timing.
runtime/typescript/packages/core/src/core/loader.ts Adds structured YAML/YML parsing for ${file:...} references.
runtime/typescript/packages/core/src/core/interfaces.ts Extends Processor with optional processStream() for canonical streaming chunk conversion.
runtime/typescript/packages/core/src/core/index.ts Re-exports discovery helpers, canonical processStream, and turn-engine modules from the core sub-entry.
runtime/typescript/packages/core/src/core/discovery.ts Adds shared capability enrichment (enrichModelInfo) and createModelInfo to preserve omitted optional collections.
runtime/typescript/packages/anthropic/tests/models.test.ts Adds Anthropic discovery vector tests and pagination coverage for listModels().
runtime/typescript/packages/anthropic/tests/e2e.test.ts Adds E2E coverage for canonical streaming chunk emission (text/thinking/tool/usage/error) via processStream.
runtime/typescript/packages/anthropic/src/processor.ts Implements canonical processStream() yielding StreamChunks and adapts legacy streaming behavior.
runtime/typescript/packages/anthropic/src/models.ts Adds Anthropic model discovery (listModels) + mapping/enrichment via shared capability dataset.
runtime/typescript/packages/anthropic/src/index.ts Re-exports processStream, listModels, and modelInfoFromWire as part of the Anthropic package public surface.

Review details

  • Files reviewed: 39/39 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines 107 to +111
if (typeof value === "string") {
let result = value;
for (const [nonce, display] of replacements) {
result = result.replaceAll(nonce, display);
}
return result;
return value.replace(
/__PROMPTY_THREAD_[a-f0-9]{8}_(\w+)__/g,
(_nonce, name: string) => `[thread: ${name}]`,
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in fb97ba7. The sanitizer now accepts punctuation in rich-input names, and render tracing sanitizes both inputs and results. Added a regression using conversation-history; the focused build/typecheck/test and full core suite are green.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 4, 2026 07:09
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

Suppressed comments (2)

runtime/typescript/packages/core/src/renderers/common.ts:33

  • prepareRenderInputs() unconditionally injects a nonce for every rich input declared on the prompt, even when the caller didn’t supply that input (and it has no default). This changes template semantics: an omitted optional rich input becomes a truthy string during rendering (e.g., Nunjucks/Mustache conditionals will treat it as present), and can also overwrite intentional “missing” behavior.

Only substitute a nonce when the input value is actually present (or was defaulted by validateInputs), so omitted optional inputs remain omitted/undefined during rendering.

  for (const [name, kind] of Object.entries(richNames)) {
    if (kind === "thread" || RICH_KINDS.has(kind)) {
      const nonce = `__PROMPTY_THREAD_${randomBytes(4).toString("hex")}_${name}__`;
      nonces.set(name, nonce);
      modified[name] = nonce;
    }

runtime/typescript/packages/core/src/core/pipeline.ts:319

  • core.processStream() is exported as an async function returning Promise<AsyncIterable<StreamChunk>>. In JS, this makes the most natural usage (for await (const c of processStream(agent, stream))) fail at runtime because you’d be iterating a Promise, not an async iterable.

Consider returning an AsyncIterable<StreamChunk> directly and doing any necessary await inside an async-generator wrapper for the legacy-adaptation path, so consumers can always iterate the result without a separate await.

export async function processStream(
  agent: Prompty,
  response: AsyncIterable<unknown>,
): Promise<AsyncIterable<StreamChunk>> {
  const provider = resolveProvider(agent);
  const processor = getProcessor(provider);
  if (processor.processStream) {
    return processor.processStream(response);
  }

  const legacy = await processor.process(agent, response);
  return adaptLegacyStream(legacy);
}
  • Files reviewed: 39/39 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings August 4, 2026 07:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

Suppressed comments (2)

runtime/typescript/packages/core/src/core/pipeline.ts:1203

  • finalizeSimpleTurnStream() buffers the entire stream into collected even when raw is false (processed stream). For long streams this is unbounded memory growth; the code only needs the concatenated text response for events/turn_end in the non-raw path.
  const collected: unknown[] = [];
  try {
    for await (const item of stream) {
      collected.push(item);
      if (!raw && typeof item === "string") {

runtime/typescript/packages/openai/src/processor.ts:231

  • appendResponsesArguments() will treat a missing/invalid call_id as an empty string (via stringValue). If any accumulated call entry still has an empty id, this can append argument deltas to the wrong tool call. Guard against empty call ids before attempting the lookup.
  • Files reviewed: 39/39 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 4, 2026 22:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

Suppressed comments (2)

runtime/typescript/packages/core/src/core/pipeline.ts:319

  • processStream() is a newly exported pipeline step but it currently emits no trace span, while other leaf steps (render/parse/prepare/run/turn) do. Adding a minimal trace span here keeps pipeline observability consistent, especially for streaming failures that occur before downstream consumption.
export async function processStream(
  agent: Prompty,
  response: AsyncIterable<unknown>,
): Promise<AsyncIterable<StreamChunk>> {
  const provider = resolveProvider(agent);
  const processor = getProcessor(provider);
  if (processor.processStream) {
    return processor.processStream(response);
  }

  const legacy = await processor.process(agent, response);
  return adaptLegacyStream(legacy);
}

runtime/typescript/packages/core/src/core/pipeline.ts:105

  • The sanitizeNonces() doc comment says it produces {{thread:name}}, but the implementation replaces nonce markers with [thread: <name>]. This mismatch can confuse anyone interpreting trace output.
/** Replace raw nonce strings with readable `{{thread:name}}` in trace output. */
  • Files reviewed: 39/39 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

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