build swagent swift cli and test it yourself until it works. use this development key for testing with openai API:
Endpoints
- Create/continue a response:
POST https://api.openai.com/v1/responses
Headers:Authorization: Bearer $OPENAI_API_KEY
,Content-Type: application/json
. (OpenAI Platform)
Core request fields
model
:"gpt-5-codex"
.instructions
: your system rules (string). Re‑send them on every turn.input
: string or an array of items (e.g., user message, function call outputs).store: true
if you’ll chain turns later withprevious_response_id
. (OpenAI Platform)
Tools (function calling)
-
Send tools as top‑level objects in
tools
with this shape:{ "type": "function", "name": "run_bash", "description": "Run a bash command and return stdout, stderr, exitCode.", "parameters": { "type": "object", "properties": { "command": { "type": "string" }, "cwd": { "type": "string" } }, "required": ["command"] } }
-
You can let the model choose with
"tool_choice": "auto"
. (OpenAI Platform)
Function‑call loop (no tool_outputs
param)
-
First call: model may return items of
type: "function_call"
inoutput
withcall_id
,name
, andarguments
(JSON string). -
Run the tool locally.
-
Continue the run by calling
POST /v1/responses
again with:-
previous_response_id
: the prior responseid
-
instructions
: the same system rules -
input
: an array of items, each{ "type": "function_call_output", "call_id": "<same id>", "output": "<stringified JSON like { stdout, stderr, exitCode }>" }
-
This is how you return tool results. Don’t send a top‑level tool_outputs
field. (OpenAI Platform)
Streaming (SSE)
-
Set
"stream": true
to get Server‑Sent Events while the model is thinking. You’ll receive events such as:response.created
(start)response.output_text.delta
(text chunks)response.function_call.delta
(incremental function args)response.completed
(final object, includesusage
) Handle errors viaresponse.error
. (OpenAI Platform)
Usage & token counters
- Use
usage.input_tokens
,usage.output_tokens
,usage.total_tokens
(snake_case) to print per‑turn stats. These arrive on the final response (orresponse.completed
in streaming). (OpenAI Platform)
Conversation state
- To continue a chat without resending past text, set
previous_response_id
and re‑send yourinstructions
. You may also pass prior output items explicitly if you need. (OpenAI Platform)
Progress signal taxonomy (what to show in the CLI)
- Before the first output token: “🧠 thinking…” (spinner) once you receive
response.created
. - While streaming text: live print each
response.output_text.delta
. - When the model starts a tool: “🔧 run_bash …” as soon as you see
response.function_call.delta
/ the finalfunction_call
item. - While executing the tool: “⏳ running command…” until you post the
function_call_output
and the model resumes. - On finalization: “✅ done” once
response.completed
arrives, then print the footer withusage
. (OpenAI Platform)
Project: swagent
Language/Tooling: Swift 6.2 with SwiftSetting.defaultIsolation(MainActor.self)
enabled via SPM; dependencies: swift-argument-parser
, apple/swift-configuration
; built‑in Swift Testing (Xcode 26); add swift-format and SwiftLint.
Model/API: OpenAI Responses API, model: gpt-5-codex
, streaming on. (OpenAI Platform)
Startup UX
-
Print 2–3 cheeky lines (random) and the masked API key (first 3 + last 4).
-
Examples:
- “🎩 I code therefore I am.”
- “⚡ One prompt. One shot. Make it count.”
- “🔧 Small diffs, big wins.”
- “🧪 If it compiles, we ship. Mostly.”
- “🐚 Bashful? I’m not.”
Flags
-v, --verbose
— extra logs (HTTP status, timings).--version
— print version.-p <prompt>
— one‑shot user interaction; internally the agent may loop via tools untilfinish
or it needs info.--yolo
— auto‑approve all shell commands (no interactive Y/n).--session <uuid>
— load a persisted session.
Commands
/new
or/clear
— reset conversation state./status
— show masked key, token totals this session, estimated remaining context./exit
— quit; print: “To resume this session, callswagent --session <uuid>
.”
System prompt (embed verbatim in instructions
every turn)
You are swagent, a coding agent for terminal workflows. Runtime: macOS 26 or later. Mission: Build, run, and refine code + shell workflows; verify your work. Behavior:
- Think step‑by‑step; prefer small diffs and working patches.
- When you propose commands, call
run_bash
to execute them; never ask the user to confirm (the CLI handles approvals).- If the runtime says yolo=true, treat commands as pre‑approved and run immediately.
- If yolo=false and a command is destructive/ambiguous, call
request_more_info(question)
once; otherwise, justrun_bash
.- When done, call
finish(summary)
with a concise summary + next steps.- Keep output terminal‑friendly and concise; never print secrets. Tools:
run_bash(command: string, cwd?: string)
→ returns{stdout, stderr, exitCode}
.request_more_info(question: string)
finish(summary: string)
Responses API rules:
- Use
model: gpt-5-codex
.- Re‑send these instructions every turn.
- Chain with
previous_response_id
.- Tools are top‑level
{ type:'function', name, description, parameters }
.- Tool calls arrive as
output
items oftype:'function_call'
with acall_id
. Return results by continuing withprevious_response_id
and sendinginput: [{ "type":"function_call_output", "call_id":"<same>", "output":"<stringified JSON>" }]
.- Read
usage.input_tokens
,usage.output_tokens
,usage.total_tokens
for per‑turn stats. [swagent runtime]yolo=true|false
•verbose=true|false
•session=<uuid>
•cwd=<path>
(OpenAI Platform)
Runtime header
Append the [swagent runtime]
block above to instructions
every turn (so the agent knows about yolo
, etc.). (OpenAI Platform)
Tooling & policies
- Bash tool: Implement
run_bash(command, cwd?)
viabash -lc
. By default, promptRun? [Y/n]
(Enter=Yes). With--yolo
, auto‑approve. Return{stdout, stderr, exitCode}
(JSON), but stringify it before sending asfunction_call_output
. - Ask‑for‑info tool:
request_more_info(question)
prints the question and waits for a one‑line user reply; forward that as the next turn’s user message (you can co‑send alongside tool outputs ininput
). - Finish tool:
finish(summary)
prints the summary and ends the current action (stay in REPL unless in-p
mode). - Self‑testing: After code changes, the agent must call
run_bash
to runswift build
(andswift test
if tests exist), and also self‑invoke the CLI (swift run swagent …
) to verify flags.
Streaming & progress
-
Always set
"stream": true
when calling/v1/responses
. Show:- Thinking spinner after
response.created
until firstresponse.output_text.delta
. - Live text streaming by writing each
delta
chunk immediately. - Tool call progress when you see a
function_call
(or its deltas): print the command preview; switch to “⏳ running…” while executing; resume streaming once you sendfunction_call_output
. - Footer on
response.completed
usingusage.*
and a monotonic timer. Event names and flow: see Responses streaming & Realtime guides. (OpenAI Platform)
- Thinking spinner after
Sessions
- Persist under
~/.swagent/<uuid>.json
via anactor
. - Save:
previous_response_id
, chain of response ids, per‑session token totals, timestamps. --session <uuid>
loads and continues from file.
Config
- Use
swift-configuration
to readOPENAI_API_KEY
from the environment; mask it assk‑abc…wxyz
on startup. (OpenAI Platform)
Testing, format, lint
- Use Swift Testing (built‑in with Xcode 26) for unit tests.
- Add
swift-format
+SwiftLint
targets/scripts.
Security
- Never echo secrets.
- Treat dangerous commands conservatively when
yolo=false
(userequest_more_info
).
Minimal JSON crib sheet (copy/paste)
Create (turn 1, with tools & streaming):
{
"model": "gpt-5-codex",
"instructions": "<SYSTEM PROMPT + [swagent runtime]>",
"input": "Create a Swift package and build it.",
"tools": [ { "type":"function","name":"run_bash","description":"Run bash","parameters":{
"type":"object","properties":{"command":{"type":"string"},"cwd":{"type":"string"}},
"required":["command"]
}}, { "type":"function","name":"request_more_info","parameters":{
"type":"object","properties":{"question":{"type":"string"}},"required":["question"]
}}, { "type":"function","name":"finish","parameters":{
"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]
}} ],
"tool_choice": "auto",
"store": true,
"stream": true
}
Continue (turn 2, return tool result):
{
"model": "gpt-5-codex",
"instructions": "<SYSTEM PROMPT + [swagent runtime]>",
"previous_response_id": "resp_123",
"input": [
{
"type": "function_call_output",
"call_id": "call_abc",
"output": "{\"stdout\":\"initialized…\",\"stderr\":\"\",\"exitCode\":0}"
}
],
"stream": true
}
Docs: Responses create, streaming events, migration guide (function_call_output), usage counters, conversation state. (OpenAI Platform)
Build
- SPM executable target; enable
SwiftSetting.defaultIsolation(MainActor.self)
inswiftSettings
. - Deps:
swift-argument-parser
,swift-configuration
. - Implement a single Responses call with
"stream": true
; streamresponse.output_text.delta
to stdout. - Startup prints 2–3 cheeky lines + masked key.
- Flags:
--version
,-v
.
Checks
swagent --version
→ prints version only.swagent -v "Ping"
→ shows cheeky lines, masked key, streams text live, then footer(in: X, out: Y, total: Z, 0m 00s)
fromusage
. Streaming/usage: see docs. (OpenAI Platform)- No key → clear single‑line error.
Build
- Interactive REPL; keep
-p
for one‑shot. - Maintain state via
previous_response_id
+store:true
. - Always re‑send
instructions
and attach a[swagent runtime]
header withyolo
,verbose
,session
,cwd
.
Checks
- Second user turn uses the first turn’s
previous_response_id
(verify in logs if-v
). /new
clears state (next call has noprevious_response_id
).- Streaming remains active in both REPL and
-p
. Chaining: see conversation state docs. (OpenAI Platform)
Build
-
Add two tools:
finish(summary: string)
request_more_info(question: string)
-
Implement the function‑call loop:
- Parse any
function_call
items. - For
request_more_info
, print the question and wait for input; continue by sending a user message item ininput
(you can send it alongside anyfunction_call_output
items). - For
finish
, print the summary and stop the action.
- Parse any
Checks
- Prompt: “Ask me one clarifying question, then summarize and finish.”
→ Model calls
request_more_info
→ collects answer → model callsfinish
→ summary printed + footer. - Confirm there’s no top‑level
tool_outputs
; onlyinput
items withtype:"function_call_output"
on continuations. (OpenAI Platform)
Build
-
Add
run_bash(command, cwd?)
:- Default approval:
Run? [Y/n]
(Enter=Yes). --yolo
: auto‑approve.- Execute via
bash -lc
; capture{stdout, stderr, exitCode}
; stringify as theoutput
field infunction_call_output
.
- Default approval:
-
System prompt and runtime header explicitly say: agent never asks for permission;
yolo=true
means pre‑approved. -
After code changes, agent must self‑test:
swift build
, optionalswift test
, thenswift run swagent …
.
Checks
swagent --yolo -p "Echo hello"
→ model callsrun_bash("echo hello")
immediately (no extra prompt), CLI runs, continuation sendsfunction_call_output
, finalizes with a reply + footer.swagent -p "Echo hello"
(non‑yolo) → agent still does not ask; CLI prompts Y/n; run completes.- Tool loop uses
previous_response_id
+input
items, streaming on. (OpenAI Platform)
Build
-
Persist sessions under
~/.swagent/<uuid>.json
using anactor
. -
/status
prints: masked key; per‑session token totals; estimated context left (model limit minus running total). -
On exit: “To resume this session, call
swagent --session <uuid>
.” -
Tests with Swift Testing for:
- Arg parsing (
-v
,--version
,-p
,--yolo
,--session
). - Session store save/load roundtrip (concurrent writes protected by actor).
- Tool approval logic (Y/n default vs
--yolo
).
- Arg parsing (
-
Add
swift-format
andSwiftLint
targets (make fmt
,make lint
,make check
).
Checks
- Two turns, then
/status
shows totals;/exit
persists a JSON containing the latestprevious_response_id
, cumulativeusage
, timestamps. --session <uuid>
resumes and continues chaining.make check
runs format, lint, and tests cleanly.
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-N \
-d '{
"model": "gpt-5-codex",
"instructions": "…system prompt…",
"input": "Say hello, slowly.",
"stream": true
}'
# Expect SSE events like: response.created, response.output_text.delta, response.completed
SSE event names and flow: Responses streaming docs (plus Realtime guide for event taxonomy). (OpenAI Platform)
References
- Responses API — create & tools. (OpenAI Platform)
- Streaming — SSE events for Responses. (OpenAI Platform)
- Conversation state —
previous_response_id
. (OpenAI Platform) - Migration guide —
function_call_output
items. (OpenAI Platform) - Usage counters (snake_case). (OpenAI Platform)
Want a tiny Swift snippet that shows parsing SSE lines and switching the UI between “🧠 thinking…”, streaming text, and tool execution?
link to my private chatgpt session: (just my own reminder) https://chatgpt.com/c/68db9e74-2c9c-832d-8e11-56248a5b9013