Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/add-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,12 @@ If anything matches, run the affected provider tests and update assertions as ne

### New API behavior is NOT data-driven

The Consumption Matrix (Step 2) tells you which capability *flags* are honored by existing provider code. But if the new model needs **net-new** request handling that the provider doesn't implement yet — a new beta header (e.g. Anthropic's `anthropic-beta` structured-outputs header in `anthropic/index.ts`), a new thinking/reasoning encoding, a Responses-API quirk — you must edit `apps/sim/providers/<provider>/core.ts` / `index.ts`. Setting a flag whose behavior isn't implemented is a silent no-op.
The Consumption Matrix (Step 2) tells you which capability *flags* are honored by existing provider code. But if the new model needs **net-new** request handling that the provider doesn't implement yet — a new beta header (e.g. Anthropic's `anthropic-beta` structured-outputs header in `anthropic/index.ts`), a new thinking/reasoning encoding, a Responses-API quirk — you must edit `apps/sim/providers/<provider>/core.ts` / `index.ts`. Setting a flag whose behavior isn't implemented is a silent no-op. When you do edit provider code, reuse the shared helpers rather than hand-rolling: streaming responses are assembled via `createStreamingExecution` (`@/providers/streaming-execution`) and tool schemas via `adaptOpenAIChatToolSchema` / `adaptAnthropicToolSchema` (`@/providers/tool-schema-adapter`).

### Wrong family entirely?

- **Embedding or rerank model** → it does NOT go in the `models[]` array. Use `EMBEDDING_MODEL_PRICING` / `RERANK_MODEL_PRICING` in `models.ts` instead.
- **Brand-new provider** (not just a new model under an existing one) → much larger surface: add the id to `ProviderId` in `providers/types.ts`, a registry entry in `providers/registry.ts`, a provider implementation under `providers/<id>/`, an icon in `components/icons.tsx`, and the `PROVIDER_DEFINITIONS` block. That is beyond this skill — tell the user.
- **Brand-new provider** (not just a new model under an existing one) → much larger surface: add the id to `ProviderId` in `providers/types.ts`, a registry entry in `providers/registry.ts`, a provider implementation under `providers/<id>/` (assemble streaming responses with `createStreamingExecution` and wrap tool schemas with the `@/providers/tool-schema-adapter` helpers), an icon in `components/icons.tsx`, and the `PROVIDER_DEFINITIONS` block. That is beyond this skill — tell the user.

## Step 5: Write, lint

Expand Down
10 changes: 4 additions & 6 deletions apps/sim/executor/execution/state.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import type { BlockStateController } from '@/executor/execution/types'
import type { BlockState, NormalizedBlockOutput } from '@/executor/types'
import { SubflowNodeIdCodec } from '@/executor/utils/subflow-node-id-codec'
import {
buildOuterBranchScopedId,
extractOuterBranchIndex,
stripCloneSuffixes,
} from '@/executor/utils/subflow-utils'

const BRANCH_SUFFIX_PATTERN = /₍\d+₎/u
const LOOP_SUFFIX_PATTERN = /_loop\d+/

function normalizeLookupId(id: string): string {
return id.replace(/₍\d+₎/gu, '').replace(/_loop\d+/g, '')
return SubflowNodeIdCodec.normalizeLookupId(id)
}

function extractBranchSuffix(id: string): string {
return id.match(BRANCH_SUFFIX_PATTERN)?.[0] ?? ''
return SubflowNodeIdCodec.extractBranchSuffix(id)
}

