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
60 changes: 59 additions & 1 deletion dag/extdeps/llm/claude_code_stream_json.dag
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ data extdeps_model_scope: ExternalModelScope = ExternalModelScope {
}

data claude_code_stream_event_type_result: String = "result"
data claude_code_stream_event_type_system: String = "system"
data claude_code_stream_event_type_assistant: String = "assistant"
data claude_code_stream_event_type_user: String = "user"
data claude_code_content_block_type_tool_use: String = "tool_use"
data claude_code_content_block_type_tool_result: String = "tool_result"
data claude_code_rate_limit_status_rejected: String = "rejected"

// The argv words that make the CLI write this wire. --verbose is required by the CLI for
Expand Down Expand Up @@ -112,6 +117,9 @@ type ClaudeCodeStreamLineGap
type ClaudeCodeStreamLine
= ClaudeCodeRateLimitEventLine { info: ClaudeCodeRateLimitInfo }
| ClaudeCodeResultLine { result: ClaudeCodeTurnResult }
| ClaudeCodeSystemLine
| ClaudeCodeAssistantLine { has_tool_use: Bool }
| ClaudeCodeUserLine { has_tool_result: Bool }
| ClaudeCodeOtherLine { event_type: String }
| ClaudeCodeLineUnreadable { gap: ClaudeCodeStreamLineGap }

Expand Down Expand Up @@ -425,6 +433,40 @@ fn claude_result_line(doc: JsonValue) -> ClaudeCodeStreamLine {
}
}

fn claude_content_blocks_have_type(content: JsonValue, block_type: String) -> Bool {
match content {
JsonArray { elements } =>
any(elements, e =>
match claude_string_member(v: e, key: "type") {
StringMemberRead { value: s } => s == block_type
StringMemberNull => false
StringMemberAbsent => false
StringMemberMalformed => false
}
)
JsonNull => false
JsonBool { value: _ } => false
JsonNumber { lexeme: _ } => false
JsonString { value: _ } => false
JsonObject { members: _ } => false
}
}

fn claude_message_content_has_type(doc: JsonValue, block_type: String) -> Bool {
match json_object_unique_member(v: doc, key: "message") {
JsonMemberFound { value: msg } =>
match json_object_unique_member(v: msg, key: "content") {
JsonMemberFound { value: content } => claude_content_blocks_have_type(content: content, block_type: block_type)
JsonMemberAbsent => false
JsonMemberDuplicated { count: _ } => false
JsonMemberNotAnObject => false
}
JsonMemberAbsent => false
JsonMemberDuplicated { count: _ } => false
JsonMemberNotAnObject => false
}
}

