feat(ts): complete runtime parity and durable turn engine - #436
feat(ts): complete runtime parity and durable turn engine#436sethjuarez wants to merge 9 commits into
Conversation
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>
There was a problem hiding this comment.
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
| 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}]`, | ||
| ); |
There was a problem hiding this comment.
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>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
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 functionreturningPromise<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
There was a problem hiding this comment.
Review details
Suppressed comments (2)
runtime/typescript/packages/core/src/core/pipeline.ts:1203
- finalizeSimpleTurnStream() buffers the entire stream into
collectedeven whenrawis 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_idas 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>
There was a problem hiding this comment.
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
Summary
.promptyload/render/parse/prepare behavior with structured YAML references, request-local rich-input markers, concurrent preparation safety, prompt-declared tool activation, and durable denied-tool resultsValidation
@prompty/core,@prompty/openai,@prompty/foundry,@prompty/anthropicgit diff --check origin/main...HEADcleanResolved cross-runtime follow-up
TypeScript follows the written turn-engine contract: permission-denied tools journal one failed
tool_result_committedevent 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 onmain, this branch only needs a merge-base update and core-suite rerun to consume the stricter vector.The local provider
.envremains 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
ModelInfomodality collections: native save still materialized both omitted collections as empty arrays, and no TypeScript generated files changed. The existingcreateModelInfonormalization 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:ModelInfoconstructor andload()values remain absent fromsave()The fix is sufficient to simplify
createModelInfo.@typra/emitter@0.4.6(integritysha512-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/StreamFailureschema, generated models, shared vectors, and provider/engine integration. Five handwritten TypeScript files overlap this PR (openaiandanthropicprocessors, 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.