feat: add /goal command for durable multi-turn objectives
- /goal set|status|checkpoint|pause|resume|clear intercept in main.py - GOAL.md injected into system prompt when active (prompt_builder.py) - Goal context added to heartbeat for autonomous progress
This commit is contained in:
@@ -462,6 +462,131 @@ def aws_describe_service(service: str, region: str = "us-east-1") -> str:
|
||||
return f"Service {service} not yet implemented. Try: lambda, s3, cloudformation, dynamodb, sqs"
|
||||
|
||||
|
||||
# ── Goal helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
from datetime import datetime as _dt
|
||||
from zoneinfo import ZoneInfo as _ZoneInfo
|
||||
|
||||
def _now_iso() -> str:
|
||||
return _dt.now(_ZoneInfo('America/Chicago')).strftime('%Y-%m-%dT%H:%M:%S%z')
|
||||
|
||||
|
||||
def _read_goal() -> str | None:
|
||||
"""Read GOAL.md from S3, return content or None."""
|
||||
try:
|
||||
return ws_tools.read_file('GOAL.md')
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _write_goal(content: str):
|
||||
ws_tools.write_file('GOAL.md', content)
|
||||
invalidate_prompt()
|
||||
|
||||
|
||||
def _delete_goal():
|
||||
try:
|
||||
_s3 = boto3.client('s3')
|
||||
_s3.delete_object(Bucket=ws_tools.get_bucket(), Key='GOAL.md')
|
||||
ws_tools._cache.pop('GOAL.md', None)
|
||||
invalidate_prompt()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _parse_goal_status(content: str) -> str:
|
||||
"""Extract Status field from GOAL.md content."""
|
||||
for line in content.splitlines():
|
||||
if line.startswith('**Status:**'):
|
||||
return line.split('**Status:**')[1].strip()
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def _get_active_goal_context() -> dict | None:
|
||||
"""Return goal context dict if active, else None."""
|
||||
content = _read_goal()
|
||||
if not content or _parse_goal_status(content) != 'active':
|
||||
return None
|
||||
objective = stopping = last_cp = ''
|
||||
for line in content.splitlines():
|
||||
if line.startswith('**Objective:**'):
|
||||
objective = line.split('**Objective:**')[1].strip()
|
||||
elif line.startswith('**Stopping condition:**'):
|
||||
stopping = line.split('**Stopping condition:**')[1].strip()
|
||||
elif line.startswith('- ['):
|
||||
last_cp = line # last checkpoint line wins
|
||||
return {'objective': objective, 'stopping_condition': stopping, 'last_checkpoint': last_cp}
|
||||
|
||||
|
||||
def _handle_goal_command(prompt: str) -> str | None:
|
||||
"""Handle /goal commands. Returns reply string or None if not a goal command."""
|
||||
parts = prompt.split(None, 2) # ['/goal', subcommand?, rest?]
|
||||
cmd = parts[1] if len(parts) > 1 else 'status'
|
||||
rest = parts[2] if len(parts) > 2 else ''
|
||||
|
||||
if cmd == 'set':
|
||||
if not rest:
|
||||
return '❌ Usage: `/goal set <objective>` or `/goal set <objective> | <stopping condition>`'
|
||||
if '|' in rest:
|
||||
objective, stopping = [s.strip() for s in rest.split('|', 1)]
|
||||
else:
|
||||
objective, stopping = rest.strip(), 'not specified'
|
||||
content = (
|
||||
f'# Goal\n\n'
|
||||
f'**Objective:** {objective}\n'
|
||||
f'**Stopping condition:** {stopping}\n'
|
||||
f'**Status:** active\n'
|
||||
f'**Set at:** {_now_iso()}\n\n'
|
||||
f'## Checkpoint log\n'
|
||||
)
|
||||
_write_goal(content)
|
||||
return f'✅ Goal set: {objective}\nStopping condition: {stopping}'
|
||||
|
||||
elif cmd in ('status', '/goal'):
|
||||
content = _read_goal()
|
||||
if not content:
|
||||
return '📋 No active goal. Use `/goal set <objective>` to set one.'
|
||||
return content
|
||||
|
||||
elif cmd == 'checkpoint':
|
||||
if not rest:
|
||||
return '❌ Usage: `/goal checkpoint <note>`'
|
||||
content = _read_goal()
|
||||
if not content:
|
||||
return '❌ No active goal to checkpoint.'
|
||||
entry = f'- [{_now_iso()}] {rest}\n'
|
||||
content = content.rstrip() + '\n' + entry
|
||||
_write_goal(content)
|
||||
return f'✅ Checkpoint added: {rest}'
|
||||
|
||||
elif cmd == 'pause':
|
||||
content = _read_goal()
|
||||
if not content:
|
||||
return '❌ No active goal to pause.'
|
||||
content = content.replace('**Status:** active', '**Status:** paused')
|
||||
_write_goal(content)
|
||||
return '⏸️ Goal paused.'
|
||||
|
||||
elif cmd == 'resume':
|
||||
content = _read_goal()
|
||||
if not content:
|
||||
return '❌ No goal to resume.'
|
||||
content = content.replace('**Status:** paused', '**Status:** active')
|
||||
_write_goal(content)
|
||||
return '▶️ Goal resumed.'
|
||||
|
||||
elif cmd == 'clear':
|
||||
_delete_goal()
|
||||
return '🗑️ Goal cleared.'
|
||||
|
||||
else:
|
||||
# Not a recognized subcommand — treat the whole thing as status check
|
||||
content = _read_goal()
|
||||
if not content:
|
||||
return '📋 No active goal. Use `/goal set <objective>` to set one.'
|
||||
return content
|
||||
|
||||
|
||||
# ── Entrypoint ────────────────────────────────────────────────────────────
|
||||
|
||||
# Module-level actor_id for tool closures (set per-invocation)
|
||||
@@ -600,16 +725,37 @@ async def main(payload: dict, context):
|
||||
tools=all_tools,
|
||||
)
|
||||
|
||||
# Intercept /goal commands — handle directly without LLM
|
||||
prompt = payload.get('prompt', '')
|
||||
if prompt.strip().startswith('/goal'):
|
||||
goal_reply = _handle_goal_command(prompt.strip())
|
||||
if goal_reply is not None:
|
||||
yield {'data': goal_reply}
|
||||
_typing_active = False
|
||||
session_manager.close()
|
||||
mcp_loader.close_mcp_clients(_mcp_to_close)
|
||||
return
|
||||
|
||||
# Intercept heartbeat: replace bare [HEARTBEAT] with a strict-format instruction.
|
||||
# Agent-runner suppresses replies that start with HEARTBEAT_OK, so only real alerts reach Telegram.
|
||||
prompt = payload.get('prompt', '')
|
||||
if prompt.strip() == '[HEARTBEAT]':
|
||||
# Inject goal context into heartbeat if active
|
||||
goal_ctx = _get_active_goal_context()
|
||||
goal_heartbeat = ''
|
||||
if goal_ctx:
|
||||
goal_heartbeat = (
|
||||
f' You have an active goal: "{goal_ctx["objective"]}". '
|
||||
f'Stopping condition: "{goal_ctx["stopping_condition"]}". '
|
||||
f'Last checkpoint: "{goal_ctx["last_checkpoint"]}". '
|
||||
f'Make progress toward this goal or report blockers.'
|
||||
)
|
||||
prompt = (
|
||||
'HEARTBEAT CHECK: Silently check for anything urgent Daniel should know about '
|
||||
'(calendar events starting within 2 hours, unread urgent emails, overdue reminders). '
|
||||
'Do NOT narrate your checking process. '
|
||||
'If nothing is urgent: reply with the single word HEARTBEAT_OK and nothing else. '
|
||||
'If something IS urgent: reply with 2-3 lines max summarising only the urgent items.'
|
||||
+ goal_heartbeat
|
||||
)
|
||||
|
||||
final_message = None
|
||||
|
||||
@@ -46,6 +46,16 @@ def _get_base_prompt(actor_id: str = '') -> str:
|
||||
s3 = boto3.client('s3')
|
||||
parts = []
|
||||
|
||||
# Inject active goal at the top of context
|
||||
try:
|
||||
obj = s3.get_object(Bucket=bucket, Key='GOAL.md')
|
||||
goal_content = obj['Body'].read().decode('utf-8')
|
||||
if '**Status:** active' in goal_content:
|
||||
parts.append(f'## Active Goal\n{goal_content}')
|
||||
print(f'[prompt_builder] Injected GOAL.md ({len(goal_content)} bytes)')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for fname in ['SOUL.md', 'STATUS.md']:
|
||||
try:
|
||||
obj = s3.get_object(Bucket=bucket, Key=fname)
|
||||
|
||||
Reference in New Issue
Block a user