Skip to content
Draft
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Step Code comes with built-in StepPage publishing. Once your local page is ready

- **Token efficiency** — tuned alongside Step models to consume fewer tokens for the same task; Long\-horizon tasks are split across parallel subagents, each with its own isolated context, so redundant content never enters the main conversation\.

- **Jev skill routing** — optional [Jev suggestions](packages/coding-agent/docs/jev-skill-routing.md) help the coding model choose among installed skills before a task starts; manual skill commands and the complete catalog remain available.

- **Static site publishing** — the built\-in steppage plugin publishes a local directory to a shareable static URL in one command, with version management and rollback\.

- **Long\-running task delegation** — `/goal` hands an objective to Step Code, which works toward it autonomously, and `/cron` runs on a schedule; the status line shows a live timer for the active task\.
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Step Code 内置 StepPage 发布能力:本地页面构建完成后,只需一

- **token 效率**——与阶跃模型深度协同,同等任务消耗更少 token;长任务拆分为子代理并行执行,子代理持有独立上下文,冗余内容不进入主对话。

- **Jev skill 路由**——可选的 [Jev 建议](packages/coding-agent/docs/jev-skill-routing.md)在任务开始前辅助挑选已安装的 skill,帮助减少选错或漏选;保留手动调用和完整目录,默认关闭。

- **静态网站发布**——内置 steppage 插件,一条指令将本地目录发布为可分享的静态网址,并支持版本管理与回滚。

- **长任务托管**——`/goal` 将目标交由 Step Code 持续自主推进,`/cron` 按计划定时执行;状态行实时显示活跃任务计时。
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/bootstrap/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
createStepCronExtension,
createStepExtensionInline,
createStepGoalExtension,
createStepJevSkillRouterExtensionInline,
type InlineExtension,
type StepExtensionOptions,
type StepTelemetryReporter,
Expand Down Expand Up @@ -48,6 +49,7 @@ export function createStepExtensionFactories(deps: StepExtensionFactoryDeps): In
traceHeaderPolicy: deps.traceHeaderPolicy,
}),
createStepCapabilitiesExtensionInline({ telemetry: deps.telemetry }),
createStepJevSkillRouterExtensionInline(),
createStepCronExtension({ telemetry: deps.telemetry }),
createStepGoalExtension({ telemetry: deps.telemetry }),
...(deps.stepCodeProviderExtension ? [deps.stepCodeProviderExtension] : []),
Expand Down
20 changes: 20 additions & 0 deletions apps/cli/test/jev-skill-router-bootstrap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ExtensionAPI } from "@step-harness/coding-agent";
import { expect, it, vi } from "vitest";
import { createStepExtensionFactories } from "../src/bootstrap/extensions.ts";

