* test: Bootstrapped sdk/ as TypeScript ESM package with full GSD-1 PLAN.… - "sdk/package.json" - "sdk/tsconfig.json" - "sdk/vitest.config.ts" - "sdk/src/types.ts" - "sdk/src/plan-parser.ts" - "sdk/src/plan-parser.test.ts" GSD-Task: S01/T01 * test: Implemented config reader and gsd-tools bridge with 25 unit tests… - "sdk/src/config.ts" - "sdk/src/config.test.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/gsd-tools.test.ts" GSD-Task: S01/T02 * test: Built prompt-builder, session-runner, and GSD class — 85 total un… - "sdk/src/prompt-builder.ts" - "sdk/src/prompt-builder.test.ts" - "sdk/src/session-runner.ts" - "sdk/src/index.ts" - "sdk/src/types.ts" GSD-Task: S01/T03 * test: Created E2E integration test with fixtures proving full SDK pipel… - "sdk/src/e2e.integration.test.ts" - "sdk/test-fixtures/sample-plan.md" - "sdk/test-fixtures/.planning/config.json" - "sdk/test-fixtures/.planning/STATE.md" - "vitest.config.ts" - "tsconfig.json" GSD-Task: S01/T04 * test: Added PhaseType/GSDEventType enums, 16-variant GSDEvent union, GS… - "sdk/src/types.ts" - "sdk/src/event-stream.ts" - "sdk/src/logger.ts" - "sdk/src/event-stream.test.ts" - "sdk/src/logger.test.ts" GSD-Task: S02/T01 * test: Built ContextEngine for phase-aware context file resolution, getT… - "sdk/src/context-engine.ts" - "sdk/src/tool-scoping.ts" - "sdk/src/phase-prompt.ts" - "sdk/src/context-engine.test.ts" - "sdk/src/tool-scoping.test.ts" - "sdk/src/phase-prompt.test.ts" GSD-Task: S02/T02 * test: Wired event stream into session runner, added onEvent()/addTransp… - "sdk/src/session-runner.ts" - "sdk/src/index.ts" - "sdk/src/e2e.integration.test.ts" GSD-Task: S02/T03 * feat: Added PhaseStepType enum, PhaseOpInfo interface, phase lifecycle… - "sdk/src/types.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/session-runner.ts" - "sdk/src/index.ts" - "sdk/src/phase-runner-types.test.ts" GSD-Task: S03/T01 * test: Implemented PhaseRunner state machine with 39 unit tests covering… - "sdk/src/phase-runner.ts" - "sdk/src/phase-runner.test.ts" GSD-Task: S03/T02 * test: Wired PhaseRunner into GSD.runPhase() public API with full re-exp… - "sdk/src/index.ts" - "sdk/src/phase-runner.integration.test.ts" - "sdk/src/phase-runner.ts" GSD-Task: S03/T03 * test: Expanded runVerifyStep with full gap closure cycle (plan → execut… - "sdk/src/types.ts" - "sdk/src/phase-runner.ts" - "sdk/src/phase-runner.test.ts" GSD-Task: S04/T02 * fix: Added 3 integration tests proving phasePlanIndex returns correct t… - "sdk/src/phase-runner.integration.test.ts" - "sdk/src/index.ts" GSD-Task: S04/T03 * test: Add milestone-level types, typed roadmapAnalyze(), GSD.run() orch… - "sdk/src/types.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/index.ts" - "sdk/src/milestone-runner.test.ts" GSD-Task: S05/T01 * test: Added CLITransport (structured stdout log lines) and WSTransport… - "sdk/src/cli-transport.ts" - "sdk/src/cli-transport.test.ts" - "sdk/src/ws-transport.ts" - "sdk/src/ws-transport.test.ts" - "sdk/src/index.ts" - "sdk/package.json" GSD-Task: S05/T02 * test: Added gsd-sdk CLI entry point with argument parsing, bin field, p… - "sdk/src/cli.ts" - "sdk/src/cli.test.ts" - "sdk/package.json" GSD-Task: S05/T03 * feat: Add InitNewProjectInfo type, initNewProject()/configSet() GSDTool… - "sdk/src/types.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/cli.ts" - "sdk/src/cli.test.ts" - "sdk/src/gsd-tools.test.ts" GSD-Task: S01/T01 * chore: Created InitRunner orchestrator with setup → config → PROJECT.md… - "sdk/src/init-runner.ts" - "sdk/src/types.ts" - "sdk/src/index.ts" GSD-Task: S01/T02 * test: Wired InitRunner into CLI main() for full gsd-sdk init dispatch a… - "sdk/src/cli.ts" - "sdk/src/init-runner.test.ts" - "sdk/src/cli.test.ts" GSD-Task: S01/T03 * test: Add PlanCheck step, AI self-discuss, and retryOnce wrapper to Pha… - "sdk/src/types.ts" - "sdk/src/phase-runner.ts" - "sdk/src/session-runner.ts" - "sdk/src/phase-runner.test.ts" - "sdk/src/phase-runner-types.test.ts" GSD-Task: S02/T01 * feat: Rewrite CLITransport with ANSI colors, phase banners, spawn indic… - "sdk/src/cli-transport.ts" - "sdk/src/cli-transport.test.ts" GSD-Task: S02/T02 * test: Add `gsd-sdk auto` command with autoMode config override, USAGE t… - "sdk/src/cli.ts" - "sdk/src/cli.test.ts" - "sdk/src/index.ts" - "sdk/src/types.ts" GSD-Task: S02/T03 * fix: CLI shebang + gsd-tools non-JSON output handling Three bugs found during first real gsd-sdk run: 1. cli.ts shebang was commented out — shell executed JS as bash, triggering ImageMagick's import command instead of Node 2. configSet() called exec() which JSON.parse()d the output, but gsd-tools config-set returns 'key=value' text, not JSON. Added execRaw() method for commands that return plain text. 3. Same JSON parse bug affected commit() (returns git SHA), stateLoad(), verifySummary(), initExecutePhase(), stateBeginPhase(), and phaseComplete(). All switched to execRaw(). Tests updated to match real gsd-tools output format (plain text instead of mocked JSON). 376/376 tests pass.
300 lines
9.4 KiB
TypeScript
300 lines
9.4 KiB
TypeScript
/**
|
|
* Session runner — orchestrates Agent SDK query() calls for plan execution.
|
|
*
|
|
* Takes a parsed plan, builds the executor prompt, configures query() options,
|
|
* processes the message stream, and extracts results into a typed PlanResult.
|
|
*/
|
|
|
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
import type { SDKMessage, SDKResultMessage, SDKResultSuccess, SDKResultError } from '@anthropic-ai/claude-agent-sdk';
|
|
import type { ParsedPlan, PlanResult, SessionOptions, SessionUsage, GSDCostUpdateEvent, PhaseStepType } from './types.js';
|
|
import { GSDEventType, PhaseType } from './types.js';
|
|
import type { GSDConfig } from './config.js';
|
|
import { buildExecutorPrompt, parseAgentTools, DEFAULT_ALLOWED_TOOLS } from './prompt-builder.js';
|
|
import type { GSDEventStream, EventStreamContext } from './event-stream.js';
|
|
import { getToolsForPhase } from './tool-scoping.js';
|
|
|
|
// ─── Model resolution ────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Resolve model identifier from options or config profile.
|
|
*
|
|
* Priority: explicit model option > config model_profile > default.
|
|
*/
|
|
function resolveModel(options?: SessionOptions, config?: GSDConfig): string | undefined {
|
|
if (options?.model) return options.model;
|
|
|
|
// Map model_profile names to model IDs
|
|
if (config?.model_profile) {
|
|
const profileMap: Record<string, string> = {
|
|
balanced: 'claude-sonnet-4-6',
|
|
quality: 'claude-opus-4-6',
|
|
speed: 'claude-haiku-3-5',
|
|
};
|
|
return profileMap[config.model_profile] ?? config.model_profile;
|
|
}
|
|
|
|
return undefined; // Let SDK use its default
|
|
}
|
|
|
|
// ─── Session runner ──────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Run a plan execution session via the Agent SDK query() function.
|
|
*
|
|
* Builds the executor prompt from the parsed plan, configures query() with
|
|
* appropriate permissions, tool restrictions, and budget limits, then iterates
|
|
* the message stream to extract the result.
|
|
*
|
|
* @param plan - Parsed plan structure
|
|
* @param config - GSD project configuration
|
|
* @param options - Session overrides (maxTurns, budget, model, etc.)
|
|
* @param agentDef - Raw agent definition content (optional, for tool/role extraction)
|
|
* @returns Typed PlanResult with cost, duration, success/error status
|
|
*/
|
|
export async function runPlanSession(
|
|
plan: ParsedPlan,
|
|
config: GSDConfig,
|
|
options?: SessionOptions,
|
|
agentDef?: string,
|
|
eventStream?: GSDEventStream,
|
|
streamContext?: EventStreamContext,
|
|
): Promise<PlanResult> {
|
|
// Build the executor prompt
|
|
const executorPrompt = buildExecutorPrompt(plan, agentDef);
|
|
|
|
// Resolve allowed tools — from agent definition or defaults
|
|
const allowedTools = options?.allowedTools ??
|
|
(agentDef ? parseAgentTools(agentDef) : DEFAULT_ALLOWED_TOOLS);
|
|
|
|
// Resolve model
|
|
const model = resolveModel(options, config);
|
|
|
|
// Configure query options
|
|
const maxTurns = options?.maxTurns ?? 50;
|
|
const maxBudgetUsd = options?.maxBudgetUsd ?? 5.0;
|
|
const cwd = options?.cwd ?? process.cwd();
|
|
|
|
const queryStream = query({
|
|
prompt: `Execute this plan:\n\n${plan.objective || 'Execute the plan tasks below.'}`,
|
|
options: {
|
|
systemPrompt: {
|
|
type: 'preset',
|
|
preset: 'claude_code',
|
|
append: executorPrompt,
|
|
},
|
|
settingSources: ['project'],
|
|
allowedTools,
|
|
permissionMode: 'bypassPermissions',
|
|
allowDangerouslySkipPermissions: true,
|
|
maxTurns,
|
|
maxBudgetUsd,
|
|
cwd,
|
|
...(model ? { model } : {}),
|
|
},
|
|
});
|
|
|
|
return processQueryStream(queryStream, eventStream, streamContext);
|
|
}
|
|
|
|
// ─── Result extraction ───────────────────────────────────────────────────────
|
|
|
|
function isResultMessage(msg: SDKMessage): msg is SDKResultMessage {
|
|
return msg.type === 'result';
|
|
}
|
|
|
|
function isSuccessResult(msg: SDKResultMessage): msg is SDKResultSuccess {
|
|
return msg.subtype === 'success';
|
|
}
|
|
|
|
function isErrorResult(msg: SDKResultMessage): msg is SDKResultError {
|
|
return msg.subtype !== 'success';
|
|
}
|
|
|
|
function emptyUsage(): SessionUsage {
|
|
return {
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheReadInputTokens: 0,
|
|
cacheCreationInputTokens: 0,
|
|
};
|
|
}
|
|
|
|
function extractUsage(msg: SDKResultMessage): SessionUsage {
|
|
const u = msg.usage;
|
|
return {
|
|
inputTokens: u.input_tokens ?? 0,
|
|
outputTokens: u.output_tokens ?? 0,
|
|
cacheReadInputTokens: u.cache_read_input_tokens ?? 0,
|
|
cacheCreationInputTokens: u.cache_creation_input_tokens ?? 0,
|
|
};
|
|
}
|
|
|
|
function extractResult(msg: SDKResultMessage): PlanResult {
|
|
const base = {
|
|
sessionId: msg.session_id,
|
|
totalCostUsd: msg.total_cost_usd,
|
|
durationMs: msg.duration_ms,
|
|
usage: extractUsage(msg),
|
|
numTurns: msg.num_turns,
|
|
};
|
|
|
|
if (isSuccessResult(msg)) {
|
|
return {
|
|
...base,
|
|
success: true,
|
|
};
|
|
}
|
|
|
|
// Error result
|
|
const errorMsg = msg as SDKResultError;
|
|
return {
|
|
...base,
|
|
success: false,
|
|
error: {
|
|
subtype: errorMsg.subtype,
|
|
messages: errorMsg.errors ?? [],
|
|
},
|
|
};
|
|
}
|
|
|
|
// ─── Shared stream processing ────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Process a query() message stream, emit events, and extract the result.
|
|
* Shared between runPlanSession and runPhaseStepSession to avoid duplication.
|
|
*/
|
|
async function processQueryStream(
|
|
queryStream: AsyncIterable<SDKMessage>,
|
|
eventStream?: GSDEventStream,
|
|
streamContext?: EventStreamContext,
|
|
): Promise<PlanResult> {
|
|
let resultMessage: SDKResultMessage | undefined;
|
|
|
|
try {
|
|
for await (const message of queryStream) {
|
|
if (eventStream) {
|
|
eventStream.mapAndEmit(message, streamContext ?? {});
|
|
}
|
|
if (isResultMessage(message)) {
|
|
resultMessage = message;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
sessionId: '',
|
|
totalCostUsd: 0,
|
|
durationMs: 0,
|
|
usage: emptyUsage(),
|
|
numTurns: 0,
|
|
error: {
|
|
subtype: 'error_during_execution',
|
|
messages: [err instanceof Error ? err.message : String(err)],
|
|
},
|
|
};
|
|
}
|
|
|
|
if (!resultMessage) {
|
|
return {
|
|
success: false,
|
|
sessionId: '',
|
|
totalCostUsd: 0,
|
|
durationMs: 0,
|
|
usage: emptyUsage(),
|
|
numTurns: 0,
|
|
error: {
|
|
subtype: 'error_during_execution',
|
|
messages: ['No result message received from query stream'],
|
|
},
|
|
};
|
|
}
|
|
|
|
const result = extractResult(resultMessage);
|
|
|
|
if (eventStream) {
|
|
const cost = eventStream.getCost();
|
|
eventStream.emitEvent({
|
|
type: GSDEventType.CostUpdate,
|
|
timestamp: new Date().toISOString(),
|
|
sessionId: resultMessage.session_id,
|
|
phase: streamContext?.phase,
|
|
planName: streamContext?.planName,
|
|
sessionCostUsd: result.totalCostUsd,
|
|
cumulativeCostUsd: cost.cumulative,
|
|
} as GSDCostUpdateEvent);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// ─── Phase step session runner ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Map PhaseStepType to PhaseType for tool scoping.
|
|
* PhaseStepType includes 'advance' which has no session-level equivalent.
|
|
*/
|
|
function stepTypeToPhaseType(step: PhaseStepType): PhaseType {
|
|
const mapping: Record<string, PhaseType> = {
|
|
discuss: PhaseType.Discuss,
|
|
research: PhaseType.Research,
|
|
plan: PhaseType.Plan,
|
|
plan_check: PhaseType.Verify,
|
|
execute: PhaseType.Execute,
|
|
verify: PhaseType.Verify,
|
|
};
|
|
return mapping[step] ?? PhaseType.Execute;
|
|
}
|
|
|
|
/**
|
|
* Run a phase step session via the Agent SDK query() function.
|
|
*
|
|
* Unlike runPlanSession which takes a ParsedPlan, this accepts a raw prompt
|
|
* string and a phase step type. The prompt becomes the system prompt append,
|
|
* and tools are scoped by phase type.
|
|
*
|
|
* @param prompt - Raw prompt string to append to the system prompt
|
|
* @param phaseStep - Phase step type (determines tool scoping)
|
|
* @param config - GSD project configuration
|
|
* @param options - Session overrides (maxTurns, budget, model, etc.)
|
|
* @param eventStream - Optional event stream for observability
|
|
* @param streamContext - Optional context for event tagging
|
|
* @returns Typed PlanResult with cost, duration, success/error status
|
|
*/
|
|
export async function runPhaseStepSession(
|
|
prompt: string,
|
|
phaseStep: PhaseStepType,
|
|
config: GSDConfig,
|
|
options?: SessionOptions,
|
|
eventStream?: GSDEventStream,
|
|
streamContext?: EventStreamContext,
|
|
): Promise<PlanResult> {
|
|
const phaseType = stepTypeToPhaseType(phaseStep);
|
|
const allowedTools = options?.allowedTools ?? getToolsForPhase(phaseType);
|
|
const model = resolveModel(options, config);
|
|
const maxTurns = options?.maxTurns ?? 50;
|
|
const maxBudgetUsd = options?.maxBudgetUsd ?? 5.0;
|
|
const cwd = options?.cwd ?? process.cwd();
|
|
|
|
const queryStream = query({
|
|
prompt: prompt,
|
|
options: {
|
|
systemPrompt: {
|
|
type: 'preset',
|
|
preset: 'claude_code',
|
|
append: prompt,
|
|
},
|
|
settingSources: ['project'],
|
|
allowedTools,
|
|
permissionMode: 'bypassPermissions',
|
|
allowDangerouslySkipPermissions: true,
|
|
maxTurns,
|
|
maxBudgetUsd,
|
|
cwd,
|
|
...(model ? { model } : {}),
|
|
},
|
|
});
|
|
|
|
return processQueryStream(queryStream, eventStream, streamContext);
|
|
}
|