fn claude_code_stream_line(line: String) -> ClaudeCodeStreamLine {
match parse_json_document(s: line) {
JsonDocumentUnreadable { gap: _ } => ClaudeCodeLineUnreadable { gap: StreamLineNotJson }
Expand All @@ -442,7 +484,23 @@ fn claude_code_stream_line(line: String) -> ClaudeCodeStreamLine {
if t == claude_code_stream_event_type_result {
claude_result_line(doc: doc)
} else {
ClaudeCodeOtherLine { event_type: t }
if t == claude_code_stream_event_type_system {
ClaudeCodeSystemLine
} else {
if t == claude_code_stream_event_type_assistant {
ClaudeCodeAssistantLine {
has_tool_use: claude_message_content_has_type(doc: doc, block_type: claude_code_content_block_type_tool_use),
}
} else {
if t == claude_code_stream_event_type_user {
ClaudeCodeUserLine {
has_tool_result: claude_message_content_has_type(doc: doc, block_type: claude_code_content_block_type_tool_result),
}
} else {
ClaudeCodeOtherLine { event_type: t }
}
}
}
}
}
StringMemberNull => ClaudeCodeLineUnreadable { gap: StreamLineMemberMissing { member: "type" } }
Expand Down
58 changes: 57 additions & 1 deletion dag/extdeps/llm/cli.dag
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import extdeps.tools.env { env_path_resolved_program }
import std.algebra { trim }

import std.types { List, Int, Bool, NonEmptyStr, RenderedTerminalText, Unit }
import extdeps.llm.claude_code_stream_json { claude_code_stream_json_print_args }
import extdeps.external_authority { ExternalAuthority }
import extdeps.uri { Uri, Https }
import extdeps.languages.json.grammar {
Expand Down Expand Up @@ -201,6 +202,61 @@ fn claude_model_args(selection: ProviderModelSelection) -> List<String> {
}
}

// HEADLESS PRINT ARGV. Interactive `claude` (shape_claude_invoke_argv) is a session in a terminal:
// it takes --session-id, --settings, --effort and a node name. Automation is a different surface of
// the same binary: `claude -p --output-format stream-json --verbose` writes one JSON object per
// stdout line (extdeps.llm.claude_code_stream_json). The Agent SDK's query() spawns that same
// process; this row is that execution without a Node runtime. --verbose is required by the CLI for
// stream-json under -p. Turn bound is a coproduct so "no --max-turns" cannot be confused with a
// sentinel count. Permission flags and the start prompt stay caller-supplied: skip-permissions is
// gunbc policy (dispatch_claude_permission_args), not an upstream default.

type ClaudePrintTurnBound
= ClaudePrintUnboundedTurns
| ClaudePrintMaxTurns { turns: Int }

data claude_cli_max_turns_flag: String = "--max-turns"

data claude_cli_append_system_prompt_flag: String = "--append-system-prompt"

data claude_cli_effort_flag: String = "--effort"

fn claude_print_max_turns_args(bound: ClaudePrintTurnBound) -> List<String> {
match bound {
ClaudePrintUnboundedTurns => []
ClaudePrintMaxTurns { turns } => [claude_cli_max_turns_flag, to_string(turns)]
}
}

fn shape_claude_print_argv(
append_system_prompt: String,
start_prompt: String,
permission_args: List<String>,
model: ProviderModelSelection,
effort: ReasoningEffort,
turn_bound: ClaudePrintTurnBound,
) -> List<String> {
concat(
[claude_cli_program],
concat(
claude_code_stream_json_print_args,
concat(
claude_print_max_turns_args(bound: turn_bound),
concat(
[claude_cli_effort_flag, reasoning_effort_label(effort: effort)],
concat(
[claude_cli_append_system_prompt_flag, append_system_prompt],
concat(
claude_model_args(selection: model),
concat(permission_args, [start_prompt]),
),
),
),
),
),
)
}

fn codex_model_args(selection: ProviderModelSelection) -> List<String> {
match selection {
ProviderAccountDefaultModel => []
Expand All @@ -226,7 +282,7 @@ fn shape_claude_invoke_argv(
claude_model_args(selection: model),
[
"--settings", settings_json,
"--effort", reasoning_effort_label(effort: effort),
claude_cli_effort_flag, reasoning_effort_label(effort: effort),
"--session-id", session_id,
"-n", node_id,
"--append-system-prompt", append_system_prompt,
Expand Down
9 changes: 9 additions & 0 deletions dag/gunbc/claude_code_limit_standing.dag
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import extdeps.llm.claude_code_stream_json {
ClaudeCodeRateLimitEventLine,
ClaudeCodeResultLine,
ClaudeCodeOtherLine,
ClaudeCodeSystemLine,
ClaudeCodeAssistantLine,
ClaudeCodeUserLine,
ClaudeCodeLineUnreadable,
ClaudeCodeRateLimitInfo,
ClaudeCodeRateLimitWindow,
Expand Down Expand Up @@ -105,6 +108,12 @@ fn claude_code_trip_reading(stdout: String) -> ClaudeCodeTripReading {
ClaudeTripFold { index: acc.index + 1, reading: ClaudeCodeTripRead { result: Present { value: result }, rate_limit: l } }
ClaudeCodeOtherLine { event_type: _ } =>
ClaudeTripFold { index: acc.index + 1, reading: acc.reading }
ClaudeCodeSystemLine =>
ClaudeTripFold { index: acc.index + 1, reading: acc.reading }
ClaudeCodeAssistantLine { has_tool_use: _ } =>
ClaudeTripFold { index: acc.index + 1, reading: acc.reading }
ClaudeCodeUserLine { has_tool_result: _ } =>
ClaudeTripFold { index: acc.index + 1, reading: acc.reading }
ClaudeCodeLineUnreadable { gap } =>
ClaudeTripFold { index: acc.index + 1, reading: ClaudeCodeTripUnreadable { line_index: acc.index, gap: gap } }
}
Expand Down
Loading
Loading