Class: Ask::CodingHarness::DemoAdapter

Inherits:
Ask::CodingProviders::Adapter
  • Object
show all
Defined in:
lib/ask/coding_harness/demo_adapter.rb

Overview

A scripted coding agent for trying the harness without API keys.

Walks through a realistic turn — todos, tool calls with output, a diff, and a tool call queued for human approval — waiting on the approval queue exactly like the real ask_agent adapter. The text and file contents are generated from the workspace it is pointed at, so every run feels like the real thing.

ach demo            # serve the web UI against the demo agent

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(approval: :require) ⇒ DemoAdapter

Returns a new instance of DemoAdapter.

Parameters:

  • approval (Symbol) (defaults to: :require)

    :require waits for human approval on the queued tool call; :off/:auto approves it immediately (headless).



23
24
25
26
27
28
29
30
31
# File 'lib/ask/coding_harness/demo_adapter.rb', line 23

def initialize(approval: :require, **)
  @approval = approval.to_sym
  @pending = {}
  @next_action_id = 1
  @mutex = Mutex.new
  @cv = ConditionVariable.new
  @aborted = false
  @sessions = {}
end

Class Method Details

.from_config(**config) ⇒ Object



33
34
35
# File 'lib/ask/coding_harness/demo_adapter.rb', line 33

def self.from_config(**config)
  new(**config)
end

.install!Object

── Registration ──



183
184
185
186
187
188
189
190
# File 'lib/ask/coding_harness/demo_adapter.rb', line 183

def self.install!
  existing = begin
    Ask::CodingProviders.resolve_adapter(:demo)
  rescue Ask::CodingProviders::ConfigurationError
    nil
  end
  Ask::CodingProviders.register_adapter(:demo, self) unless existing
end

Instance Method Details

#abort(_session_id) ⇒ Object



177
178
179
# File 'lib/ask/coding_harness/demo_adapter.rb', line 177

def abort(_session_id)
  @mutex.synchronize { @aborted = true; @cv.broadcast }
end

#approve_action(_session_id, action_id) ⇒ Object

── Approval controls (same duck-typed surface as ask_agent) ──



146
147
148
149
150
151
152
153
# File 'lib/ask/coding_harness/demo_adapter.rb', line 146

def approve_action(_session_id, action_id)
  @mutex.synchronize do
    action = @pending[action_id]
    next [] unless action
    @pending[action_id] = action.merge("status" => "approved")
    @cv.broadcast
  end
end

#approve_all(_session_id) ⇒ Object



164
165
166
167
168
169
# File 'lib/ask/coding_harness/demo_adapter.rb', line 164

def approve_all(_session_id)
  @mutex.synchronize do
    @pending.each_value { |a| a["status"] = "approved" if a["status"] == "pending" }
    @cv.broadcast
  end
end

#create_session(workspace_path, mode: nil, model: nil, system_prompt: nil) ⇒ Object



42
43
44
45
46
# File 'lib/ask/coding_harness/demo_adapter.rb', line 42

def create_session(workspace_path, mode: nil, model: nil, system_prompt: nil, **)
  sid = "demo_#{@sessions.size + 1}"
  @sessions[sid] = workspace_path
  sid
end

#pending_approvals(_session_id) ⇒ Object



171
172
173
174
175
# File 'lib/ask/coding_harness/demo_adapter.rb', line 171

def pending_approvals(_session_id)
  @mutex.synchronize do
    @pending.values.select { |a| a["status"] == "pending" }
  end
end

#reject_action(_session_id, action_id) ⇒ Object



155
156
157
158
159
160
161
162
# File 'lib/ask/coding_harness/demo_adapter.rb', line 155

def reject_action(_session_id, action_id)
  @mutex.synchronize do
    action = @pending[action_id]
    next [] unless action
    @pending[action_id] = action.merge("status" => "rejected")
    @cv.broadcast
  end
end

#send_and_stream(session_id, content, turn_timeout: 600.0, &block) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/ask/coding_harness/demo_adapter.rb', line 48