function extractLoopSuffix(id: string): string {
return id.match(LOOP_SUFFIX_PATTERN)?.[0] ?? ''
return SubflowNodeIdCodec.extractLoopSuffix(id)
}
export interface LoopScope {
iteration: number
Expand Down
175 changes: 175 additions & 0 deletions apps/sim/executor/utils/subflow-node-id-codec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { SubflowNodeIdCodec } from '@/executor/utils/subflow-node-id-codec'

describe('SubflowNodeIdCodec', () => {
describe('branch subscripts', () => {
it('builds and round-trips branch node IDs', () => {
const id = SubflowNodeIdCodec.buildBranchNodeId('block-1', 2)
expect(id).toBe('block-1₍2₎')
expect(SubflowNodeIdCodec.isBranchNodeId(id)).toBe(true)
expect(SubflowNodeIdCodec.extractBaseBlockId(id)).toBe('block-1')
expect(SubflowNodeIdCodec.extractBranchIndex(id)).toBe(2)
})

it('returns null index and false predicate for non-branch IDs', () => {
expect(SubflowNodeIdCodec.isBranchNodeId('block-1')).toBe(false)
expect(SubflowNodeIdCodec.extractBranchIndex('block-1')).toBeNull()
expect(SubflowNodeIdCodec.extractBaseBlockId('block-1')).toBe('block-1')
})

it('only strips a trailing branch subscript', () => {
expect(SubflowNodeIdCodec.extractBaseBlockId('a₍1₎b')).toBe('a₍1₎b')
expect(SubflowNodeIdCodec.extractBaseBlockId('a₍1₎b₍2₎')).toBe('a₍1₎b')
})
})

describe('loop sentinels', () => {
it('builds and parses loop sentinel IDs', () => {
const start = SubflowNodeIdCodec.buildLoopSentinelStartId('loop-1')
const end = SubflowNodeIdCodec.buildLoopSentinelEndId('loop-1')
expect(start).toBe('loop-loop-1-sentinel-start')
expect(end).toBe('loop-loop-1-sentinel-end')
expect(SubflowNodeIdCodec.isLoopSentinelNodeId(start)).toBe(true)
expect(SubflowNodeIdCodec.isLoopSentinelNodeId(end)).toBe(true)
expect(SubflowNodeIdCodec.extractLoopIdFromSentinel(start)).toBe('loop-1')
expect(SubflowNodeIdCodec.extractLoopIdFromSentinel(end)).toBe('loop-1')
})

it('returns null when not a loop sentinel', () => {
expect(SubflowNodeIdCodec.isLoopSentinelNodeId('block-1')).toBe(false)
expect(SubflowNodeIdCodec.extractLoopIdFromSentinel('block-1')).toBeNull()
})
})

describe('parallel sentinels', () => {
it('builds and parses parallel sentinel IDs', () => {
const start = SubflowNodeIdCodec.buildParallelSentinelStartId('p-1')
const end = SubflowNodeIdCodec.buildParallelSentinelEndId('p-1')
expect(start).toBe('parallel-p-1-sentinel-start')
expect(end).toBe('parallel-p-1-sentinel-end')
expect(SubflowNodeIdCodec.isParallelSentinelNodeId(start)).toBe(true)
expect(SubflowNodeIdCodec.isParallelSentinelNodeId(end)).toBe(true)
expect(SubflowNodeIdCodec.extractParallelIdFromSentinel(start)).toBe('p-1')
expect(SubflowNodeIdCodec.extractParallelIdFromSentinel(end)).toBe('p-1')
})

it('returns null when not a parallel sentinel', () => {
expect(SubflowNodeIdCodec.isParallelSentinelNodeId('block-1')).toBe(false)
expect(SubflowNodeIdCodec.extractParallelIdFromSentinel('block-1')).toBeNull()
})
})

describe('outer-branch clone scoping', () => {
it('builds and extracts outer branch index', () => {
const id = SubflowNodeIdCodec.buildOuterBranchScopedId('loop-1', 3)
expect(id).toBe('loop-1__obranch-3')
expect(SubflowNodeIdCodec.extractOuterBranchIndex(id)).toBe(3)
})

it('extracts the innermost outer branch index for nested clones', () => {
const id = 'loop-1__obranch-2__obranch-5'
expect(SubflowNodeIdCodec.extractOuterBranchIndex(id)).toBe(2)
expect(SubflowNodeIdCodec.extractInnermostOuterBranchIndex(id)).toBe(5)
})

it('returns undefined when no outer branch suffix is present', () => {
expect(SubflowNodeIdCodec.extractOuterBranchIndex('loop-1')).toBeUndefined()
expect(SubflowNodeIdCodec.extractInnermostOuterBranchIndex('loop-1')).toBeUndefined()
})

it('strips outer-branch and clone-digest suffixes', () => {
expect(SubflowNodeIdCodec.stripOuterBranchSuffix('loop-1__obranch-2')).toBe('loop-1')
expect(SubflowNodeIdCodec.stripOuterBranchSuffix('loop-1__cloneABCDEF__obranch-2')).toBe(
'loop-1'
)
})

it('strips all clone suffixes and branch subscripts to the base block ID', () => {
expect(SubflowNodeIdCodec.stripCloneSuffixes('block-1__obranch-2₍3₎')).toBe('block-1')
expect(SubflowNodeIdCodec.stripCloneSuffixes('block-1__clone0a1f__obranch-2₍0₎')).toBe(
'block-1'
)
})
})

describe('normalizeNodeId', () => {
it('normalizes branch, loop sentinel, and parallel sentinel IDs', () => {
expect(SubflowNodeIdCodec.normalizeNodeId('block-1₍2₎')).toBe('block-1')
expect(SubflowNodeIdCodec.normalizeNodeId('loop-loop-1-sentinel-start')).toBe('loop-1')
expect(SubflowNodeIdCodec.normalizeNodeId('parallel-p-1-sentinel-end')).toBe('p-1')
expect(SubflowNodeIdCodec.normalizeNodeId('block-1')).toBe('block-1')
})
})

describe('loop digest lookup helpers', () => {
it('strips branch subscripts and loop digests for lookup keys', () => {
expect(SubflowNodeIdCodec.normalizeLookupId('block-1₍2₎_loop3')).toBe('block-1')
expect(SubflowNodeIdCodec.normalizeLookupId('block-1')).toBe('block-1')
})

it('extracts the leading branch suffix and loop digest segments', () => {
expect(SubflowNodeIdCodec.extractBranchSuffix('block-1₍2₎_loop3')).toBe('₍2₎')
expect(SubflowNodeIdCodec.extractBranchSuffix('block-1')).toBe('')
expect(SubflowNodeIdCodec.extractLoopSuffix('block-1₍2₎_loop3')).toBe('_loop3')
expect(SubflowNodeIdCodec.extractLoopSuffix('block-1')).toBe('')
})
})

describe('findEffectiveContainerId', () => {
it('returns the original ID for branch 0 / missing scope', () => {
const map = new Map<string, unknown>([['loop-1', {}]])
expect(SubflowNodeIdCodec.findEffectiveContainerId('loop-1', 'block-1', map)).toBe('loop-1')
})

it('prefers the mapped cloned scope when present', () => {
const map = new Map<string, unknown>([
['loop-1', {}],
['loop-1__obranch-2', {}],
])
expect(SubflowNodeIdCodec.findEffectiveContainerId('loop-1', 'block-1', map, 2)).toBe(
'loop-1__obranch-2'
)
})

it('resolves the cloned scope from the current node ID suffix', () => {
const map = new Map<string, unknown>([
['loop-1', {}],
['loop-1__obranch-3', {}],
])
expect(SubflowNodeIdCodec.findEffectiveContainerId('loop-1', 'block-1__obranch-3', map)).toBe(
'loop-1__obranch-3'
)
})

it('prefers __clone scopes when the current node carries a clone marker', () => {
const map = new Map<string, unknown>([
['loop-1__obranch-2', {}],
['loop-1__cloneabc__obranch-2', {}],
])
expect(
SubflowNodeIdCodec.findEffectiveContainerId('loop-1', 'block-1__cloneabc__obranch-2', map)
).toBe('loop-1__cloneabc__obranch-2')
})
})

describe('round-trip parse ∘ build', () => {
it('builds then parses branch IDs symmetrically', () => {
for (const index of [0, 1, 7, 20]) {
const id = SubflowNodeIdCodec.buildBranchNodeId('base-id', index)
expect(SubflowNodeIdCodec.extractBranchIndex(id)).toBe(index)
expect(SubflowNodeIdCodec.extractBaseBlockId(id)).toBe('base-id')
}
})

it('builds then parses outer-branch scoped IDs symmetrically', () => {
for (const index of [1, 4, 19]) {
const id = SubflowNodeIdCodec.buildOuterBranchScopedId('base-id', index)
expect(SubflowNodeIdCodec.extractOuterBranchIndex(id)).toBe(index)
expect(SubflowNodeIdCodec.stripOuterBranchSuffix(id)).toBe('base-id')
}
})
})
})
Loading
Loading