Skip to content
Open
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
24 changes: 24 additions & 0 deletions codex/hooks/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,30 @@ def configure_codex_hooks() -> bool:
}
]
}
],
"SubagentStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": hook_command,
"timeout": 60
}
]
}
],
"SubagentStop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": hook_command,
"timeout": 60
}
]
}
]
}

Expand Down
33 changes: 28 additions & 5 deletions codex/hooks/unbound.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,28 @@ def process_stop_event(event: Dict, api_key: str):
# Parse tool uses from Codex transcript (function_call/function_call_output pairs)
assistant_tool_uses = parse_codex_transcript_for_tools(transcript_path, user_prompt_timestamp)

# Fold in subagent invocations from SubagentStop hook events. Codex subagents
# run in their own transcript and don't appear as function_calls in the parent
# rollout, so we recover them from the accumulated hook events (same session,
# after the user prompt) and forward them as Task tool_uses for the gateway.
for log in logs:
log_session_id = log.get('session_id') or log.get('event', {}).get('session_id')
if log_session_id != session_id:
continue
log_event = log.get('event', {}) if 'event' in log else log
if log_event.get('hook_event_name') != 'SubagentStop':
continue
if user_prompt_timestamp and log.get('timestamp') and log.get('timestamp') <= user_prompt_timestamp:
continue
assistant_tool_uses.append({
'type': 'SubagentStop',
'tool_name': 'Task',
'tool_input': {'subagent_type': log_event.get('agent_type')},
'tool_response': {
'last_assistant_message': log_event.get('last_assistant_message'),
},
})
Comment on lines +1086 to +1106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing metrics for new subagent flow

Same observability gap as in cursor/unbound.py: the new SubagentStop scanning loop has no Prometheus counters or histograms. Suggested additions: subagent_stop_events_forwarded_total (labelled subagent_type) to track which subagent types are active, and a subagent_stop_events_skipped_total counter when the timestamp filter drops an event. Without these, there is no signal to distinguish "subagents not running" from "subagent events being silently filtered out".

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +1086 to +1106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No structured logging on the new SubagentStop scanning path

The new loop that scans all logs for SubagentStop events has no log output. If the subagent_type or last_assistant_message fields are missing in a real event (e.g. log_event.get('agent_type') returns None), the resulting tool_use will be silently malformed and forwarded to the gateway with None values. A log line recording session_id, the number of events appended, and which agent_type values were found would allow failures to be diagnosed from logs without reproducing the environment.


assistant_msg = {
'role': 'assistant',
'content': last_assistant_message or ''
Expand Down Expand Up @@ -1427,13 +1449,13 @@ def main():
input_data = sys.stdin.read().strip()

if not input_data:
print('{"suppressOutput": true}', flush=True)
print("{}", flush=True)
return

try:
event = json.loads(input_data)
except json.JSONDecodeError:
print('{"suppressOutput": true}', flush=True)
print("{}", flush=True)
return

hook_event_name = event.get('hook_event_name')
Expand Down Expand Up @@ -1465,7 +1487,8 @@ def main():
'session_id': event.get('session_id'),
'event': event
})
response["suppressOutput"] = True
# Codex does not support suppressOutput — emit the plain response.
response.pop("suppressOutput", None)
print(json.dumps(response), flush=True)
return

Expand All @@ -1485,12 +1508,12 @@ def main():

cleanup_old_logs()

print('{"suppressOutput": true}', flush=True)
print("{}", flush=True)

except Exception as e:
# Still return empty JSON object to Codex to indicate completion
log_error(f"Exception in main: {str(e)}", 'general')
print('{"suppressOutput": true}', flush=True)
print("{}", flush=True)


if __name__ == '__main__':
Expand Down
10 changes: 10 additions & 0 deletions cursor/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@
"command": "./hooks/unbound.py"
}
],
"subagentStart": [
{
"command": "./hooks/unbound.py"
}
],
"subagentStop": [
{
"command": "./hooks/unbound.py"
}
],
"stop": [
{
"command": "./hooks/unbound.py"
Expand Down
35 changes: 30 additions & 5 deletions cursor/unbound.py
Original file line number Diff line number Diff line change
Expand Up @@ -1003,12 +1003,28 @@ def build_llm_exchange(events, api_key=None):

elif hook_event_name == 'afterAgentResponse':
assistant_response = event.get('text')


elif hook_event_name == 'subagentStop':
assistant_tool_uses.append({
'type': 'subagentStop',
'tool_name': 'Task',
'tool_input': {
'subagent_type': event.get('subagent_type'),
'task': event.get('task'),
},
'tool_response': {
'status': event.get('status'),
'summary': event.get('summary'),
'duration_ms': event.get('duration_ms'),
'tool_call_count': event.get('tool_call_count'),
},
})

if user_prompt:
messages.append({'role': 'user', 'content': user_prompt})
if assistant_response:
assistant_msg = {'role': 'assistant', 'content': assistant_response}

if assistant_response or assistant_tool_uses:
assistant_msg = {'role': 'assistant', 'content': assistant_response or ''}
if assistant_tool_uses:
assistant_msg['tool_use'] = assistant_tool_uses
messages.append(assistant_msg)
Expand Down Expand Up @@ -1157,7 +1173,16 @@ def process_stop_event(generation_id, api_key=None):
)

if has_stop:
exchange = build_llm_exchange(events, api_key)
turn_start = min((l.get('timestamp', '') for l in events), default='')
subagent_events = [
log
for gen_events in generations.values()
for log in gen_events
if log.get('event', {}).get('hook_event_name')
in ('subagentStart', 'subagentStop')
Comment on lines +1181 to +1182

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 subagentStart events are included here but build_llm_exchange has no handler for them — they will silently fall through without being processed. If only completed invocations are needed, filter to subagentStop only.

Suggested change
if log.get('event', {}).get('hook_event_name')
in ('subagentStart', 'subagentStop')
if log.get('event', {}).get('hook_event_name')
in ('subagentStop',)

and (not turn_start or log.get('timestamp', '') >= turn_start)
]
Comment on lines +1176 to +1184

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No structured logging on the new subagent detection path

When subagent_events is populated (or empty), there is no log line recording what was found. If the gateway ever receives an exchange with missing or wrong subagent tool_uses, there is no way to reconstruct whether: (a) no subagent events existed in the log at all, (b) the timestamp filter dropped them, or (c) they were from a different generation. A single structured log at debug/info level—recording generation_id, len(subagent_events), and turn_start—would make this path debuggable from logs alone.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

exchange = build_llm_exchange(events + subagent_events, api_key)
Comment on lines +1177 to +1185

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing metrics for new subagent flow

The new subagent detection and forwarding path has zero instrumentation. Per the observability requirements, every new flow must ship a latency histogram and success/failure counters at minimum. For this specific flow, useful metrics would be: a counter for subagent_events_forwarded_total labelled by subagent_type, and a counter for subagent_events_skipped_total (e.g. filtered by timestamp) so failures in this logic are visible in production without needing to dig through logs. Without them, there is no way to tell how often subagent events are being found and forwarded, or whether the timestamp filter is silently dropping valid events.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

if exchange:
send_to_api(exchange, api_key)
break
Expand Down