From a282d853a6ec2db75ecc27e091202904ac33ca68 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Thu, 2 Apr 2026 22:47:30 -0700 Subject: [PATCH] Added streaming --- .../__pycache__/config.cpython-311.pyc | Bin 0 -> 18219 bytes .../__pycache__/orchestrator.cpython-311.pyc | Bin 0 -> 40662 bytes .../__pycache__/task_store.cpython-311.pyc | Bin 0 -> 6181 bytes orchestration/__pycache__/ui.cpython-311.pyc | Bin 0 -> 42722 bytes orchestration/orchestrator.py | 119 +++++-- orchestration/ui.py | 314 +++++++++++++++++- 6 files changed, 400 insertions(+), 33 deletions(-) create mode 100644 orchestration/__pycache__/config.cpython-311.pyc create mode 100644 orchestration/__pycache__/orchestrator.cpython-311.pyc create mode 100644 orchestration/__pycache__/task_store.cpython-311.pyc create mode 100644 orchestration/__pycache__/ui.cpython-311.pyc diff --git a/orchestration/__pycache__/config.cpython-311.pyc b/orchestration/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..060aaa209f0f33653786e9a4aa2eabd019c9253c GIT binary patch literal 18219 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 literal 0 HcmV?d00001 diff --git a/orchestration/__pycache__/orchestrator.cpython-311.pyc b/orchestration/__pycache__/orchestrator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e26df96d6870ac043f5a37d37b2037d487cbf3b4 GIT binary patch literal 40662 zcmch=3v?S-dL~!}6oA5m1Rvo01&X9dh%db@$+B!pq$KJ=sU^w}+HN+*0wqcmDHlM= zfeF{O|v|fBm?q$RptR!Rh~T^1pjU5dH)G z$Q?!5z{CH;AqejXX9PtU5fo9eO^G8SyV^!<>}nse<7%IhE;&XVmz*O`k>W{Hu1l^F z7Yo~_;iK$4Gg355}VHfAwm`X3pyL@8nKD8f5R@s3iwuV})i zlp$QEl(TRE;c}&d#q%RxC4L7KajYU6w+g>2-2(k7=ass!U#UmjhH#bAi2tT+{yrsm z-7!+FG~@RMcCRK}Qe%{P!l|?%pIV*|a+*}ykkiI+iP9dfW4U#(R3#HWC4`*n+50Xi zo5FSBqOgC$X6B`IhAnyLrMBFk@F-o#sR3`hsC46PwRl4h{;BNL4j!c!&l}nE&3L{g z@0qM@#j_^XBiqo<+pX<954P|Yau$#ZlP`Fjuu@~*3?6js(9z#ha zWfxkf%_fA~l--&_w;h^#aYoRK%<-Y4u!I;>{(mr`kRGxZd%Dz=8+gZx} zNO|CqDf?HY>|iMekn-RoQyyBCGQ?6IM#{lQraZDLC#RVlk!%3-8D^T?D>uS(g&Qa*!}&ptBcbE{JJvXmo8`TQeO zzOX9gW|s0rr2P6LQ;x1mxrI`m6O?cKNL0oinfC0ev|9=H04I%O25iGjC9H@UiaDWR z25!e4`ejqX__afsxGs$B3_qrvyU4Jt>cg9NTIT}rwCyze4^7SfsVE4Z8Fl=8I1*LI zqBCmG3F%~`+9#bMLWX6f`EF(Q0&zZ>Bx$quO5Xa?f(erpuVL|ATBVE9US>Kf(S5}dglo(_%$l}R-`9?cbtWsSSjW0%4T&)4HQ6@FRSRQ+VmnScy;`~mxx+=Qom$Z-!g=}fJG8P>hpBjrq z(vFGA@RSk~(=zUbqm!4yfG_*w;r9@DM+gg8>IGWnvBcZLl41`#!cG%CD-!NG z!Y;)bmK9f6!ZIXdK)SzTTsPu+zAP>l91r4o74K;wI;)1$o=amdk49%MhNmNGry9n+>2YUX2iOs&|A#?5Ax&&6#IVq!(l zhxtQlgLWl6F*Z9D9W|q%S$KZEeEj!Z8@n{sgTYfiG!q@Xba{%6*swA{P4l5MEk(jp z6RM07t8M~BT&i9IG_E34Q9j|mXz!i59POpf=rw0Ia7x}UhIG;r&A?c>wbOg(W-Db` z3otKyR@|JiiAB$d5A1fo=T`ziMzFg*5Ajs6w66;{zK#Efy8w;~BHB1EEF!%6h>LMs z+#Z+Wjt@Wpe}sR2oEAP5heD!Sj-P4?KwiDnGW`G^`oq*b-B1Cu{|8c+VqYVmE5=&#tH z7T&RcN1AggwzzY`_KbkvA`$GEY4+H+`=|y%U_KhX7$MgPbr3e}XveskzKCFZyd5JX ziiz0X6^w>oj)sEc=Vzxc0+I8c4hH-Au-X$0Uz&_Y*qED`1rAfgk;^mFn6_gy$0EpV zDjYO&=cE4qoXeY(PW>vMn{#Iaoo0}=xJ$cG7?Qo%H((yD1B;X zESk1s{o;d?+DWYg_)uW4V6|7NIT&a}Ad&%?H~um<$suP10v?CMv#>2A;Ih=Z6#aM; z3*J7>E~(n4j2%x>-JAJ!$1Zkd5A#5=&2dy*#7pS9tlZxI(+fQOX*s{9Hg;z0^f0x1 zZ#K}^&jUPy1oQW}a!0x|{0q!(1hvyovukSe4k|=z>cByEWjEIXyfLU9#_?|) zgs(_R8hx9&(0{<#wGT44k*m<|6x`Z#wrV!#5qY6 zYyxl*aMH(iE2$V4Mh(_EZoe!}0;7#m9_&yAlt>y3P8x!c6bMfL#9 z3n_V{W<682Z3~@C=kCZ_?O?KYP_G?KNH@Hx+E%T0(~=Ege*%ANW5aJF&xd~mz_Fns z#v!P9*LDu=6~o;*XI!{|D?ct6Yv=`|M*P(Am$2?k3n5wU z!7nV5=Yasc<1@4*N3o28xEKe115He^fkV&FOpb#jab<}Up1=GhsI-Xi^k|NrQE9D> z&}7LX)gZ?!gl%F~mhLw3Y#*h$0l*PwYQv`GD_YGyI;)?&(~+z`k|;{mG`w4<*M#(% z9rtSX-mTf2DyvFWx1_3?QxzLhRdu(%sa5qXwPw6R?Gq0KXDt?ZfQ;Y_tPc`$rdTMc z*8NS3qVC_I`#0(St(t$=@>xyZwR#8lDAI0d1B$<;^C$6 z^7D5#C4I+r-|+`_TM1eU0I1aMSzTx9Nk&BUU^@I)nKpJs`q=RSCc#I>M8L$c=TDri z!nXW7a$Gj2QbfGr&;PCDF(T&FnsZ0}s9E-iyA_vWx5^ObJaLZ-gdO*oBv0IZf!c!| zDrAiLzs*=TPV7YK2*t;erx^heyfQh)NE+Tv-82QqqLXJq%JiKa>@rEF9KjR>8%#ts zkxZ*drQs}9733U3a7^x5_=4xw1ZVG?K&5Zjzyy?Pvm_xW>R-k9{X5wMsBKvXVj+vBE)Xw z5EQN)M-ed;YhE>#I3aG5d0(Rp0+}2EGDHdPT2l*Q>VOtJ-t7YR}sEDpg*U zDH5t5e;~N3F%bbWf~#zOkZ@*7g!&B`L3VF>G`QK8DzCovL`D#cYEmWDsX)!GIeNM!RTW%p zd%s(&+^m(D=jWMH6l!7T!cNBKeByBK57;;+bF1}eFSUUH(UfYCzy<6Mj@=o- z0F`zJTH64g3J2PCHQ;5UlB7M1&1JN>O6res3TTTo4 z*i@0?!uO%GiHqO2DbDvC)^b1~;fnncLp%FtC#MvK7sqE*CjGK9Sxh!zk%4i3F&8Ts zy>=ORFG5V>%+wVUW~eilvQjuEg5l$7jw> zLuRCy5*ez;o?vWePw>F`@c2d6B$spI8VFp7S|(BII+7L{bR4T=x||QqT-s5}n6q?j zL4D}h=)(6LmT3~bi}+gFfB#ipe_$%>tN#m<$g1|39SELpWjuiBGwNhO{K0>G~b zfKhdOejO0}H6-=v7jIr51lBKdc;|TjFCH$C3vpryg*T2I_5biTZkaqFl%9}$hNZAW zu@UdWLUto0v5*r_9W3NR$jL%7LM|3^BP3G@B6p}QKXzLu&4@4TwWz`ziict=UPx1I ziVu>{;%vAGVZTyrjgL5f+=t2%WJzh-(?_H7G?vnEY}={XX%g^)Aiz&Hk8N`|(E*&5vo;%qRK(cYhtkq)1d>@ITRya@Bo9kF z$1Y>~D)`AHMZ`*32D3b-D5I1rR&KUdPwwVlQuK`g#G?9c`0mMdcjdapr*FHo`loOv zW7P#!GoJi4C0t#<=Wjow z)eqoI$_I7%pq4xN>bPn#q%LILNMqI7jj9WsYINmvasDsJGlYMVr<6t)+J=7lpa0<> z{sBm67+J9%gs6{tE<%}b0xn&e<3dNFTN)+)GFG2g!&Of;N*@G3FE+KjJE!>%B%(Ls ziTHBsPrGibwXSB~U;BeU0uB`0w!_`Fnu*yubU>6Poly%2PByI6wH> zV5+=QljuxJl__uW{E_)1uN_I1=G`nQpFc5w;0 zeMx$M4?Shb_z{o~E2{lC{*8l%6dez#!Sc~?7)V$VX|S;nN5*uIBV!6NWUN3e$6_!v zthh#;>=#48_~l~1+zi2bF!;P_X?QGhG3|;1j!r7jlrd!;xURI9rp&qNumXKe+HqE$ z3{Rw8gjj$vi3x&2C!%gci6A+fjb5IGAeO0!h9H%x$L&JGm@2AH5x7KPnn1yjn4o(E zbo@sY0LzdlDw#hvf9$nmDSxhuO9PrjXHp87qoS;C{$S#OCe`6gN_DBS0RNqfSXw?m zkl4mY3qjr&^xCwSBmB{c$*Hh~_%+b>Uf>)MzlfYW%()&HV&J}r)yGV$xUBV#RID3Qw#HO#ihIrxcOZpx(P!pphT;Wzi^7e=if56~|EeSIwC4WR zS|sF+{ZqywoPrfV(82<=M~H3)y%D70$LM%QutIlEib_tBwVdq#o>vT9L=#7Xygbxh4_Zevv^^WF+X4@zlvTGX9Ozg@$+eC|B2zgW5?2tbFf^voOVDqGkH1fiCmtVjKXjMB3};Mgur%hW&uF>U>&a`8qC*MUkb0r*ltEkDkXn!)n??YKl>Q8;U37N>d~!WZ{zk zGU%X`Q7hT_CSv^u{6{vT_TUpdg0D>XHY|FR-p={{l-K{(<8MCx#uGP4l7UrcrMO88 zK6Sel0B5qePcJ4q>ptS`dvo97V~Kr8r&H3;e8o2gQ}TWgXe+YkhWmj_DsqA~D00pZ zWITdgL>X>`XydI{-h5?Y%NwuWc=evY<*vUa>2K5hZJOMcaSBDHBsNX@8gyU7{6MNC zFh7_AYCMuEsUS^QD+38BVW%VH|dZ=>K5{8gV_$I|oAgtKeL98{$!Js}3thGKCEgUNHQtRw5;> z0;`VIm>5EXyjUhC{z@ebu9NmFzgv;8useo+`4q~azC=I;m^}i}0b>awq?r<%t>}EN zGt(xnFbP7*8tKhN8fB4sEK`0BG`j!P;K0Gr zlYPU7)tB)q*g}9pNV_7@(a2;ByawGX=aGg7j-Nc%H$1>%gV_Bxp5+s}<$Nw>3(Cu& zeixB!7y^-35P<04TdI58FpFH?*I)V8E3eI6Cn=s|^FrVUHNR8y+jXQ-=~`0n)@;#g zwxnQN@amhdE?!N%n)LVT{$5S){mfOQyPEI0I_|nUlCDj!klL!U6N zk&t$l0V$7+T?s={%m+`lPyQL6`>0Pyu#G;EJi63)PYT_YLQCzp4{1^;DfR18zb5ss zNY3jgv_N-K>d~bhEqC(PJcZwPqTD^;$D7P$Bs-C+5o*YI_b;e#}!~m9OtX#8L>BnHA zWr#4=6B7=Mji1jNE!c!^<&A{YR zwrTtL#5oJ|?&b0|7GM27qL7R^auETxY+|l^8W*Cvr*;0oink(FRJu~xu~feFq8@rY zS-DrQ+`Cd7(2IkMfn@PUy?A3PP_^&;N`+3_r)RE` zoJyS_30MK2mG(}Duku8biuxnO@22M7jR0)Pq|%#T1D8F2Jmo#~wtUO?-Qrut_e$FD zmb5R)$&xL4$rjCf2Yhtpp4HMmE03@X61gLP=yN5ZC@9zE@XrQs-P zewqHti!#Cxi4BtR{56h79W4wG)8M$WHeNP$Gx(%Dkr`TSo%YX_k1-v1m;G+#R|i1`(9(!29!Z%-tkO5I{#8ZMlpL&4=v*~TGp~Kg(zAgY#R!2^vy{$j)il$pKJGCpd4-i zq|37tkjLVZ9Impiqd05@I7=ZKaCyrBGqUqj4!*3MoiS=^kfM>$FJ03=u>Z^<7#j~_ zb>M5ni8I3|&kUbVmvU+&Te7j~@Dx^@_bKJ?5g_Er(1ZG83e^In#qer_#u9F5!&Vt) zRHFV%MBPQm?5_Z_TI^y$_7iP4C#L-I#o6~`nm>;751-a|oX}35(NB&fcRa7}c%I+Z z!Y^s+6)kcFP!C_t-b4yHeqVCGKJ=}jn|qT|P?v(56wFAKj!h|VX+|h_7q2ul=?&Y! zKHc&V@ZBrfa<^nlsxGM4^`sg$+-ul=w_$s-VW-}(6Y|J+J5#|nJ-8*+)T%f2!nmh& zD~x+e%V6A7S_b2t%H~YDr?L2f0PrgU8NpM%K1du+9L`h=jVh8^JMdn52LOzU=pE~T#Np3B|8=q8YogX< z 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 alqAphzjpBY!Fl!vl_NfNZm(e^%>NJC2=0*p literal 0 HcmV?d00001 diff --git a/orchestration/__pycache__/task_store.cpython-311.pyc b/orchestration/__pycache__/task_store.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81c95c7475ecc3f6f3180be09cbfcb992e6eb374 GIT binary patch literal 6181 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 eRGHzmepF#Iu{- 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 ivVaQIymYQJXoSzxn{X)vZj1Q}@Y{r>@qrWdjR literal 0 HcmV?d00001 diff --git a/orchestration/orchestrator.py b/orchestration/orchestrator.py index f2db31b..3bb96dc 100644 --- a/orchestration/orchestrator.py +++ b/orchestration/orchestrator.py @@ -44,6 +44,13 @@ class ProviderClient: temperature: float, max_tokens: int) -> str: return self.call(system, messages, model, temperature, max_tokens) + async def call_async_streaming(self, system: str, messages: list[dict], model: str, + temperature: float, max_tokens: int): + """Async generator yielding ('thinking'|'content', text) chunks. + Default: emits the full response as a single 'content' chunk.""" + response = await self.call_async(system, messages, model, temperature, max_tokens) + yield ('content', response) + # --- OpenAI and any OpenAI-compatible endpoint --- # Covers AIPA_PROVIDER / {AGENT}_PROVIDER = openai or openai_compatible. @@ -71,6 +78,29 @@ class OpenAIClient(ProviderClient): self.call_async(system, messages, model, temperature, max_tokens) ) + async def call_async_streaming(self, system, messages, model, temperature, max_tokens): + """Stream via the OpenAI-compatible API, yielding thinking and content chunks. + Thinking tokens arrive in delta.reasoning_content (Qwen3, DeepSeek R1 style); + regular tokens arrive in delta.content.""" + full_messages = [{"role": "system", "content": system}] + messages + stream = await self.client.chat.completions.create( + model=model, + temperature=temperature, + max_tokens=max_tokens, + messages=full_messages, + extra_body=self.extra_body or None, + stream=True, + ) + async for chunk in stream: + if not chunk.choices: + continue + delta = chunk.choices[0].delta + thinking = getattr(delta, 'reasoning_content', None) + if thinking: + yield ('thinking', thinking) + if delta.content: + yield ('content', delta.content) + # --- Anthropic --- # Uncomment when ready to use. Install: pip install anthropic @@ -283,6 +313,8 @@ async def call_agent_async( agent: AgentState, user_message: str, dry_run: bool = False, + stream: bool = True, + stream_mode: str = "deliverable", ) -> str: if dry_run: return f"[DRY RUN] Would call {agent.name} ({config.agent_provider_name(agent.name)}) with: {user_message[:100]}..." @@ -290,19 +322,32 @@ async def call_agent_async( agent.add_user(user_message) try: - response = await agent.client.call_async( - system=agent.system_prompt, - messages=agent.messages_for_call(), - model=agent.model, - temperature=agent.temperature, - max_tokens=agent.max_tokens, - ) + if stream: + response = await ui.stream_agent_output( + agent.name, + agent.client.call_async_streaming( + system=agent.system_prompt, + messages=agent.messages_for_call(), + model=agent.model, + temperature=agent.temperature, + max_tokens=agent.max_tokens, + ), + mode=stream_mode, + ) + else: + response = await agent.client.call_async( + system=agent.system_prompt, + messages=agent.messages_for_call(), + model=agent.model, + temperature=agent.temperature, + max_tokens=agent.max_tokens, + ) except NotImplementedError as e: raise RuntimeError(str(e)) from e agent.add_assistant(response) - if config.DEBUG_PRINT_AGENT_OUTPUTS: + if config.DEBUG_PRINT_AGENT_OUTPUTS and not stream: ui.print_agent_panel(agent.name, response) return response @@ -357,7 +402,7 @@ async def dispatch_to_lead( if session_id: task_store.update_task(task, session_id) try: - output = await call_agent_async(lead, task.brief, dry_run=dry_run) + output = await call_agent_async(lead, task.brief, dry_run=dry_run, stream=False) task.output = output task.status = "complete" except Exception as e: @@ -424,7 +469,7 @@ async def run_auditor( f"{deliverable}" ) auditor.history = [] # Auditor is stateless — fresh call each time - return await call_agent_async(auditor, audit_request, dry_run=dry_run) + return await call_agent_async(auditor, audit_request, dry_run=dry_run, stream_mode="audit") # --------------------------------------------------------------------------- @@ -446,7 +491,7 @@ async def update_standing_brief( f"log entry at the top of Section 9. Archive anything older than " f"{config.SESSION_LOG_RETENTION} sessions." ) - return await call_agent_async(orchestrator, update_request, dry_run=dry_run) + return await call_agent_async(orchestrator, update_request, dry_run=dry_run, stream_mode="background") # --------------------------------------------------------------------------- @@ -502,7 +547,7 @@ class Session: f"Directive: {directive}" ) cos_response = await call_agent_async( - self.orchestrator, cos_prompt, dry_run=self.dry_run + self.orchestrator, cos_prompt, dry_run=self.dry_run, stream=True ) # ── Step 2: Parse any task briefs the orchestrator issued ─────────── @@ -532,10 +577,9 @@ class Session: f"Please synthesize these into a final deliverable for the Principal.\n\n" f"{lead_outputs_text}" ) - with ui.synthesis_progress(): - deliverable = await call_agent_async( - self.orchestrator, synthesis_prompt, dry_run=self.dry_run - ) + deliverable = await call_agent_async( + self.orchestrator, synthesis_prompt, dry_run=self.dry_run, stream=True + ) dispatched_leads = sorted({t.assigned_to for t in completed_tasks}) self.session_log.append( @@ -549,10 +593,9 @@ class Session: audit_memo = "" if self.auto_audit: try: - with ui.audit_progress(): - audit_memo = await run_auditor( - self.auditor, deliverable, task_id, dry_run=self.dry_run - ) + audit_memo = await run_auditor( + self.auditor, deliverable, task_id, dry_run=self.dry_run + ) self.session_log.append(f"Audit complete for {task_id}.") except RuntimeError as e: ui.print_warning(f"Audit skipped — {e}") @@ -561,10 +604,9 @@ class Session: async def close(self): summary = "\n".join(self.session_log) - with ui.brief_update_progress(): - updated_brief = await update_standing_brief( - self.orchestrator, summary, dry_run=self.dry_run - ) + updated_brief = await update_standing_brief( + self.orchestrator, summary, dry_run=self.dry_run + ) if not self.dry_run: save_standing_brief(updated_brief) ui.print_session_footer(self.directive_count) @@ -645,17 +687,26 @@ async def cmd_history(session: Session, args: str) -> bool: return True +@command(["/thinking"], "Show the thinking output from the last response") +async def cmd_thinking(session: Session, args: str) -> bool: + last = ui.get_last_thinking() + if not last: + ui.print_system("No thinking output recorded — thinking mode may not be active.") + else: + ui.print_thinking_expansion(last) + return True + + @command(["/audit"], "Re-run Vera's audit on the last deliverable") async def cmd_audit(session: Session, args: str) -> bool: if not session.last_deliverable: ui.print_warning("No deliverable yet — issue a directive first.") return True ui.print_system("Running audit on last deliverable…") - with ui.audit_progress(): - memo = await run_auditor( - session.auditor, session.last_deliverable, - session.last_task_id, dry_run=session.dry_run, - ) + memo = await run_auditor( + session.auditor, session.last_deliverable, + session.last_task_id, dry_run=session.dry_run, + ) ui.print_audit_memo(memo, task_id=session.last_task_id) return True @@ -731,7 +782,15 @@ async def direct_agent_session(agent_name: str, dry_run: bool = False): if not user_input or user_input.lower() in ("/quit", "/exit", "/q"): break - response = await call_agent_async(agent, user_input, dry_run=dry_run) + if user_input.lower() == "/thinking": + last = ui.get_last_thinking() + if not last: + ui.print_system("No thinking output recorded — thinking mode may not be active.") + else: + ui.print_thinking_expansion(last) + continue + + response = await call_agent_async(agent, user_input, dry_run=dry_run, stream=True) ui.print_agent_panel(agent_name, response) ui.print_session_footer(0) diff --git a/orchestration/ui.py b/orchestration/ui.py index cdcbba7..5bf7de4 100644 --- a/orchestration/ui.py +++ b/orchestration/ui.py @@ -23,8 +23,9 @@ from datetime import datetime import config as _config -from rich.console import Console +from rich.console import Console, Group from rich.columns import Columns +from rich.live import Live from rich.markdown import Markdown from rich.panel import Panel from rich.progress import ( @@ -75,6 +76,10 @@ def _build_theme() -> Theme: console = Console(theme=_build_theme(), highlight=False) +_last_thinking: str = "" # Stored from most recent streamed response +_last_stream_rendered: bool = False # True when stream_agent_output rendered the final panel + + # --------------------------------------------------------------------------- # Agent visual config # --------------------------------------------------------------------------- @@ -268,7 +273,13 @@ def print_agent_panel(agent_name: str, content: str, task_id: str = ""): """ Render an agent's output in a styled panel. Content is treated as Markdown. + Skips printing if stream_agent_output already rendered this response. """ + global _last_stream_rendered + if _last_stream_rendered: + _last_stream_rendered = False + return + color = _agent_color(agent_name) title_text = _agent_display_title(agent_name) subtitle = f"[dim]{task_id}[/dim]" if task_id else "" @@ -286,15 +297,51 @@ def print_agent_panel(agent_name: str, content: str, task_id: str = ""): def print_deliverable(content: str, task_id: str = ""): - """Orchestrator's synthesized deliverable.""" - print_agent_panel(_config.ORCHESTRATOR_AGENT, content, task_id) + """ + Orchestrator's synthesized deliverable. + Skips printing if stream_agent_output already rendered this response. + """ + global _last_stream_rendered + if _last_stream_rendered: + _last_stream_rendered = False + return + _print_deliverable_panel(content, task_id) + + +def _print_deliverable_panel(content: str, task_id: str = ""): + """Internal: always render the deliverable panel unconditionally.""" + agent_name = _config.ORCHESTRATOR_AGENT + color = _agent_color(agent_name) + title_text = _agent_display_title(agent_name) + subtitle = f"[dim]{task_id}[/dim]" if task_id else "" + + panel = Panel( + Markdown(content), + title=f"[{color}] {title_text} [/{color}]", + subtitle=subtitle, + border_style=color, + padding=(1, 2), + box=box.ROUNDED, + ) + console.print() + console.print(panel) def print_audit_memo(content: str, task_id: str = ""): """ Auditor's memo. Verdict line drives the border colour. PASS = green, PASS WITH NOTES = yellow, FLAG = yellow, REJECT = red. + Skips printing if stream_agent_output already rendered this response. """ + global _last_stream_rendered + if _last_stream_rendered: + _last_stream_rendered = False + return + _print_audit_memo_panel(content, task_id) + + +def _print_audit_memo_panel(content: str, task_id: str = ""): + """Internal: always render the audit memo panel with verdict-based border.""" aud_name = _config.AUDITOR_AGENT aud_title = _config.agent_title(aud_name) aud_color = _agent_color(aud_name) @@ -390,6 +437,267 @@ def brief_update_progress(): progress.add_task("", total=None) yield +# --------------------------------------------------------------------------- +# Streaming output with thinking support +# --------------------------------------------------------------------------- + +def _thinking_panel_title(agent_name: str, mode: str) -> str: + """Return the thinking panel title, tinted by agent identity.""" + if mode == "audit": + color = _agent_color(agent_name) + return f"[{color}]Vera thinking…[/{color}]" + color = _agent_color(agent_name) + return f"[{color} dim]Thinking…[/{color} dim]" + + +def _build_thinking_renderable( + thinking_text: str, + active: bool, + agent_name: str, + mode: str, + window: int = 300, +) -> object: + """ + Build the thinking panel (active) or collapsed summary line (done). + + Active: dim panel with a rolling window of the last `window` chars, + plus an overflow indicator when earlier content has scrolled off. + Collapsed: single dim line — ' ▸ Thinking (N chars, L lines)'. + """ + if not thinking_text: + return None + + if active: + overflow = len(thinking_text) - window + if overflow > 0: + preview = thinking_text[-window:] + header = Text(f" +{overflow} chars earlier\n", style="dim italic") + body = Text(preview, style="dim") + content = Group(header, body) + else: + content = Text(thinking_text, style="dim") + + return Panel( + content, + title=_thinking_panel_title(agent_name, mode), + border_style="dim", + padding=(0, 1), + box=box.SIMPLE, + ) + else: + lines = thinking_text.count("\n") + 1 + return Text( + f" ▸ Thinking [{len(thinking_text):,} chars · {lines} lines]", + style="dim", + ) + + +async def stream_agent_output( + agent_name: str, + stream_gen, + mode: str = "deliverable", +) -> str: + """ + Consume an async generator of ('thinking'|'content', text) chunks and + render them live in the terminal. + + mode values + ----------- + "deliverable" Normal agent response — thinking panel + full styled panel. + The Live display is NOT transient: the final Markdown panel + is rendered inside the Live block, so there is no erase/ + reprint flash. Sets _last_stream_rendered = True so that + the caller's print_deliverable / print_agent_panel becomes + a no-op. + + "audit" Like "deliverable" but uses Vera's yellow identity cues + during the thinking phase. Sets _last_stream_rendered so + print_audit_memo is a no-op (the verdict-border panel is + rendered here instead). + + "background" Housekeeping call (e.g. standing-brief update). A compact + transient progress line is shown; no full panel is printed. + _last_stream_rendered is set so the caller's print_* is a + no-op. + + In all modes the full content string is returned and _last_thinking is + updated so /thinking works. + + Thinking tokens stream inside a dim panel. When the first content token + arrives the panel collapses to a single dim summary line and content begins + streaming in the agent's normal styled panel. + + The full thinking text is stored in _last_thinking and can be retrieved + with get_last_thinking() (exposed to the /thinking command). + + Returns the full content string. + """ + global _last_thinking, _last_stream_rendered + + color = _agent_color(agent_name) + title_text = _agent_display_title(agent_name) + + # ── Background mode: compact transient progress line ──────────────────── + if mode == "background": + thinking_parts: list[str] = [] + content_parts: list[str] = [] + + with Progress( + SpinnerColumn(style="dim"), + TextColumn("[dim]Updating standing brief…[/dim]"), + TimeElapsedColumn(), + console=console, + transient=True, + ) as prog: + prog.add_task("", total=None) + async for chunk_type, text in stream_gen: + if chunk_type == "thinking": + thinking_parts.append(text) + elif chunk_type == "content": + content_parts.append(text) + + _last_thinking = "".join(thinking_parts) + _last_stream_rendered = True + return "".join(content_parts) + + # ── Deliverable / audit modes ──────────────────────────────────────────── + thinking_parts: list[str] = [] + content_parts: list[str] = [] + phase = "start" # "start" | "thinking" | "content" + + def _build_final_panel(content_text: str) -> Panel: + """Render the agent's finished output as a polished Markdown panel.""" + if mode == "audit": + # Re-derive verdict border from completed content + aud_name = _config.AUDITOR_AGENT + aud_title = _config.agent_title(aud_name) + aud_color = _agent_color(aud_name) + verdict_style = "dim" + for line in content_text.splitlines(): + upper = line.upper() + if "VERDICT:" in upper: + if "REJECT" in upper: + verdict_style = "reject" + elif "FLAG" in upper: + verdict_style = "flag" + elif "PASS WITH NOTES" in upper: + verdict_style = "warning" + elif "PASS" in upper: + verdict_style = "pass" + break + return Panel( + Markdown(content_text), + title=f"[{aud_color}] {aud_name.capitalize()} — {aud_title} [/{aud_color}]", + border_style=verdict_style if verdict_style != "dim" else aud_color, + padding=(1, 2), + box=box.ROUNDED, + ) + else: + return Panel( + Markdown(content_text), + title=f"[{color}] {title_text} [/{color}]", + border_style=color, + padding=(1, 2), + box=box.ROUNDED, + ) + + def _renderable(final: bool = False): + """ + Build the live renderable for the current stream state. + + final=True: replace plain Text content with Markdown and apply the + verdict-aware border so the Live display ends in its final polished + form. This avoids a transient erase + reprint flash. + """ + thinking_text = "".join(thinking_parts) + content_text = "".join(content_parts) + items: list = [] + + # ── Agent identity line (shown while only thinking, not yet content) ── + if phase in ("start", "thinking") and not content_text: + items.append(Text( + f" {title_text}", + style=f"dim {color}", + )) + + # ── Thinking panel / collapsed summary ────────────────────────────── + thinking_active = (phase == "thinking") + think_renderable = _build_thinking_renderable( + thinking_text, thinking_active, agent_name, mode + ) + if think_renderable is not None: + items.append(think_renderable) + + # ── Content panel ──────────────────────────────────────────────────── + if content_text or phase == "content": + if final and content_text: + items.append(_build_final_panel(content_text)) + else: + items.append(Panel( + Text(content_text), + title=f"[{color}] {title_text} [/{color}]", + border_style=color, + padding=(1, 2), + box=box.ROUNDED, + )) + + if not items: + # Nothing yet — show a faint connecting indicator + return Text( + f" [{_agent_color(agent_name)} dim]{title_text} …[/{_agent_color(agent_name)} dim]", + style="dim", + ) + return Group(*items) if len(items) > 1 else items[0] + + console.print() + # transient=False: the Live display persists — we render the final panel + # inside it before stopping, so there is no erase/reprint cycle. + with Live( + _renderable(), + console=console, + refresh_per_second=15, + transient=False, + vertical_overflow="visible", + ) as live: + async for chunk_type, text in stream_gen: + if chunk_type == "thinking": + if phase == "start": + phase = "thinking" + thinking_parts.append(text) + elif chunk_type == "content": + if phase in ("start", "thinking"): + phase = "content" + content_parts.append(text) + live.update(_renderable()) + + # Final update: replace plain Text with formatted Markdown panel + # (and apply verdict border for audit mode) before the Live context + # exits and the display freezes in place. + live.update(_renderable(final=True)) + + _last_thinking = "".join(thinking_parts) + _last_stream_rendered = True + return "".join(content_parts) + + +def get_last_thinking() -> str: + """Return the thinking text captured from the most recent streamed response.""" + return _last_thinking + + +def print_thinking_expansion(thinking_text: str): + """Render the stored thinking output in a dim panel.""" + panel = Panel( + Text(thinking_text, style="dim"), + title="[dim] Thinking — last response [/dim]", + border_style="dim", + padding=(1, 2), + box=box.ROUNDED, + ) + console.print() + console.print(panel) + + # --------------------------------------------------------------------------- # Dry-run callout # ---------------------------------------------------------------------------