def send_and_stream(session_id, content, turn_timeout: 600.0, &block)
  workspace = @sessions[session_id] || Dir.pwd
  emit(block, "turn.started", {})

  emit(block, "model.streaming", { "delta" => "Alright, let me take a look at #{File.basename(workspace)}. " })
  emit(block, "model.streaming", { "delta" => "I'll start by mapping the project structure.\n\n" })

  emit(block, "todos.updated", {
    "todos" => [
      { "id" => "1", "title" => "Inspect the project structure", "status" => "in_progress" },
      { "id" => "2", "title" => "Find the main entry point", "status" => "pending" },
      { "id" => "3", "title" => "Propose improvements", "status" => "pending" }
    ]
  })

  files = list_files(workspace)
  emit(block, "tool.use", {
    "toolName" => "bash", "toolCallId" => "call_1",
    "input" => { "command" => "find . -type f | head -30" }
  })
  emit(block, "tool.result", {
    "toolName" => "bash", "toolCallId" => "call_1",
    "output" => files.join("\n"),
    "isError" => false, "durationMs" => 84
  })

  readme = read_first(workspace, %w[README.md readme.md])
  if readme
    emit(block, "tool.use", {
      "toolName" => "read", "toolCallId" => "call_2",
      "input" => { "path" => readme[0] }
    })
    emit(block, "tool.result", {
      "toolName" => "read", "toolCallId" => "call_2",
      "output" => readme[1],
      "isError" => false, "durationMs" => 12
    })
  end

  emit(block, "todos.updated", {
    "todos" => [
      { "id" => "1", "title" => "Inspect the project structure", "status" => "completed" },
      { "id" => "2", "title" => "Find the main entry point", "status" => "completed" },
      { "id" => "3", "title" => "Propose improvements", "status" => "in_progress" }
    ]
  })

  emit(block, "model.streaming", {
    "delta" => "I can see the project has #{files.size} files. Here's a diff I'd like to apply — it needs your approval first:\n\n"
  })

  emit(block, "tool.use", {
    "toolName" => "apply_patch", "toolCallId" => "call_3",
    "input" => { "patch" => "diff --git a/README.md b/README.md\nindex 8f1a2b3..c9d4e5f 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,3 +1,4 @@\n # #{File.basename(workspace)}\n \n+Generated by the ask-coding-harness demo agent.\n" }
  })

  # Queue an approval — the demo pauses here until the user decides
  # (or auto-approves in headless mode).
  action_id = queue_action(block, "apply_patch", { "patch" => "" }, "Applying a patch to README.md requires approval")
  outcome =
    if %i[off auto].include?(@approval)
      @mutex.synchronize do
        @pending[action_id] = @pending[action_id].merge("status" => "approved")
      end
      :approved
    else
      wait_for_decision(action_id)
    end

  case outcome
  when :approved
    emit(block, "approval.updated", { "actionId" => action_id, "status" => "approved" })
    emit(block, "tool.result", {
      "toolName" => "apply_patch", "toolCallId" => "call_3",
      "output" => "Patch applied to README.md",
      "isError" => false, "durationMs" => 5
    })
    emit(block, "model.streaming", {
      "delta" => "Done — README.md now documents that this workspace was demoed by the harness. " \
                "Try asking me to do something real: point the harness at a project, add an API key, " \
                "and I'll work through it with tools, approvals, and diffs just like this.\n\n" \
                "Things to try: run tests, refactor a file, explain an error.\n"
    })
    emit(block, "turn.completed", { "response" => "Demo turn completed." })
  when :rejected
    emit(block, "approval.updated", { "actionId" => action_id, "status" => "rejected" })
    emit(block, "model.streaming", {
      "delta" => "Understood — I won't touch anything. That's the approval flow working: " \
                "nothing mutates without your say-so.\n"
    })
    emit(block, "turn.completed", { "response" => "Demo turn completed (rejected)." })
  when :aborted
    emit(block, "turn.aborted", {})
  end
end

#startObject



37
# File 'lib/ask/coding_harness/demo_adapter.rb', line 37

def start; end

#stopObject



38
39
40
# File 'lib/ask/coding_harness/demo_adapter.rb', line 38

def stop
  @mutex.synchronize { @aborted = true; @cv.broadcast }
end