it("mounts Jev skill routing in the Step CLI without enabling outbound calls by default", async () => {
vi.stubEnv("STEP_JEV_SKILL_ROUTING", "");
const extensions = createStepExtensionFactories({
telemetry: { track: vi.fn() },
traceHeaderPolicy: { allowedBaseUrls: [], highSensitivityFields: [] },
stepSettings: undefined,
feedbackIdentity: undefined,
permission: undefined,
stepCodeProviderExtension: undefined,
});
const router = extensions.find((extension) => extension.name === "Jev skill routing");
if (!router || typeof router === "function") throw new Error("Jev inline extension is missing");
const on = vi.fn();
await router.factory({ on } as unknown as ExtensionAPI);
expect(on).not.toHaveBeenCalled();
});
4 changes: 4 additions & 0 deletions packages/coding-agent/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
"title": "Skills",
"path": "skills.md"
},
{
"title": "Jev Skill Routing",
"path": "jev-skill-routing.md"
},
{
"title": "Prompt Templates",
"path": "prompt-templates.md"
Expand Down
3 changes: 3 additions & 0 deletions packages/coding-agent/docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Step reads the variables below. Configuration shared across launches belongs in
| `STEP_CODING_AGENT_DIR` | Override the agent directory; default is `~/.stepcode/agent` |
| `STEP_CODING_AGENT_SESSION_DIR` | Override session storage; `--session-dir` takes precedence |
| `STEP_API_KEY` | StepFun API credential |
| `STEP_JEV_SKILL_ROUTING` | Set to `1` to enable optional [Jev skill routing](jev-skill-routing.md); disabled by default |
| `TYPESAFE_API_KEY` | TypeSafe credential for Jev routing; requires `STEP_JEV_SKILL_ROUTING=1` |
| `JEV_API_KEY` | Fallback Jev credential when `TYPESAFE_API_KEY` is empty |
| `STEP_BASE_URL` | Override the StepFun API endpoint |
| `STEP_PROVIDER`, `STEP_MODEL` | Default provider and model selection |
| `VISUAL`, `EDITOR` | External editor fallback when `externalEditor` is unset |
Expand Down
6 changes: 3 additions & 3 deletions packages/coding-agent/docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1018,14 +1018,14 @@ Access to models, providers, and resolved authentication. `ctx.modelRegistry.get

### ctx.signal

The current agent abort signal, or `undefined` when no agent turn is active.
The current operation's abort signal, including prompt preparation, or `undefined` when idle.

Use this for abort-aware nested work started by extension handlers, for example:
- `fetch(..., { signal: ctx.signal })`
- model calls that accept `signal`
- file or process helpers that accept `AbortSignal`

`ctx.signal` is typically defined during active turn events such as `tool_call`, `tool_result`, `message_update`, and `turn_end`.
`ctx.signal` is defined during `before_agent_start` and active turn events such as `tool_call`, `tool_result`, `message_update`, and `turn_end`. Pass it to asynchronous preparation work so cancellation stops that work before the coding model starts. A cancelled `before_agent_start` skips remaining handlers and does not start the agent loop.
It is usually `undefined` in idle or non-turn contexts such as session events, extension commands, and shortcuts fired while step is idle.

```typescript
Expand All @@ -1043,7 +1043,7 @@ pi.on("tool_result", async (event, ctx) => {

### ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()

Control flow helpers. `ctx.isIdle()` is false while Step is processing an agent run, automatic retry, auto-compaction retry, or queued continuation.
Control flow helpers. `ctx.isIdle()` is false while Step is processing `before_agent_start` hooks, an agent run, automatic retry, auto-compaction retry, or queued continuation.

### ctx.shutdown()

Expand Down
75 changes: 75 additions & 0 deletions packages/coding-agent/docs/jev-skill-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Jev skill routing

Step Code can use [TypeSafe Jev](https://typesafe.ai/) to suggest an installed skill before the coding model starts a request. This is optional and disabled by default. It is intended to reduce missed or unnecessary skill loads when several skills are available.

## Enable

Install at least two [skills](skills.md), obtain a TypeSafe API key, and start Step Code with:

```bash
export TYPESAFE_API_KEY="<your-typesafe-api-key>"
export STEP_JEV_SKILL_ROUTING=1
step
```

For a single prompt:

```bash
STEP_JEV_SKILL_ROUTING=1 TYPESAFE_API_KEY="<your-typesafe-api-key>" \
step -p "Use a browser to check the checkout page"
```

Use `pnpm step` in place of `step` when running from source. `JEV_API_KEY` is accepted as a fallback when `TYPESAFE_API_KEY` is empty. A key alone does not enable routing. Unset `STEP_JEV_SKILL_ROUTING` or set it to `0` before starting Step Code to disable it.

Jev uses a separate TypeSafe credential and billing account. Step sign-in and the coding model selected in `/model` continue to work as usual. This extension does not increase a Step Plan allowance.

## What happens

1. Before a new text request starts, Jev ranks the names and descriptions of the eligible skills and checks whether any skill is needed.
2. If there is a match, Step reads a short excerpt from each of up to three shortlisted skills. A second Jev call checks their suitability.
3. A sufficiently confident result adds a small suggestion to the current system prompt. The coding model decides whether to read and follow the suggested skill.

The full skill catalog stays available, including skills Jev did not select. Explicit skill requests and project instructions take precedence. The suggestion grants no tool permissions and does not execute a skill. It is reset before the next request and is not recomputed on every tool call within a request.

Both API calls and excerpt reads share a 1,500 ms deadline and honor task cancellation. Cancelling or closing the session while routing is pending stops further Jev calls and prevents the coding model from starting. There are no retries. A timeout, API error, malformed answer, uncertain result, or no match leaves the original prompt in place so the task can continue.

Routing is skipped for:

- `/skill:name` commands, expanded skill commands, and `$name` mentions of an installed skill.
- Requests with images, empty requests, or requests over 12,000 characters.
- Sessions without a `read` or `read_file` tool.
- Catalogs with fewer than two or more than 254 eligible skills, or duplicate names.

Skills with `disable-model-invocation: true` are excluded from automatic routing; explicit `/skill:name` commands still work. Existing discovery and project-trust rules determine which skills are available. `/reload` refreshes the catalog used for the next request.

## Data sent to TypeSafe

Enabling this feature sends data to `https://api.typesafe.ai/v1/systemone`, using the `jev-latest` model:

| Request | Data |
| --- | --- |
| Ranking | Current user request, eligible skill names and descriptions |
| Verification | Current user request, shortlisted names and descriptions, and up to 700 characters from each of at most three skill bodies |

The excerpt reader skips YAML frontmatter and reads at most the first 16 KiB of a skill file. A frontmatter block that extends beyond that prefix yields an empty excerpt. The extension does not separately attach local file paths, the system prompt, project context files, conversation history, tool results, images, or Step credentials. Text already present in the user request or skill metadata/body is sent as described above, so use this feature only for content you can share with TypeSafe.

The endpoint is fixed, and HTTP redirects are rejected. The extension does not persist routing requests or responses.

## Efficiency and measurement

The potential benefit is better skill selection: avoiding an irrelevant full skill read or finding a useful skill earlier. Keeping the complete catalog preserves its existing system-prompt prefix, but does not remove its tokens. The changing recommendation can affect caching after that prefix; it does not guarantee a cache hit for the rest of the conversation. Jev also adds API calls, billable input, and latency.

TypeSafe's [skill-suggestion cookbook](https://docs.typesafe.ai/cookbooks/skill_suggestion.md) reports an evaluation using 182 Hermes skills and 488 constructed requests. On 315 requests with a matching skill, the rate of a wrong or missing first skill load fell from 16.8% to 7.3%; on 173 requests without a match, unnecessary loads fell from 9.8% to 4.0%. Those results used Jev 1.12 and Claude Haiku 4.5. They are evidence from that experiment, not measured Step Code results. This integration uses its own conservative confidence gates and the `jev-latest` alias.

As of September 22, 2026, TypeSafe lists Jev 1.13.0 input at $0.042 per million tokens and output as free. Consult the current [models and pricing](https://docs.typesafe.ai/models.md) before enabling it.

To measure the effect on your workflow, compare the same tasks, skill catalog, coding model, and initial repository state with routing disabled and enabled. Include both matching and no-match tasks, and repeat runs to account for model variation. TypeSafe currently reports its best accuracy in English, so evaluate Chinese and other languages separately when they are part of your workflow. Record:

- Task completion quality and whether the first skill read was appropriate.
- Unnecessary skill reads and coding-model input, output, and cache usage.
- Jev charges and total cost per completed task.
- Time to the first coding-model response and total task latency, including slow or unavailable Jev calls.

Step's `/session` statistics cover the coding session; this extension's TypeSafe usage is not added to those totals. Include TypeSafe account usage separately when comparing costs. The automated tests exercise the real request format with a fake transport and a scripted coding model; they do not measure live Jev accuracy, production latency, token savings, or user growth.

See the [TypeSafe API reference](https://docs.typesafe.ai/api.md) for the typed Choice and Noul response formats.
2 changes: 2 additions & 0 deletions packages/coding-agent/docs/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ For project-level Claude Code skills, add to `.stepcode/settings.json`:

This is progressive disclosure: only descriptions are always in context, full instructions load on-demand.

For catalogs with several skills, optional [Jev skill routing](jev-skill-routing.md) can suggest a relevant skill before the coding model starts. It is disabled by default, keeps the full catalog available, and respects explicit skill commands.

## Skill Commands

Skills register as `/skill:name` commands:
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/src/core/agent-session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,8 @@ export class AgentSessionRuntime implements AgentSessionRuntimeHost {
const session = this._session;
try {
if (!this.currentSessionDisposed) {
// Settle prompt hooks and model work before invalidating their context.
await session.abort();
await emitSessionShutdownEvent(session.extensionRunner, {
type: "session_shutdown",
reason: "quit",
Expand Down
89 changes: 54 additions & 35 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,7 @@ export class AgentSession {
*/
dispose(): void {
try {
this._agentRunAbortController?.abort();
this.abortRetry();
this.abortCompaction();
this.abortBranchSummary();
Expand Down Expand Up @@ -988,12 +989,12 @@ export class AgentSession {
return this.agent.state.thinkingLevel;
}

/** Whether the session is currently processing an agent run or post-run continuation. */
/** Whether the session is processing prompt hooks, an agent run, or post-run continuation. */
get isStreaming(): boolean {
return this._isAgentRunActive;
}

/** Whether the session has no active agent run, retry, auto-compaction, or queued continuation. */
/** Whether the session has no active prompt hooks, agent run, retry, or queued continuation. */
get isIdle(): boolean {
return !this._isAgentRunActive;
}
Expand Down Expand Up @@ -1203,11 +1204,16 @@ export class AgentSession {
// Prompting
// =========================================================================

private async _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void> {
private async _runAgentPrompt(
messages: AgentMessage | AgentMessage[],
beforeStart?: (signal: AbortSignal) => Promise<void>,
): Promise<void> {
const runAbortController = new AbortController();
this._agentRunAbortController = runAbortController;
this._isAgentRunActive = true;
try {
if (beforeStart) await beforeStart(runAbortController.signal);
if (runAbortController.signal.aborted) return;
await this.agent.prompt(messages);
while (
!runAbortController.signal.aborted &&
Expand Down Expand Up @@ -1276,6 +1282,7 @@ export class AgentSession {
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
const preflightResult = options?.preflightResult;
let messages: AgentMessage[] | undefined;
let beforeStart: ((signal: AbortSignal) => Promise<void>) | undefined;

try {
// Handle extension commands first (execute immediately, even during streaming)
Expand Down Expand Up @@ -1389,36 +1396,49 @@ export class AgentSession {
}
this._pendingNextTurnMessages = [];

// Emit before_agent_start extension event
const result = await this._extensionRunner.emitBeforeAgentStart(
expandedText,
currentImages,
this._baseSystemPrompt,
this._baseSystemPromptOptions,
);
// Add all custom messages from extensions
if (result?.messages) {
for (const msg of result.messages) {
messages.push({
role: "custom",
customType: msg.customType,
// Untyped extensions can pass null/missing content; normalize at ingestion.
content: msg.content ?? [],
display: msg.display,
details: msg.details,
timestamp: Date.now(),
});
const promptMessages = messages;
beforeStart = async (signal) => {
try {
// Emit before_agent_start extension event
const result = await this._extensionRunner.emitBeforeAgentStart(
expandedText,
currentImages,
this._baseSystemPrompt,
this._baseSystemPromptOptions,
);
if (signal.aborted) {
preflightResult?.(false);
return;
}
// Add all custom messages from extensions
if (result?.messages) {
for (const msg of result.messages) {
promptMessages.push({
role: "custom",
customType: msg.customType,
// Untyped extensions can pass null/missing content; normalize at ingestion.
content: msg.content ?? [],
display: msg.display,
details: msg.details,
timestamp: Date.now(),
});
}
}
// Apply extension-modified system prompt, or reset to base
if (result?.systemPrompt !== undefined) {
this._systemPromptOverride = result.systemPrompt;
this.agent.state.systemPrompt = result.systemPrompt;
} else {
// Ensure we're using the base prompt (in case previous turn had modifications)
this._systemPromptOverride = undefined;
this.agent.state.systemPrompt = this._baseSystemPrompt;
}
preflightResult?.(true);
} catch (error) {
preflightResult?.(false);
throw error;
}
}
// Apply extension-modified system prompt, or reset to base
if (result?.systemPrompt !== undefined) {
this._systemPromptOverride = result.systemPrompt;
this.agent.state.systemPrompt = result.systemPrompt;
} else {
// Ensure we're using the base prompt (in case previous turn had modifications)
this._systemPromptOverride = undefined;
this.agent.state.systemPrompt = this._baseSystemPrompt;
}
};
} catch (error) {
preflightResult?.(false);
throw error;
Expand All @@ -1428,8 +1448,7 @@ export class AgentSession {
return;
}

preflightResult?.(true);
await this._runAgentPrompt(messages);
await this._runAgentPrompt(messages, beforeStart);
}

/**
Expand Down Expand Up @@ -2753,7 +2772,7 @@ export class AgentSession {
getScopedModels: () => this._scopedModels,
isIdle: () => this.isIdle,
isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
getSignal: () => this.agent.signal,
getSignal: () => this.agent.signal ?? this._agentRunAbortController?.signal,
abort: () => {
this._abortCurrentRun();
// Hosts may restore queued input after the session has been cancelled.
Expand Down
5 changes: 5 additions & 0 deletions packages/coding-agent/src/core/extensions/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,8 @@ export class ExtensionRunner {
this.assertActive();
return currentSystemPrompt;
};
// Keep this operation's signal usable after disposal invalidates its context.
const signal = ctx.signal;
const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
let systemPromptModified = false;

Expand All @@ -1163,6 +1165,7 @@ export class ExtensionRunner {
if (!handlers || handlers.length === 0) continue;

for (const handler of handlers) {
if (signal?.aborted) return undefined;
try {
const event: BeforeAgentStartEvent = {
type: "before_agent_start",
Expand All @@ -1172,6 +1175,7 @@ export class ExtensionRunner {
systemPromptOptions,
};
const handlerResult = await handler(event, ctx);
if (signal?.aborted) return undefined;

if (handlerResult) {
const result = handlerResult as BeforeAgentStartEventResult;
Expand All @@ -1184,6 +1188,7 @@ export class ExtensionRunner {
}
}
} catch (err) {
if (signal?.aborted) return undefined;
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
this.emitError({
Expand Down
Loading
Loading