diff --git a/.agents/skills/stop-slop/CHANGELOG.md b/.agents/skills/stop-slop/CHANGELOG.md new file mode 100644 index 00000000..64d9f541 --- /dev/null +++ b/.agents/skills/stop-slop/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +## 2026-01-13 + +### Added + +**Phrases (references/phrases.md)** +- Throat-clearing: "Here's what I find interesting", "Here's the problem though" +- Performative emphasis: "creeps in", "I promise", "They exist, I promise" +- Telling instead of showing: "This is genuinely hard", "This is what leadership actually looks like" + +**Structures (references/structures.md)** +- Binary contrasts: "Not X. But Y.", "It's not this. It's that.", "stops being X and starts being Y" +- Rhythm patterns: staccato fragmentation, dashes for dramatic pause, hedging as reassurance +- Word patterns: absolute words (always, never, everyone, etc.), AI-overused intensifiers (deeply, truly, fundamentally, inherently, simply, literally, inevitably) + +## 2026-01-12 + +- Restructured skill following Claude Code best practices (PR #1) +- Split into SKILL.md and references/ folder + +## 2025-01-12 + +- Initial release diff --git a/.agents/skills/stop-slop/LICENSE b/.agents/skills/stop-slop/LICENSE new file mode 100644 index 00000000..e0ede9b6 --- /dev/null +++ b/.agents/skills/stop-slop/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Hardik Pandya + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/stop-slop/README.md b/.agents/skills/stop-slop/README.md new file mode 100644 index 00000000..3c982562 --- /dev/null +++ b/.agents/skills/stop-slop/README.md @@ -0,0 +1,62 @@ +# Stop Slop + +A skill for removing AI tells from prose. + +G-Yg4RVbIAAhVxW + +## What this is + +AI writing has patterns. Predictable phrases, structures, rhythms. This skill teaches Claude (or any LLM) to catch and remove them. + +## Skill Structure + +``` +stop-slop/ +├── SKILL.md # Core instructions +├── references/ +│ ├── phrases.md # Phrases to remove +│ ├── structures.md # Structural patterns to avoid +│ └── examples.md # Before/after transformations +├── README.md +└── LICENSE +``` + +## Quick start + +**Claude Code:** Add this folder as a skill. + +**Claude Projects:** Upload `SKILL.md` and reference files to project knowledge. + +**Custom instructions:** Copy core rules from `SKILL.md`. + +**API calls:** Include `SKILL.md` in your system prompt. Reference files load on demand. + +## What it catches + +**Banned phrases** - Throat-clearing openers, emphasis crutches, business jargon, all adverbs, vague declaratives, meta-commentary. See `references/phrases.md`. + +**Structural clichés** - Binary contrasts, negative listings, dramatic fragmentation, rhetorical setups, false agency, narrator-from-a-distance voice, passive voice. See `references/structures.md`. + +**Sentence-level rules** - No Wh- sentence starters, no em dashes, no staccato fragmentation, no lazy extremes, active voice required. + +## Scoring + +Rate 1-10 on each dimension: + +| Dimension | Question | +|-----------|----------| +| Directness | Statements or announcements? | +| Rhythm | Varied or metronomic? | +| Trust | Respects reader intelligence? | +| Authenticity | Sounds human? | +| Density | Anything cuttable? | + +Below 35/50: revise. + +## Author + +[Hardik Pandya](https://hvpandya.com) + +## License + +MIT. Use freely, share widely. diff --git a/.agents/skills/stop-slop/SKILL.md b/.agents/skills/stop-slop/SKILL.md new file mode 100644 index 00000000..83f2032f --- /dev/null +++ b/.agents/skills/stop-slop/SKILL.md @@ -0,0 +1,68 @@ +--- +name: stop-slop +description: Remove AI writing patterns from prose. Use when drafting, editing, or reviewing text to eliminate predictable AI tells. +metadata: + trigger: Writing prose, editing drafts, reviewing content for AI patterns + author: Hardik Pandya (https://hvpandya.com) +--- + +# Stop Slop + +Eliminate predictable AI writing patterns from prose. + +## Core Rules + +1. **Cut filler phrases.** Remove throat-clearing openers, emphasis crutches, and all adverbs. See [references/phrases.md](references/phrases.md). + +2. **Break formulaic structures.** Avoid binary contrasts, negative listings, dramatic fragmentation, rhetorical setups, false agency. See [references/structures.md](references/structures.md). + +3. **Use active voice.** Every sentence needs a human subject doing something. No passive constructions. No inanimate objects performing human actions ("the complaint becomes a fix"). + +4. **Be specific.** No vague declaratives ("The reasons are structural"). Name the specific thing. No lazy extremes ("every," "always," "never") doing vague work. + +5. **Put the reader in the room.** No narrator-from-a-distance voice. "You" beats "People." Specifics beat abstractions. + +6. **Vary rhythm.** Mix sentence lengths. Two items beat three. End paragraphs differently. No em dashes. + +7. **Trust readers.** State facts directly. Skip softening, justification, hand-holding. + +8. **Cut quotables.** If it sounds like a pull-quote, rewrite it. + +## Quick Checks + +Before delivering prose: + +- Any adverbs? Kill them. +- Any passive voice? Find the actor, make them the subject. +- Inanimate thing doing a human verb ("the decision emerges")? Name the person. +- Sentence starts with a Wh- word? Restructure it. +- Any "here's what/this/that" throat-clearing? Cut to the point. +- Any "not X, it's Y" contrasts? State Y directly. +- Three consecutive sentences match length? Break one. +- Paragraph ends with punchy one-liner? Vary it. +- Em-dash anywhere? Remove it. +- Vague declarative ("The implications are significant")? Name the specific implication. +- Narrator-from-a-distance ("Nobody designed this")? Put the reader in the scene. +- Meta-joiners ("The rest of this essay...")? Delete. Let the essay move. + +## Scoring + +Rate 1-10 on each dimension: + +| Dimension | Question | +|-----------|----------| +| Directness | Statements or announcements? | +| Rhythm | Varied or metronomic? | +| Trust | Respects reader intelligence? | +| Authenticity | Sounds human? | +| Density | Anything cuttable? | + +Below 35/50: revise. + +## Examples + +See [references/examples.md](references/examples.md) for before/after transformations. + +## License + +MIT diff --git a/.agents/skills/stop-slop/references/examples.md b/.agents/skills/stop-slop/references/examples.md new file mode 100644 index 00000000..0376b6d6 --- /dev/null +++ b/.agents/skills/stop-slop/references/examples.md @@ -0,0 +1,59 @@ +# Before/After Examples + +## Example 1: Throat-Clearing + Binary Contrast + +**Before:** +> "Here's the thing: building products is hard. Not because the technology is complex. Because people are complex. Let that sink in." + +**After:** +> "Building products is hard. Technology is manageable. People aren't." + +**Changes:** Removed opener, binary contrast structure, and emphasis crutch. Direct statements. + +--- + +## Example 2: Filler + Unnecessary Reassurance + +**Before:** +> "It turns out that most teams struggle with alignment. The uncomfortable truth is that nobody wants to admit they're confused. And that's okay." + +**After:** +> "Teams struggle with alignment. Nobody admits confusion." + +**Changes:** Cut hedging ("most"), removed throat-clearing phrases, deleted permission-granting ending. + +--- + +## Example 3: Business Jargon Stack + +**Before:** +> "In today's fast-paced landscape, we need to lean into discomfort and navigate uncertainty with clarity. This matters because your competition isn't waiting." + +**After:** +> "Move faster. Your competition is." + +**Changes:** Eliminated jargon entirely. Core message in six words. + +--- + +## Example 4: Dramatic Fragmentation + +**Before:** +> "Speed. Quality. Cost. You can only pick two. That's it. That's the tradeoff." + +**After:** +> "Speed, quality, cost—pick two." + +**Changes:** Single sentence. No performative emphasis. + +--- + +## Example 5: Rhetorical Setup + +**Before:** +> "What if I told you that the best teams don't optimize for productivity? Here's what I mean: they optimize for learning. Think about it." + +**After:** +> "The best teams optimize for learning, not productivity." + +**Changes:** Direct claim. No rhetorical scaffolding. diff --git a/.agents/skills/stop-slop/references/phrases.md b/.agents/skills/stop-slop/references/phrases.md new file mode 100644 index 00000000..f9234fe2 --- /dev/null +++ b/.agents/skills/stop-slop/references/phrases.md @@ -0,0 +1,128 @@ +# Phrases to Remove + +## Throat-Clearing Openers + +Remove these announcement phrases. State the content directly. + +- "Here's the thing:" +- "Here's what [X]" +- "Here's this [X]" +- "Here's that [X]" +- "Here's why [X]" +- "The uncomfortable truth is" +- "It turns out" +- "The real [X] is" +- "Let me be clear" +- "The truth is," +- "I'll say it again:" +- "I'm going to be honest" +- "Can we talk about" +- "Here's what I find interesting" +- "Here's the problem though" + +Any "here's what/this/that" construction is throat-clearing before the point. Cut it and state the point. + +## Emphasis Crutches + +These add no meaning. Delete them. + +- "Full stop." / "Period." +- "Let that sink in." +- "This matters because" +- "Make no mistake" +- "Here's why that matters" + +## Business Jargon + +Replace with plain language. + +| Avoid | Use instead | +|-------|-------------| +| Navigate (challenges) | Handle, address | +| Unpack (analysis) | Explain, examine | +| Lean into | Accept, embrace | +| Landscape (context) | Situation, field | +| Game-changer | Significant, important | +| Double down | Commit, increase | +| Deep dive | Analysis, examination | +| Take a step back | Reconsider | +| Moving forward | Next, from now | +| Circle back | Return to, revisit | +| On the same page | Aligned, agreed | + +## Adverbs + +Kill all adverbs. No -ly words. No softeners, no intensifiers, no hedges. + +Specific offenders: + +- "really" +- "just" +- "literally" +- "genuinely" +- "honestly" +- "simply" +- "actually" +- "deeply" +- "truly" +- "fundamentally" +- "inherently" +- "inevitably" +- "interestingly" +- "importantly" +- "crucially" + +Also cut these filler phrases: + +- "At its core" +- "In today's [X]" +- "It's worth noting" +- "At the end of the day" +- "When it comes to" +- "In a world where" +- "The reality is" + +## Meta-Commentary + +Remove self-referential asides. The essay should move, not announce its own structure. + +- "Hint:" +- "Plot twist:" / "Spoiler:" +- "You already know this, but" +- "But that's another post" +- "X is a feature, not a bug" +- "Dressed up as" +- "The rest of this essay explains..." +- "Let me walk you through..." +- "In this section, we'll..." +- "As we'll see..." +- "I want to explore..." + +## Performative Emphasis + +False intimacy or manufactured sincerity: + +- "creeps in" +- "I promise" +- "They exist, I promise" + +## Telling Instead of Showing + +Announcing difficulty or significance rather than demonstrating it: + +- "This is genuinely hard" +- "This is what leadership actually looks like" +- "This is what X actually looks like" +- "actually matters" + +## Vague Declaratives + +Sentences that announce importance without naming the specific thing. Kill these. + +- "The reasons are structural" +- "The implications are significant" +- "This is the deepest problem" +- "The stakes are high" +- "The consequences are real" + +If a sentence says something is important/deep/structural without showing the specific thing, cut it or replace it with the specific thing. diff --git a/.agents/skills/stop-slop/references/structures.md b/.agents/skills/stop-slop/references/structures.md new file mode 100644 index 00000000..bbcc359c --- /dev/null +++ b/.agents/skills/stop-slop/references/structures.md @@ -0,0 +1,134 @@ +# Structures to Avoid + +## Binary Contrasts + +These create false drama. State the point directly. + +| Pattern | Problem | +|---------|---------| +| "Not because X. Because Y." / "Not because X, but because Y." | Telegraphed reversal | +| "[X] isn't the problem. [Y] is." | Formulaic reframe | +| "The answer isn't X. It's Y." | Predictable pivot | +| "It feels like X. It's actually Y." | Setup/reveal cliche | +| "The question isn't X. It's Y." | Rhetorical misdirection | +| "Not X. But Y." / "not X, it's Y" / "isn't X, it's Y" | Mechanical contrast | +| "It's not this. It's that." | Same formula, different words | +| "stops being X and starts being Y" | False transformation arc | +| "doesn't mean X, but actually Y" | Negation-then-assertion crutch | +| "is about X but not Y" | False distinction | +| "not just X but also Y" | Additive hedge | + +**Instead:** State Y directly. "The problem is Y." "Y matters here." Drop the negation entirely. + +## Negative Listing + +Listing what something is *not* before revealing what it *is*. A rhetorical striptease. + +| Pattern | Problem | +|---------|---------| +| "Not a X... Not a Y... A Z." | Dramatic buildup through negation | +| "It wasn't X. It wasn't Y. It was Z." | Same structure, past tense | + +**Instead:** State Z. The reader doesn't need the runway. + +## Dramatic Fragmentation + +Sentence fragments for emphasis read as manufactured profundity. + +| Pattern | Problem | +|---------|---------| +| "[Noun]. That's it. That's the [thing]." | Performative simplicity | +| "X. And Y. And Z." | Staccato drama | +| "This unlocks something. [Word]." | Artificial revelation | + +**Instead:** Complete sentences. Trust content over presentation. + +## Rhetorical Setups + +These announce insight rather than deliver it. + +| Pattern | Problem | +|---------|---------| +| "What if [reframe]?" | Socratic posturing | +| "Here's what I mean:" | Redundant preview | +| "Think about it:" | Condescending prompt | +| "And that's okay." | Unnecessary permission | + +**Instead:** Make the point. Let readers draw conclusions. + +## Formulaic Constructions + +| Pattern | Problem | +|---------|---------| +| "By the time X, I was Y." | Narrative template | +| "X that isn't Y" | Indirect. Say "X is broken" | + +## False Agency + +Giving inanimate things human verbs. Complaints don't "become" fixes. Bets don't "live or die." Decisions don't "emerge." A person does something to make those things happen. AI loves this because it avoids naming the actor. + +| Pattern | Problem | +|---------|---------| +| "a complaint becomes a fix" | The complaint did nothing. Someone fixed it. | +| "a bet lives or dies in days" | Bets don't have lifespans. Someone kills the project or ships it. | +| "the decision emerges" | Decisions don't emerge. Someone decides. | +| "the culture shifts" | Cultures don't shift on their own. People change behavior. | +| "the conversation moves toward" | Conversations don't move. Someone steers. | +| "the data tells us" | Data sits there. Someone reads it and draws a conclusion. | +| "the market rewards" | Markets don't reward. Buyers pay for things. | + +**Instead:** Name the human. "The team fixed it that week" beats "the complaint becomes a fix." If no specific person fits, use "you" to put the reader in the seat. + +## Narrator-from-a-Distance + +Floating above the scene instead of putting the reader in it. + +| Pattern | Problem | +|---------|---------| +| "Nobody designed this." | Disembodied observation | +| "This happens because..." | Lecturer voice | +| "This is why..." | Same | +| "People tend to..." | Armchair sociologist | + +**Instead:** Put the reader in the room. "You don't sit down one day and decide to..." beats "Nobody designed this." + +## Passive Voice + +Every sentence needs a subject doing something. Passive voice hides the actor and drains energy. + +| Pattern | Fix | +|---------|-----| +| "X was created" | Name who created it | +| "It is believed that" | Name who believes it | +| "Mistakes were made" | Name who made them | +| "The decision was reached" | Name who decided | + +**Instead:** Find the actor. Put them at the front of the sentence. + +## Sentence Starters to Avoid + +| Pattern | Fix | +|---------|-----| +| Sentences starting with What, When, Where, Which, Who, Why, How | Restructure. Lead with the subject or the verb. | +| Paragraphs starting with "So" | Start with content | +| Sentences starting with "Look," | Remove | + +Wh- openers become a crutch. "What makes this hard is..." becomes "The constraint is..." or better, name the specific constraint. + +## Rhythm Patterns + +| Pattern | Fix | +|---------|-----| +| Three-item lists | Use two items or one | +| Questions answered immediately | Let questions breathe or cut them | +| Every paragraph ends punchily | Vary endings | +| Em-dashes | Remove. Use commas or periods. No em dashes at all. | +| Staccato fragmentation | Don't stack short punchy sentences | +| "Not always. Not perfectly." | Hedging disguised as reassurance | + +## Word Patterns + +| Pattern | Problem | +|---------|---------| +| Lazy extremes (every, always, never, everyone, everybody, nobody) | False authority. Use specifics instead of sweeping claims. | +| All adverbs (-ly words, "really," "just," "literally," "genuinely," "honestly," "simply," "actually") | Empty emphasis. See phrases.md for full list. | diff --git a/.changeset/acp-login-auth-method.md b/.changeset/acp-login-auth-method.md new file mode 100644 index 00000000..afde0ee4 --- /dev/null +++ b/.changeset/acp-login-auth-method.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": minor +--- + +Rename the ACP authentication method to reflect that login is multi-provider: it now reads "Log in with a provider" and explains that the provider is chosen in a terminal. Clients matching the previous wording will need updating. diff --git a/.changeset/catalog-and-batch-robustness.md b/.changeset/catalog-and-batch-robustness.md new file mode 100644 index 00000000..63e0f23b --- /dev/null +++ b/.changeset/catalog-and-batch-robustness.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Survive two malformed inputs that used to end a run. A catalog entry that is not an object is now dropped when the catalog is read, instead of reaching the provider picker and throwing past the bundled-catalog fallback that was meant to save the login. A non-finite subagent concurrency limit now falls back to the default: `NaN` passed every clamp, and each free-slot test against it was false, so the batch launched nothing and never finished. diff --git a/.changeset/codex-login-declared-efforts.md b/.changeset/codex-login-declared-efforts.md new file mode 100644 index 00000000..4dc329e6 --- /dev/null +++ b/.changeset/codex-login-declared-efforts.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Offer a model's declared thinking-effort levels when signing in to OpenAI Codex. The picker previously fell back to low / medium / high regardless of what the model supports, disagreeing with the effort list recorded in the config it then wrote. diff --git a/.changeset/dynamic-workflow-output-schema.md b/.changeset/dynamic-workflow-output-schema.md new file mode 100644 index 00000000..cbe842ac --- /dev/null +++ b/.changeset/dynamic-workflow-output-schema.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": minor +--- + +Let a Dynamic Workflow require structured output from its subagents. Passing `output_schema` makes each subagent return a validated object instead of free text, and a subagent that cannot satisfy the schema is reported separately from one that failed outright. diff --git a/.changeset/login-cancel-and-provider-id.md b/.changeset/login-cancel-and-provider-id.md new file mode 100644 index 00000000..68fa5b12 --- /dev/null +++ b/.changeset/login-cancel-and-provider-id.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Accept a provider's plain id for `--provider` at login, so a catalog provider no longer has to be named by its full display name, and stop a cancelled OpenAI Codex sign-in from holding the process open for the rest of its two-minute callback timeout. In the editor extension, signing in now shows one cancellable progress notification, a repeated sign-in joins the one already running instead of opening a second set of prompts, and a completed sign-in is no longer reported as failed when the status refresh behind it fails. diff --git a/.changeset/login-effort-persists.md b/.changeset/login-effort-persists.md new file mode 100644 index 00000000..25a0cf45 --- /dev/null +++ b/.changeset/login-effort-persists.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Save the thinking-effort level picked during login. Only an on/off flag was stored, so choosing low, medium, or xhigh reopened the session at high, and an OpenAI Codex login reopened at the model's maximum effort regardless of the choice. diff --git a/.changeset/login-effort-reaches-disk.md b/.changeset/login-effort-reaches-disk.md new file mode 100644 index 00000000..dda8f6fa --- /dev/null +++ b/.changeset/login-effort-reaches-disk.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Write the thinking effort picked at login to disk. The apply step recorded the level, but the patch that saved the result listed everything except it, so an API-key login still reopened at the default effort. Choosing `off` now also clears a level a previous login left behind, which a patch that only merges could not do by omitting the key. diff --git a/.changeset/login-keeps-provider-on-cancel.md b/.changeset/login-keeps-provider-on-cancel.md new file mode 100644 index 00000000..8819a7c7 --- /dev/null +++ b/.changeset/login-keeps-provider-on-cancel.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Keep the configured provider signed in when a login is abandoned. Backing out at the model picker, or a failure while fetching the model list, no longer clears the existing credentials, and dismissing the provider picker returns to the sign-in screen instead of reporting a failed login. diff --git a/.changeset/login-url-scheme-guard.md b/.changeset/login-url-scheme-guard.md new file mode 100644 index 00000000..914b9c2a --- /dev/null +++ b/.changeset/login-url-scheme-guard.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Refuse a device authorization whose verification URL is not HTTPS. Every surface hands that URL to the host's "open externally" API, so a provider answering with `file:`, `javascript:`, or an installed application's own scheme had the agent launch it. The check runs where the response is parsed, so the terminal, the TUI, and the editor extension are all covered. diff --git a/.changeset/multi-provider-login-picker.md b/.changeset/multi-provider-login-picker.md new file mode 100644 index 00000000..3b85e88b --- /dev/null +++ b/.changeset/multi-provider-login-picker.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": minor +--- + +`pythinker login` now opens a provider picker instead of going straight to one provider, and accepts `--provider ` to skip it. The VS Code extension's sign-in offers the same providers, and both surfaces present the same thinking-effort levels for a given model. diff --git a/.changeset/workflow-blank-items-followups.md b/.changeset/workflow-blank-items-followups.md new file mode 100644 index 00000000..2580bdb7 --- /dev/null +++ b/.changeset/workflow-blank-items-followups.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Finish handling blank Dynamic Workflow items. A run that dropped one reported its results after a note explaining the drop, which made the whole result parse as unsupported and rendered a successful run as failed; the note now follows the results. A blank entry also no longer leaves a row queued forever with the header stuck below its total, and no longer pushes a full item list over the subagent cap and back into whole-call rejection. diff --git a/.changeset/workflow-ignores-empty-items.md b/.changeset/workflow-ignores-empty-items.md new file mode 100644 index 00000000..820ec412 --- /dev/null +++ b/.changeset/workflow-ignores-empty-items.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Ignore empty entries in a Dynamic Workflow's item list instead of rejecting the call. A trailing empty item used to fail argument validation, which discarded the whole workflow before any subagent started and forced the agent to send every prompt again. The dropped count is now reported with the results, and the launch panel counts only the subagents that will actually run. diff --git a/.changeset/workflow-queued-rows-not-stalled.md b/.changeset/workflow-queued-rows-not-stalled.md new file mode 100644 index 00000000..d285a7ae --- /dev/null +++ b/.changeset/workflow-queued-rows-not-stalled.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Stop a Dynamic Workflow row that has not started from reading as stalled. A queued row measured its silence from the launch of the whole run, so a long queue turned every waiting row amber and then red while nothing was wrong. A queued row now shows the same placeholder a finished one does, and a suspended row keeps its count without the alarm colours, because only a running row can stall. diff --git a/.changeset/workflow-retry-keeps-output-schema.md b/.changeset/workflow-retry-keeps-output-schema.md new file mode 100644 index 00000000..68699c75 --- /dev/null +++ b/.changeset/workflow-retry-keeps-output-schema.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Keep a Dynamic Workflow subagent's output schema when a provider rate limit forces its turn to be retried. The retried turn lost the schema, so the subagent answered in prose and the workflow reported it as completed rather than as a schema failure. diff --git a/.changeset/workflow-run-correlation.md b/.changeset/workflow-run-correlation.md new file mode 100644 index 00000000..6d453fa6 --- /dev/null +++ b/.changeset/workflow-run-correlation.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": minor +--- + +Give every Dynamic Workflow run an id and stamp it on the subagent events it produces, so a client can tell which run a given subagent belongs to when several are in flight. diff --git a/.changeset/workflow-running-row-spinner.md b/.changeset/workflow-running-row-spinner.md new file mode 100644 index 00000000..81139e21 --- /dev/null +++ b/.changeset/workflow-running-row-spinner.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Show a Dynamic Workflow's running rows with a spinning grey dot, so a working agent reads as motion rather than as a static dot the eye cannot tell from a finished one, and shimmer the Orchestrating label in periwinkle instead of grey. diff --git a/.changeset/workflow-size-guideline-and-kill-switch.md b/.changeset/workflow-size-guideline-and-kill-switch.md new file mode 100644 index 00000000..aeb193c4 --- /dev/null +++ b/.changeset/workflow-size-guideline-and-kill-switch.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": minor +--- + +Add two ways to rein in Dynamic Workflow fan-out: `disableWorkflows` turns the tool off entirely, and `workflowSizeGuideline` sets an advisory ceiling that is mentioned to the model and warned about, on every surface, when a run exceeds it. Both are settable in config or by environment variable. diff --git a/.changeset/workflow-subagent-caps.md b/.changeset/workflow-subagent-caps.md new file mode 100644 index 00000000..f2ff9cc4 --- /dev/null +++ b/.changeset/workflow-subagent-caps.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": minor +--- + +Bound subagent fan-out with hard caps: 128 subagents per call, 200 per session, and a nesting depth of 3. Nesting was previously unbounded, so a workflow that spawned workflows could grow without limit; past depth 3 the call now fails instead. diff --git a/.changeset/workflow-warning-wraps.md b/.changeset/workflow-warning-wraps.md new file mode 100644 index 00000000..d4f9730d --- /dev/null +++ b/.changeset/workflow-warning-wraps.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Show the whole large-workflow warning in the editor extension. The line was truncated to the panel width, so in a narrow side panel the reader saw the opening words and no reason. diff --git a/.changeset/workflow-work-and-idle-column.md b/.changeset/workflow-work-and-idle-column.md new file mode 100644 index 00000000..bb6dc42f --- /dev/null +++ b/.changeset/workflow-work-and-idle-column.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": minor +--- + +Replace the Dynamic Workflow progress bar with the two things it can actually know: how many tool calls each agent has made, and how long it has been silent. The old bar pinned every tool-using agent at 75% until it finished, so an agent working hard and one wedged for ten minutes looked identical. A row that goes quiet now turns amber, then red. diff --git a/AGENTS.md b/AGENTS.md index b5d02eae..df440da0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Repository-level Agent Guide -Reply in English unless the user explicitly asks otherwise. +Reply in English unless the user explicitly asks otherwise. Always talk in ASD-STE100 Simplified Technical English. TypeScript monorepo for **pythinker-code**, a provider-agnostic AI coding agent. This file covers product identity, project map, hard constraints, and workflow rules. diff --git a/apps/pythinker-code/package.json b/apps/pythinker-code/package.json index ea676256..5fc947d7 100644 --- a/apps/pythinker-code/package.json +++ b/apps/pythinker-code/package.json @@ -88,6 +88,7 @@ "zod": "^4.4.3" }, "dependencies": { + "@clack/prompts": "1.7.0", "@opentui/core": "0.4.3", "@opentui/keymap": "0.4.3", "@opentui/solid": "0.4.3", diff --git a/apps/pythinker-code/src/auth/terminal-login-ui.ts b/apps/pythinker-code/src/auth/terminal-login-ui.ts new file mode 100644 index 00000000..b28c0298 --- /dev/null +++ b/apps/pythinker-code/src/auth/terminal-login-ui.ts @@ -0,0 +1,274 @@ +/** + * Terminal renderer for the multi-provider login flows (`runLogin` from + * `@pythoughts/pythinker-code-sdk`). + * + * `pythinker login` (and `pythinker acp --login`) build a `LoginUi` backed by + * `@clack/prompts`: the provider picker, API-key / redirect-URL input, model + * and effort selection, and the OAuth device-code spinner. Catalog resolution + * mirrors the TUI's `promptPlatformSelection` — bundled catalog first, live + * fetch with a bundled fallback, so login still works offline. + * + * `--provider ` skips the picker: the option is resolved against the + * same `buildPlatformOptions` list the picker renders, and a non-matching + * value fails loudly instead of falling back to any default provider. + */ + +import { isCancel, log, password, select, spinner, text } from '@clack/prompts'; +import { + type DeviceAuthorization, + type ManagedKimiCodeModelInfo, + type OpenPlatformDefinition, +} from '@pythoughts/pythinker-code-oauth'; +import { + buildPlatformOptions, + catalogModelToAlias, + coerceEffortForModel, + DEFAULT_CATALOG_URL, + effortLevelsForModel, + fetchCatalog, + formatErrorMessage, + loadBuiltInCatalog, + managedModelToAlias, + resolvePlatformOption, + type ApiKeyPromptOptions, + type CatalogModel, + type LoginProgressSpinnerHandle, + type LoginUi, + type ModelAlias, + type PlatformSelection, + type PythinkerHarness, +} from '@pythoughts/pythinker-code-sdk'; + +import { BUILT_IN_CATALOG_JSON } from '#/built-in-catalog'; +import type { ColorToken } from '#/tui/theme'; +import { openUrl } from '#/utils/open-url'; + +/** Thrown when `--provider` does not match any platform id or display name. */ +export class UnknownProviderError extends Error { + constructor( + readonly input: string, + readonly validIds: readonly string[], + ) { + super(`Unknown provider "${input}"`); + this.name = 'UnknownProviderError'; + } +} + +export type TerminalLoginUi = LoginUi; + +export function createTerminalLoginUi( + harness: PythinkerHarness, + options: { readonly provider?: string | undefined } = {}, +): TerminalLoginUi { + const { provider } = options; + let cancelInFlight: (() => void) | undefined; + + function showStatus(message: string, level?: ColorToken): void { + if (level === 'error') { + log.error(message); + } else if (level === 'warning') { + log.warn(message); + } else { + log.info(message); + } + } + + function showLoginProgressSpinner(label: string): LoginProgressSpinnerHandle { + const clackSpinner = spinner(); + clackSpinner.start(label); + return { + stop({ ok, label: finalLabel }) { + if (ok) { + // clack's spinner.stop renders the success tone. + clackSpinner.stop(finalLabel); + } else if (clackSpinner.isCancelled) { + // SIGINT already cancelled the spinner — clack rendered its own + // "Canceled" line — so keep the flow's label visible to preserve + // the "Login cancelled." / "Login failed." message style. + log.error(finalLabel); + } else { + clackSpinner.error(finalLabel); + } + }, + }; + } + + function showLoginAuthorizationPrompt(auth: DeviceAuthorization): LoginProgressSpinnerHandle { + const url = auth.verificationUriComplete || auth.verificationUri; + // Print the manual fallback before attempting to open the user's browser + // so headless/browser-opener failures never hide the URL and code needed + // to complete login. + log.info(`Go to: ${url}`); + log.info(`Enter code: ${auth.userCode}`); + try { + openUrl(url); + } catch { + // Best effort only: the manual fallback has already been printed. + } + return showLoginProgressSpinner('Waiting for authorization…'); + } + + async function promptPlatformSelection(): Promise { + let catalog = loadBuiltInCatalog(BUILT_IN_CATALOG_JSON) ?? {}; + const controller = new AbortController(); + const cancel = (): void => { + controller.abort(); + }; + cancelInFlight = cancel; + const catalogSpinner = showLoginProgressSpinner('Loading provider catalog'); + try { + catalog = await fetchCatalog(DEFAULT_CATALOG_URL, controller.signal); + catalogSpinner.stop({ ok: true, label: 'Provider catalog loaded.' }); + } catch (error) { + if (controller.signal.aborted) { + catalogSpinner.stop({ ok: false, label: 'Aborted.' }); + log.info('Login cancelled.'); + return undefined; + } + catalogSpinner.stop({ ok: false, label: 'Using bundled provider catalog.' }); + showStatus(`Live provider catalog unavailable: ${formatErrorMessage(error)}`, 'warning'); + } finally { + if (cancelInFlight === cancel) cancelInFlight = undefined; + } + + const options = buildPlatformOptions(catalog); + if (provider !== undefined) { + const resolved = resolvePlatformOption(options, provider); + if (resolved === undefined) { + throw new UnknownProviderError(provider, options.map((option) => option.value)); + } + return { platformId: resolved.value, catalog }; + } + + const selected = await select({ + message: 'Select a provider', + options: options.map((option) => ({ + value: option.value, + label: option.label, + hint: option.description, + })), + }); + if (isCancel(selected)) { + log.info('Login cancelled.'); + return undefined; + } + return { platformId: selected, catalog }; + } + + async function promptApiKey( + platformName: string, + subtitleLines?: readonly string[], + promptOptions: ApiKeyPromptOptions = {}, + ): Promise { + for (const line of subtitleLines ?? []) { + log.info(line); + } + const message = promptOptions.title ?? `Enter API key for ${platformName}`; + const emptyMessage = promptOptions.emptyMessage ?? 'API key cannot be empty.'; + const validate = (value: string | undefined): string | undefined => + value === undefined || value.length === 0 ? emptyMessage : undefined; + const result = + promptOptions.secret === false + ? await text({ message, validate }) + : await password({ message, validate }); + if (isCancel(result)) { + log.info('Login cancelled.'); + return undefined; + } + return result; + } + + async function selectModelAndEffort( + modelDict: Record, + ): Promise<{ alias: string; effort: string } | undefined> { + const aliases = Object.keys(modelDict); + const selected = await select({ + message: 'Select a model', + options: aliases.map((alias) => ({ + value: alias, + label: modelDict[alias]?.displayName ?? modelDict[alias]?.model ?? alias, + })), + }); + if (isCancel(selected)) { + log.info('Login cancelled.'); + return undefined; + } + // Same default the TUI's model selector starts from: the first supported + // level, coerced against what the model actually supports. + const model = modelDict[selected]; + const levels = effortLevelsForModel(model); + const initialEffort = coerceEffortForModel( + model, + levels.find((level) => level !== 'off') ?? 'off', + ); + const effort = await select({ + message: 'Select effort level', + options: levels.map((level) => ({ value: level, label: level })), + initialValue: initialEffort, + }); + if (isCancel(effort)) { + log.info('Login cancelled.'); + return undefined; + } + return { alias: selected, effort }; + } + + async function promptModelSelectionForOpenPlatform( + models: readonly ManagedKimiCodeModelInfo[], + platform: OpenPlatformDefinition, + ): Promise<{ model: ManagedKimiCodeModelInfo; effort: string } | undefined> { + const modelDict: Record = {}; + for (const m of models) { + modelDict[`${platform.id}/${m.id}`] = managedModelToAlias(platform.id, m); + } + const selection = await selectModelAndEffort(modelDict); + if (selection === undefined) return undefined; + const model = models.find((m) => `${platform.id}/${m.id}` === selection.alias); + return model === undefined ? undefined : { model, effort: selection.effort }; + } + + async function promptModelSelectionForCatalog( + providerId: string, + models: readonly CatalogModel[], + ): Promise<{ model: CatalogModel; effort: string } | undefined> { + const modelDict: Record = {}; + for (const m of models) { + modelDict[`${providerId}/${m.id}`] = catalogModelToAlias(providerId, m); + } + const selection = await selectModelAndEffort(modelDict); + if (selection === undefined) return undefined; + const model = models.find((m) => `${providerId}/${m.id}` === selection.alias); + return model === undefined ? undefined : { model, effort: selection.effort }; + } + + const ui: TerminalLoginUi = { + harness, + get cancelInFlight() { + return cancelInFlight; + }, + set cancelInFlight(value: (() => void) | undefined) { + cancelInFlight = value; + }, + openBrowser(url: string): void { + openUrl(url); + }, + showStatus, + showError(message: string): void { + log.error(message); + }, + showLoginProgressSpinner, + showLoginAuthorizationPrompt, + promptPlatformSelection, + promptApiKey, + promptModelSelectionForOpenPlatform, + promptModelSelectionForCatalog, + async refreshConfigAfterLogin() { + // No live TUI session to refresh: the config on disk is already current. + }, + track() { + // No CLI telemetry sink yet. Success is reported by runLogin's return + // value, never inferred from a telemetry call. + }, + }; + return ui; +} diff --git a/apps/pythinker-code/src/cli/run-prompt.ts b/apps/pythinker-code/src/cli/run-prompt.ts index 4d8c1f2d..47ab0077 100644 --- a/apps/pythinker-code/src/cli/run-prompt.ts +++ b/apps/pythinker-code/src/cli/run-prompt.ts @@ -605,6 +605,7 @@ function runPromptTurn( case 'turn.started': case 'turn.step.completed': case 'warning': + case 'workflow.warning': return; } }); diff --git a/apps/pythinker-code/src/cli/sub/login-flow.ts b/apps/pythinker-code/src/cli/sub/login-flow.ts index e7308cf5..b2993cb9 100644 --- a/apps/pythinker-code/src/cli/sub/login-flow.ts +++ b/apps/pythinker-code/src/cli/sub/login-flow.ts @@ -1,65 +1,65 @@ /** - * Shared device-code login flow used by both `pythinker login` (top-level - * subcommand) and `pythinker acp --login` (the first-class ACP terminal-auth - * entry point). Exiting the process is part of the contract — callers - * MUST treat the returned promise as `Promise`. + * Shared login flow used by both `pythinker login` (top-level subcommand) and + * `pythinker acp --login` (the first-class ACP terminal-auth entry point). + * Exiting the process is part of the contract — callers MUST treat the + * returned promise as `Promise`. + * + * The terminal `LoginUi` renderer (`#/auth/terminal-login-ui`) shows the same + * provider picker as the TUI's `/login`. `--provider ` skips the + * picker; anything that does not match a known provider fails loudly rather + * than falling back to a default. Without a TTY the flow refuses to guess and + * exits non-zero. */ -import { createPythinkerHarness } from '@pythoughts/pythinker-code-sdk'; +import { createPythinkerHarness, runLogin } from '@pythoughts/pythinker-code-sdk'; +import { createTerminalLoginUi, UnknownProviderError } from '#/auth/terminal-login-ui'; import { writeAndDrain } from '#/cli/output'; import { createPythinkerCodeHostIdentity } from '#/cli/version'; -import { openUrl } from '#/utils/open-url'; -export async function runLoginFlow(): Promise { +export interface LoginFlowOptions { + /** `--provider ` — resolve and skip the interactive picker. */ + readonly provider?: string | undefined; +} + +export async function runLoginFlow(options: LoginFlowOptions = {}): Promise { + if (!process.stdin.isTTY) { + try { + await writeAndDrain( + process.stderr, + 'Login requires an interactive terminal.\n', + ); + } finally { + process.exit(1); + } + } + const identity = createPythinkerCodeHostIdentity(); const harness = createPythinkerHarness({ identity, uiMode: 'cli', }); - const controller = new AbortController(); + const ui = createTerminalLoginUi(harness, { provider: options.provider }); + // Ctrl-C cancels whatever is in flight (catalog fetch, OAuth poll). While a + // clack prompt is active, clack handles the signal itself and resolves the + // prompt with the cancel symbol — cancelInFlight is unset then, so this is + // a no-op and the prompt's own cancel path decides the outcome. process.once('SIGINT', () => { - controller.abort(); + ui.cancelInFlight?.(); }); try { - const result = await harness.auth.login(undefined, { - signal: controller.signal, - onDeviceCode: (data) => { - const url = data.verificationUriComplete || data.verificationUri; - // Print the manual fallback before attempting to open the user's - // browser so headless/browser-opener failures never hide the URL - // and code needed to complete login. - process.stderr.write( - [ - '', - `Opening browser for Pythinker device login: ${url}`, - `If the browser did not open, paste the URL above and enter code: ${data.userCode}`, - data.expiresIn !== null && data.expiresIn !== undefined - ? `Code expires in ${data.expiresIn}s.` - : undefined, - 'Waiting for authorization to complete...', - '', - ] - .filter((line): line is string => line !== undefined) - .join('\n'), - ); - try { - openUrl(url); - } catch { - // Best effort only: the manual fallback has already been printed. - } - }, - }); - // Flush before exit so the confirmation survives process.exit. + const loggedIn = await runLogin(ui); + // Flush before exit so clack's final frame survives process.exit. try { - await writeAndDrain(process.stderr, `Logged in to ${result.providerName}.\n`); + await writeAndDrain(process.stderr, ''); } finally { - process.exit(0); + process.exit(loggedIn ? 0 : 1); } } catch (error) { - const message = controller.signal.aborted - ? 'Login cancelled.\n' - : `Login failed: ${error instanceof Error ? error.message : String(error)}\n`; + const message = + error instanceof UnknownProviderError + ? `Unknown provider "${error.input}"\nValid provider ids: ${error.validIds.join(', ')}\n` + : `Login failed: ${error instanceof Error ? error.message : String(error)}\n`; try { await writeAndDrain(process.stderr, message); } finally { diff --git a/apps/pythinker-code/src/cli/sub/login.ts b/apps/pythinker-code/src/cli/sub/login.ts index 0c3efd07..642bc54e 100644 --- a/apps/pythinker-code/src/cli/sub/login.ts +++ b/apps/pythinker-code/src/cli/sub/login.ts @@ -1,5 +1,8 @@ /** - * `pythinker login` — drive the OAuth device-code flow non-interactively. + * `pythinker login` — interactive multi-provider login. The action drives the + * same provider picker the TUI's `/login` offers (OAuth and API-key flows, + * plus every catalog provider). `--provider ` skips the picker. + * * The `authMethods.terminal-auth.args=['login']` (legacy `_meta` path) * advertised by the ACP server points clients at this entry point. The * first-class ACP `args=['--login']` path enters the same flow via @@ -13,8 +16,12 @@ import { runLoginFlow } from './login-flow'; export function registerLoginCommand(parent: Command): void { parent .command('login') - .description('Authenticate with Pythinker Code CLI via the device-code flow.') - .action(async () => { - await runLoginFlow(); + .description('Log in to a model provider.') + .option( + '-p, --provider ', + 'provider id or name to log in to (skips provider selection)', + ) + .action(async (opts: { provider?: string }) => { + await runLoginFlow({ provider: opts.provider }); }); } diff --git a/apps/pythinker-code/src/generated/dashboard-web-asset.ts b/apps/pythinker-code/src/generated/dashboard-web-asset.ts index 738d2ad4..0664559f 100644 --- a/apps/pythinker-code/src/generated/dashboard-web-asset.ts +++ b/apps/pythinker-code/src/generated/dashboard-web-asset.ts @@ -1,2 +1,2 @@ // GENERATED by scripts/build-dashboard-asset.mjs — do not edit. -export const DASHBOARD_WEB_GZIP_B64 = "H4sIAAAAAAACE9y9a1vjONYo+n1+hfGuk7YHJZVAXR1UDA0FBQ0Ut7o1zWaEoyQuHDtty4EUyfz2/ayli2Un0N3zznvOPm899RBLWrovSUvrpo2VXhqK6Zg7QzGK3/1tA36cmCUD6vLEffc3x9kYctaDD8fZGHHBnHDIspwL6n662G2+cZ3ndmLCRpy6k4jfjdNMuE6YJoIngrp3UU8MaY9PopA3MUCcKIlExOJmHrKY0w5xdL5mPxI0TCc8W1p8mMZp1szDIR9xq4o4GgyF02PZ7dJcAsCbmNfK9L/aN+1eZ63MISIR83fjqRhGyS3PnB7Lhzcpy3obz2WSBFtpNp2TjDfHLEpE4GQ8T+MJd3I24T0Hq3K8NHPyaS74yBlnvM8znoTcd1jSc3IusBj812OCNWWWG95PM+6ccRYKZzjtZUzwnDgiddgkjXoOc/oxy4dO2oc6nLssTQZlQWMWcyF4y/mFj4UjomQKOZNUODdxGt46GU96PGs5zabqRB5m0Vi8UyV4/SIJRZQmjuc7D6ZYkU2tkONMWOZMHOrEacjic5FmbMBbAy72BR95P5nhamGPfvK7tazCoVaMA0VR6vyEk/eTM5vpCJjHnyqQjrPpTGoxgXMXJb30rjViIhwe8V7EvJ88Odx500aUwMEq/J98CcvzWlFQvGrGQkqg2mN3ppeGxYgnoqU/3sccwzkXW0Jk0U0hOAyInt2fiCOs4Zg7IbTD8a79yvj+O+XK1tmFq6+576nYjef2bG/EUQIIEVN3nPEwTRIeCtcZZrxP3aEQ4zx4/ryfJiJvDdJ0EHM2jvJWmI7KhfIXSsgFE1Eos4dZmudpFg2ipFKUavAft+B5mOdrm302iuIp3eNRLoK7wVD840W73X3ZbndftdsNlXrAxc8Zi5J89ShN0kWwXpSPYzal+R0bu6oB2KNcTGOeDzkXMvp5ZcE4sGNSd5T2iphXevTOrCHPfwjTJBdORM2Ehhlngqvp9Fzoteu3Mh4fRrnoRn0vajSiVl6MYQ/M7W9PVTbOeJyynuv7GRdFlnT7aebJevqwKZiqfi94Nj3nMQ9Fmm3FsfcT1HaJnasWdfWT76de3+8m/M45KmCm0uTjTc6zCc+8Pn33UNbRgzr6ftT3ei0cA0rdcBjFPeiA65eAI2xMi/V6vHec9njuj1qCDY5hI6bUPdw//sVtNEbQdwhXW9RopN7In/utVLbC070iD6ayYKVN8uJGZJwHK+253zV7V+H19dD36MO8KwfK6beiRPBBFolpo+H1yhC1UnzSb+HWkfHsJI2jUMJWo2gdBnIhDnxEHIAOFTlvhhnv8QQOuNzd7LWsIHWjJIyLHneDhZwsSZPpKC0W86SjSLhBLTJnI96UuOeS3tyMAszoQ9T3+i0+NrjS4mO60u7qwYGR6va5CIdevwXrjvT8OW4YsE+HKX3g94h9wcN8Tg4YjObzv//9b87fnX/EUciTXJ1UEJPBR/N7ft/MikREI94aZ2mvwNa0vud/czDfdjqeZnhMe6HvHMHhfBIz0U+zUU6c/SRs4fHI+v0ojuDsa6mMF8Mod/K0yELuhGmPO1HuqDb0nAKONTwQj/YvdLTTT4uk50QJJEARh/vb74/P3zv9KOYq2snSVDi9KMNlMpXHalkRYBc04DmMx/6wxLH9iYejuz9UY+scsO7+kHZw5BJ6Ph3dpHEL1oOLA9MSGUvyCDKzuMXlDuD6JFoC2s/YQCbbSJ0SmJ4HKH9EkyKOYb/orVCKdEG70fBG1HVXe4CMt3y6kICxPnFv+dSNEqfvP+DqgHqhzIGDkYMVShEE8P5ycEX7l4Mrf87jnDs92jeLieKiIA/PnsE2kPaDhMBHkJJbPg1GJOP9oG/asNkPoMVknKXjPOjN52bQWruqszQiB6z1Pb+nhfrI8WsOjTuxhv4Ehl7lPxnOZt7JkHZImLYUrlKYG9+KwBJCCvlIv4bTgv8xTv9Pw2M+KgeT30s85iONx4J3+eg/g8cwzCx2fVI8ieQkXZKciywKxfUo7XHXJ/1lpWcpDEDm+qS3JBn2uGKEyaPlyYLfQ+2DZY1LszuW9a4z3nd9Ml7WviIfw+S4Prlfkjzio9T1yXRJUsx+TF2fnC9JYqGIJpGA5G2dHAmeMZFm5Zx9847NAjimFPeC2UwuQ+d4hbrpzXegxDYhIfCO6XajcXy5fTWbHV+6//iHLtC9IjoPpa4u3d08xqXq46r5QR+i/CgtEsF7gUXYyNpXOnPCk98LXvDdNAv5p3GPCW7DmfQzPo5ZyM/FYwDnXCwmzskH+hE702J5Hg0SsoVblh6JHe+YnJKv/gPgegs3F3pMMKDml57KYMb7Od2S3wU2M6NfZ7Mf8x3IJlIYiVaU48LfTkfjNIEt6WFO7PRctZGaNh6TU1w+i4PfaFhxZnAbjeMVnC9fDLP0znmfZTD7gt3y3GGJIzPD6gWCmcN1KWI3Mc/hGifb7aQZXAL1ENwNo3DoyPl4uoiW63ft/rdqI+9BIoEBdXVHXb86AP1ylq0xUMNfK9bCCF2ya+V3fWseb2Gyb8uKqFVpCXX4X59t3P44PbSqAqL3tss5bhgiK2D/pIfkg8e53XefcECQkyLjNSRZaeNe+YVuZRmbtqIcf8tmZxx6ByCf6cMHeQ5uyZ8L+XOOP3MSc43t5ZAPWf7xLjnJ0jHPhFXqFtejASUf0K/QaXU6LxzKx3gon+KhfFAeygf2ofx1XtKOX7nEbLXJQF1I7ZNTciwH3i+Bf+XWfmTtKGYhHK/IParROG7plsG2VRZxh0VAR07pg0vdwKVtl7gBfKy5moJ3n7mrx61M7iTe80saXD0fEIOHX00jTi+/Xs3lBvaM0+e/PV99PihHLhGVzv1Ri5GUgsDmHfdcaAGQUcFpS6TnIouSgbf+yh4N6El+FwFNfdyCJVjk/kPIcu72i7gfxTHvuYHevVsTFhe8i8kZh9ohVe4N0FWWp0m3x/usiEWgStUNVoVTuKtCO9zNY+C4JF7GScb9wFRP3TFPegBBFIQZs1P/oSynhGs0rMxls4lqLz315+QvlWH6RnSvoAzfJ/+ZIZrPVbichyO5PMgBSbhErZRTPXRdL8VbKJBb/SjhPXc2kzE3aRpzlrg+NF/u1Li6p5yudIDg1geuDzHtLtLFamJSrnpxA5cx4QYYUJMjA0kxuuGZG8jMNxlnt7JnCvUCgzl6ocginQTzO9FCTmeqR2rK6XHrGjiq5MibwpK9HrMp3Kf9ciTm86jvTbkm9hLY/7xjn0w5PYD+u5tuy13FJdL2gwPyxUu4v+l9pa5LpnxFrQrvK51ysxKfceI+azx3/VX3ueuTIy/hWKHrlkgiWLng2Nz3g6Qs7FcOlTQaXsLpV/gmX1e9hMM608TNsVqJMPiYsOm6AaxGGfIfacvqlPs+OW2Ni3wIdfik051yKjdsJqwuBwerbuDCBH/xjn1fX4oOAfiQbxy3Yp4MxLB7yFdX/QN6fHnIr0jKKRMwWAfkkMMYrtIj7wD7nnIYbYkeUd875BSoNk1wHXKb4sLajukhb4UsjgEMq13xDuhxK+H3wvP9Vi9NeNc/oAdyPVTrXl19snaJ2grFKvQKbgeVtqhJOvJgIyvxpisX2ClVe96xTxTtIk+s3GEZR0bzhMXAps4dppjYyLFxPLzEBI676p1Cay4VnSKzX7mbqn3OXSSGzi2f5s6Du6qOQwh6x37rexolnkscmN65G5z6q67fcvb7zjQtnBFniQA6STK5HeaEaQzsL+SG9GU7Mp4Qp8g5UEoMzmknSnLBWa/l+nOzkMptZE+fsnLp48rXO1NXnryXVySB+TIjd0wOAPdt9IetQR9OcpbVJK2u+nOfHJQVnsD5AZW1rs2e2uzow/G4dZ3xvIhF95SeeoDa1f38q/9QydmezWoF4basYzrEFEi/2nv6XyxnrVoObOs2cAW2bcGe+vNaVztmdDVQSx1/Xb3lq3g83r+bPT3jcNtEnKzcZqz4wCZZy0UgxQd0kXJXcgXM+34CpJ69UNSMAPm4AOi5HL5d8nBT3ADtDczKkCUhj4EUh9CI5zkb8OBp8sOsUwVuHfZ6IeokPyiXJtYeHM992NBWVPOA1Q3iBtnCU81AnutdQtU1ztKQ5/mS8VApLT6KakNhp3hukYSsGAzF+/uQjxGCHPtqecyByk5j3sImesf+nFxw+jBi42CP9NPsPQuHgX29grUHi9C6Fp622HgcT+WtgmUD5Azn/pwACodwWa3MtJwnsz7rha2uzn1yOiciRbq9knVZlpJyPJ37s9nl1ZykSTytI9cKksXVax7uh61ttQ+1IJvD78dI0siNK+TRhDvMyaNkEGspoGKyyA0MbnF6icwNb1vw1pbiHdBzIriphF5wDJrbyg4EDc+tgNCJYqTQPoaKjJfghxB1jryYo7THaYphxfugYwhdX28f7r8/vrjeP754f3a8dXh+vfPx+vjjxfWn8/fXH8+uv2ydHcP32fn1xYf33663t44x9WTvbGvnPf2sCvl4dLJ/+P7s+uzT8cX+0Xv6cH2N16Dra3lNCZdNzefWh1aR8yM+SrdZOIQxR65eK4QQXZJjgYvhHCt8wlosfCrLOY8GCYvpYlZ5bwOwOE20XIfW8dc6Oyr4cDHkjq7PGRW5cG64OTbVrBPnphB4vo1ZDixCd/V41QUskKfPB+9hbi5lcBAhnQTr/lRxGoC6SJFPeFrnCyccGMOnkjF86q/EigQ5JSn3NUkMzGD9fX2d87hvB5G/aCKAX9ZonMJdlOp6ZjPv4DLlV/QU/sp2A/WiB1qRVs21riZVOv5BS5/W9KshYzobcIxq0mzK5YUbzlbCBG13mdhIeZeJ1VV/yi+ZuCrruGRide2qaxU75fOF223CyYEvpxPldNuKq7AEi45peckekeuwyDKeiM9AmgXH1fAaRIhhxllvGzenNjnJ0knU45lE7G3FqVRcgOOWTqbH5LilU60Ke+RaMTyC4zk5tlr8GAYirgB2PMyBctTSAxtFDpZiSFrFkBJBDvxG46CUFeCnwo4yJJEDEe3yABDg4Mpcp5bP/1TOf8KXI8DUQgAmFAJMuaKYD/nGlEsSnQmg0C0EOOSAAHa5TCDxATcLRWKc4CIyg4EXKjuJTLkve1JZQxAx5dg3C6UUEW5NzhnvL24hDwpV5OQjtGI+V8AN5pVYMCCS0A2OZbYo/wykt0aBX3HjB2bzk6VMib4lBg+KDAuaHaKILMAvvFIGJ7IS4GvX2J/1Eu81zylMR2OW8eDUDJfkSJ/KonLBMnFhpAl08cz+3LogX4Hh+7l1Qb92RTZVzK5jD7e6z63zLtwjFbGUcO8rOTCXrAObhDmok1QHC1efRuPA8E++c3+OuhlIuX/n8DPvRwmL4+nDqSnrK+4buQl7pzKCqgSfQMtVf4skF0D9XRc5x7PqjPczng+XHCvqVLOhPIlJRb70UFMZkJ6SUFsIssiuJl/rmSxIBaDL2GZxfMPC26UTXjZSAmGayfn4zqmzSQirxTv8phjgjkkrAgKdigL3Xg1gSYMqkJVWve/3eSiezCxBluSSF4DH+2NBWX3a7z0+u/u9ck73R2MQykQT/oElvfiPpqwOXpu3/STnGeT+Ex2uwVZ6fsimaSH+RCE2YKWEo8e2C5twq+T4OBbRKMpFFD6ZrwSr5D7jvSIESvbJ4VNQtVFbvt+WefrWvNbXVR1araYSfpqE7+8FzxKpMPdHE7wAX2vrsm2zXkYJo/BswrMccridt6211muXCI6XaGHJg+/Sshwxms08MaId0i+F6yAw9q0ILOE9hXxkWBOu74LCCBnVYsfpkyJ30NUDXaDsf5rYPbaGWdyXwxzDMMcwzCUfJ/EfDGzkHZE9eSSe0CPNiDySTM09v8sDoFi67Y2TroT6zulJs/Pu3bsO3K2PLr/zKyCt2ht974KTPd/HKLpHji5PruCCeEK/c0lkIV/Z4ZYoqPCOTEt15cAQkkf50WX7ylY5OsLLjg3nWzclJP32KGQi0JVxOvaQRXGyQume/wAJ9ER1SPWkjX1QBZJjesGhY93vfOO4q+mEtb973/lqx292yFd6dHl6RQ7o6WoHSISjywPZ+Xd97ys58f2DjQveaEAQ+Mz+picHI+EEQOkJ+c7pgR+o6K8EipOxpyVDtVLIiR7Qahm1AdWshHK0+tV5zdNM7Cc9ft/cK781QQkD1N48CY5aUa+514p6QLomJUmRpHeKxNLUz5hnsCiA57SMlVOmthJkftUYWz1qgXRrNS1uOT0oxfPnyExSKko7THAyoCOZ9MdlKMDmYI57yhgYq/fwZ0o75JzKqxJdJ9/oSof8gD8f4M8W/NnRbMCci4toxNOiwqHaLKPlletWw4cxZ9myHHaCzHNo1bE/GoGiseAbboGlmwiEtfRrOKwJjc97tPDu/e6eIhi7uFr28EqFdJZiEqQApHFtTxHK0Yhv0CNMIxaG0D3YiaMMlTYBiETemOz5Fvp1ZbXWov6iFioOIDaRrPwApc7CG/uqcf4PutImGZ/NvAxkTuSOe74s9mFJXxqNX70vxGps88iXEwm5O+QzbXZAwP2SbHHa7HRtWXMpaN5caQcrNcT2/OYW34h5VeSM7ce5z5R874jW83W3OD1Sm85KG+8PPHiQqNNoyN7fep99bJwPiCUlQyd0G4FFgDMnB+icwuB0z01/V7zz2tC/O2o0vuIomY343Exut2Sxfq+Kfx5KII3l561xFqWgkHrIJ1xunBecfucLdQJOQNGLvdc7wcXjtV1wNft7RqjoiPk5pdhX0MId+xJCcolTHAFMhA1Ij4W/p0Wh2O1jiRnHFcw4rmAGVNiZz/XuqG9WZpWf4Cqf76lNbW4g9kAK7weIU3OJX3fcGtrDSl/vKleJQ+9X7s+7NX73kWSfbw9ZkvAY1rOcvGdSNaSaTBJBn3FUbFvrqo9OK000e/5XTqp1JqAFlwtViic1q+RgVgF3vF85adtKMb/K8+Ez3bFVyo8WVoc/J3v+3Ird78X8RKEPfUnsFL1LmeSOnXyY3pmEF3bCMRwEsUlbt9Mk9zhKBnLurJRPOc9+BrOTKBmYvGs2hBSJLF4zj/yHo+qamNvZUI1oN2MjflYhw4/8h/a7o9mss/Zy42izKmlwq5kcpXLljFOgkSdA0AH7VdxxnjhtJCI7ay+JA9miZOD0IaeD1jjOMBoMkV5kCQA5/XEO5CRIP5W6Pu+5fhBz2t442jxiYtjqx2maeR2+/vzID15WejPgYlvygE7sNb/khNyu5EsqF+wjo3WyrVQGOlJlYE3+rAdyH1xXugNaqWSPbs/1nrdN93DfM5LMkuexTYEBZNWe8d8LnosTMH6ym7pFV9pVwCL5EomhQYCyxYDdqs1Hj7QZf17In5dBteVHdN1q+ZHd8r2nWq4vGEvQjuyRE7N1L5wmVf2bE5usOlF73aZ3Qk9aPR6zKTmhFqRS/Gg02hsnm9/56knwnfsBEN6k7Lva5ZsdW79jLbjgdO1lRefjJcR12q/XX7/ovFlbt5NeYBJ/UZvmC05f8nVNhV5werJ6wckRfYh6wXR1lejFFuyRyskTHBGzbwcnpHr2BBecGEIkaHbm5OTdd77pWcQsPSGRd0+OfAKHBtUnwhEeMfcgRf6w6ZVncPABKA04L06a37nvAyFuFXYhqZsjn/yYzb7NZt5SGsX3yVF1xodpEfe+RTzu0a/cTrnL2Hjp9iMXy3Z3UWj02GJ5RChZQUOwrRinvk/GKaJuYl0JY+tKmMCVMIEr4ai8eQu8eY+qN+9B7XIdiz9ho9FLR//TLtfMGslE6bQzo9Meiy4rddqBV9G1rtdjOan3pdGZ1Mbu8clzPDzy5+7quIsCiLrUwn+4X6XuJssG+eUVdVd5Av3/dLZvRKheKYPoXPnG3GJK17rTheK609VVH0ps/LkSp1daa8U9ipKoH/GeFh9Cw53/5a6OV92uM4nySDju6v2qCycajm6/iGNHES6gVAxaMRCfpElzpAvr8YnDk0mUAY2TCMyMGeXASHzo9ZQ9gDPk8bhfxM4dy5IoGeQt1+YkKDXYlD70god+UJDM1va2RaOR93JtDSibnaAg20FBDoOCjIKCfA0Kch4U5Cgo5mQctEk/Sno7H4/AxEzJz/pPWCKU097zxuSeTOXUn9P1haloNMphXr8qlWbtWHnZqwtA+qhte65OBOSTuO7qOdGSp2BMQHzHooRn+0k/De5JNBpLsQ3urcFUUrYjmvxXpetlhwfQYVwXY5DUgl2l1vZyXYuCvi91TPTyuV9q0HYfuO7cLLDW9fXOx6N/VwcgJbE2jjzBqSo3Y2y1WjFPTtLa0klaU5MEKhn3s9l9K0l7/GI6BrFRp9GohN/Wwp2OX0XKtbdvfSPkkxiERO/Uh5231Y+LfAisW6v1em8ZtS7IlKatMR4aUd8btS4kxZy2xnSNaBs9Z2zRLgBzjwBTkrZ6rT6wWKCm0t62NlCav1NOYqPh3W969/TetjMk93Rxujcfn+dAjqsf3Os291rbWKOv24NmhDvH55W+L2sO5N2BG6TKCOLFWi8sbSQ7573hX923mKX4rPEDYsk5HXhTUuktWNOYjKXZp9XxMlb1lHwrc2DPDP1q56qkqJxdUFKV5sPuJvT13BsTUxZMHIxtyCsFldG6/ger/cE5Kdu3Taq1fpv7gawSLZPVAH/1xn+hCJKkULVpJQYrDZQA6jZeIiHM3RGa7v6ZGfSru4yhnu81HwGyyamVar/y23RNz/MAgWpz3IVuH9W7PbW6/VdR4C8PCl7r7zWNq9pj8Bxk7H8Oz58ao393CeDwHHpjMn0cL/5bB0hqBRhgaTxuwWKyAq2i57+5EEnVRrsspmbMbZdTy6IKikZswM+z8JwLa4zKyOooWcCV/NEPntezQ9yS3AiqMiPHpsyHwUoWCbCwNAHd/trSvP/j9TWC9cWsbtg4CKcnA2UIvV43Iaz78R9alXPNipStkatLMSN202x0xsEzjX0GAWSmIc3N7wY9gPSkMVpeGyJzFN+rXDmYs41qUl9FQBrxxYcKnEqu5y/ypaIPzPshzS2FGAntyRIWZLdKATqy7j2HI3nvicy9Z2Aui90I7j/lFUlCrmhMuL4+e7+1fXG98/7zxcePh+fXe4cff946vP7w8eMv19fv3MI1BqZPg7bCIQ9vd7bf21aPvg80z5/M6CVa9SaSXhxKHl7kGzla4vmk7B2FK58Vnv/xBbgZxhG4yfgfdg/OLXxg6h6cG3zYZd28vAcDx4FEUnRfUMCeriXKVVIV8eStmD92Kxb/3q04pmvdePFWHK+u+uLP34rjP74Vc/tWLP4/vBX3PWMasuKt8NmM124ovHZD4bUbyty60po54ySmODe8xWJUIxEcFRu7sFmiLxBfUPONohtORbeXYmYP7jJskDdetN++AYEgKDrGBt4nvMx7N4xi7nFjPSPAzQuldH0zlhdy074RtA/bJCE666a5qFcY/eA93DrxNmp4hR6nVi9Iqe/nLWT0fSJWqlY6gLDKjVZvbmttWxfjarPWO/9XNGusmgWzukIpr1xFU6/z5o1vzf29Nfdlq/DWK+SRT6Egotu/rDRjTADVSb4Fn5crkxNGRVeJNnMaawSAHUaXKpnOKPWlebUdGbWIfEZzlZ0wQ/zHlIFnGhElBZeiQRAy5lJjl1KayS8pTofyMdjNpBgdyo/LS3TuE1Mtq0SLbkazVh7dgNBqvmxUo76ne7dCKdNYH9OcMJrJ1aLHZQhC5IlpzEQ2ZoKNeRgCT1pnMz2aYJNkIqM5iU3ihE5Mw2Dqhkp1QHd9efEZlPJY8RnU/2jx9d6/9aWFaFzOHYxAHe5tW4+SYOAwZv0pdIrRxJcDh66llJ2h+Zs8ECX+Tm38FWyg19rL2Qx+1l6p39fy95WeUY5HByxFOUC8oloh6BSRvr74upBBj4S9+iQzsOpyYnsJP7H0e/Ltz7tI+fGUi5QPT7tI2fpDFyk7T7tIuX3aRcrhky5SOP9DHylfnvaRkvEn0q9jdB5GPj/uSCXmj3tS2eJPu1L5yh8p9xqNe65zYHUlHGbhV/6425U7Xh7VDq/7XeELfldAG6DR4Je/8qvZjC/3vFJRzdjklueVZ8sarWhW40rSZmgnQp9jVWTX3M9lNZpVZPtleKb3/pZyjgc+25Auwd8l5dXYxVxLSpU9vPNBmai72u7MlbLKHR2vLdBU/JaOLw3PVMoXk6JRS8ZnvJ6A/uhUcSZR28i582oHtGm0bnfd+v6HaahcrrLcw8AMX2WkXKXbrsBuFZjHW9ps5xF4f9U15j4qM+eB3hWltUfXIGClVgIyUKDKKiULNWeuSziFrRGM3XeNeYmHdLDvBlYcrHZZ9WfdPUFrHUQWsN5UN0UAqIf8G382c0GjWzU+5gHk1QYm0AbpoMAWnEJmT/jq0vcwX9iPf606ViFHNPqvSkXIHi3+SwILckIflKOLYKVDwBOnVBIccTFMe/Kb4SJTUqnvHFQZL6q6b8fljmLsgGzl21NIb7+7wMk1h+d3fnnBr4j8kfz4C95sWtTgV48T4T9c8NVVDWayk7IgyUIAcxrcMNB6UH2m5efUfFpXSyFrUAvmq5dCkHwFfVgOvwdEZhbmtqKW0lupzNHpBJxKhK36OPUVZQ2Im49ZCHc8f/McNpOgXVNqwM1OaLeSBC40lWyCng89AbeV70NPEO0Robo7uflk4Aac2koX7oiJIUSu1WrktD2fn3oHsou2SuIhqCRiyil4miCn0mTIKHwzqK92LyjtiLwpjFtXLvUDM1WxarhcXF1Ycuh9wxrm2L4FFBHUkth0FgerJNMsmM2FRLCf+8paFaNFeiKPoV9y8iwt530vVofML7kx6kLmjk3/af5NrCk6pADD25bIopGnnO96z39LPOfvHhOOv+k/97u/5FQ0GuKyc4X71bOUNjsbOmcE6h8f+94/0QkrE//0N13H2zCuMt/5brAE3P2H62+6/yiS2yS9S4J20HZLoeU///bP1V/yVb76TCpj/JqDixfT1YNcojgQynw2+zUvhaUAKTVFY4pdBn4rmLqdQ+0XGQt595F4NWbGlo3Rhx0ueDaKktLqWmqppamoiMel2FDIQf2dPiI59+dwOCsKVnq30c6jvN9Lv1LERYthlzzkXASPljU3WqRnvA++NGyRhIoqHWfJRi5Ee7+TyyuNFHey/df0br4IyMnlFfldcnixrN+luamVGTIqI1SrO1rz/DFUVBm93ykoCBkZChQvwmHVClBF2kqXwFY3BUV9767RuC7dREikW6CDLlUCuZa/VwrvLqWeZBHHV/N5l7WemH/72KXuE4BuV17K1bwPuLCchu1wyY9PM+/J2ogLu6frd/NGA12q9aNBkQG3vNFYjlB/pjjygA5rgiebP/cV0+DJEsHEiGZgPTKh2WUHDTuGjcZEotQNHbbycRwJ759/+6dPzuCya4J4TcwpAzPxjRujPbByc8muWsoXb+492Ua/67PVVSyom2+clWWcXeZ/oYx8dbWLHBBKdTtmM9hNdYnIp2MmsdkhuUlrdrqdDcpAjZDmjQa0foVSaEDXz5tN2bgSosuaTZI3myDdsWAlCwbYh7MZyGs6fi8FjWgFTdrv8tmsAg/j+4nCjokDpr0tuQ4TDmhJuwQ/S98UFYqx0fhkjY+1a6ObrU/0U1mgnUgqhfg++TSXzMayg77ibJQK4riLk8d231itQS+mfHPpBSdwXX9zL/ZiH04KcxpcjysED/IoFEWz9koprb7WeqpaCzXWZLEkhzuvrBT3EC+vKmW9vEogH2OF4mGoSWyA1xcbZ1fpK7p+YMebst7atVSuQwqirUgw01A46qQHhJWOLqWzkKguIBbMkvxtlbbesVth7l2+oaT0YWpbpaDQQZ+MAlyOxZKZA2zpVYpzEPvA2sarRI0BbdiXaqs2Hsf++TdEB2fAE7SaTQYObsiB889Vpt3nrAJyM7lTS+2r3fyPvDGSZzl9QrGYfKkkV5XdydaYLldRJUeVFFvRmoxETS2Z3FWAH9ElJ/spfcoMgJxU0pcp7pMsoo9aBJAflVZYdgSEh/QR0wSyA5nidEA+VYeCi50oh08cDiRHSczkRWcsaNXAaqgpUnUa74yrp/kn4KOTsShN34T042pc2tj8EEC+OoAXM8LL6ylq3QqKWv1h/GN9bbP8DH4ekw9jmQYdO1Tfh8drZYt/Htt8pHfv3tE24WhSub4WrHea3gcAeH44nrX9WVtqTEZ07eUrEkZ07dVa58UL0o/oi87bF+vtF5YT1djioTZeoDsN2EHaJdtT716NJjdK5yqxo7XNVXhNq5Or8AsZfqPDb+p7WueVWvmmiHVVxitTyCtVSmfNlNNZUyWtvVT76MuOUv/vtNeU6v9a+8UbZQvQfqvA3nTearhX628U4Pra61cK8tXLl+sKtLPeab82zeKNtVedN290tTigqua1F2tv3ujKX7x5+Vpv7e23rzsvrRLW366vdV611RjJmVDNWn/z5lVbF/Lq9evXax1Vyvr6y5cvXqxbxbxae9t58VKX8+p1p/3mjTVaKqzPiBdrndevrZHTEbojb16sv3zx0kyIiVA2A+uv3rxuv+2Y6k2EKl+bE5j627XNumIX3IvgQCTqjscobyl+yCFLeK4pHIN7bUWgtkkGrHc8lXq8h8BkCLmjZKDCyJ2/Y9lIhvC1Gcoaur+v9UY/QaNYj9FJ418ZypHamzm9jT3mB96wQSdkWMYN/SAGO2sA5iRWMk1MisFkwA88qORfGZmUmSZ+sKwItrwIgp7yNtuBkEnwk4M8sJGB5QMAZzRvNEHiIxpNQbJ3NJ7NQEK1vgYC1gZgUvuFlLj6myLIy/FOmCRADC/RHu7Gv7z6oDb+VRlTvyGwDWWBv9comqUWOBqpg9pCFqtgkaJ3AL3uzdr+f2lB/9sLWKy+5Ov/pcXb7NTWbG2N1pZkbQUuX3DNTm3BNTvWghOhMj7htB8Zu5RoYwMdB0RmL9GoVh4RPrEcWu7nnuVDSQBTMqbt7nrnXSzVLKRfAZucKrl+CgeryDejApnBprfAwathIxxxNjpi2Cxx2rZ4WKdjubEQRnKSyS4P69tLNUhj8lcrJFza0erVgkWA/j343avEAel4xvEpsqmiS+z0fMjjWFLZ0r0Xz5R/2QktS0S9FHJDec16NydnlLeGUa/HE6WKhne4mA4b/4q77Y24qy9f653mQHixT36nnY2NT93J5acr2iY38NOUKj3X9OzyE96Kr7XEEwqDSEk9faLt7qeNa61W82l1VZZ+R68h412pw3DXilnCG7SpkXbdn8cN+q/f50xubnHoccJI2yeZjMgl3nElEm4vwYIZzRr/8oaNfwmbaYkl4UmyiFcLuxr9l5DOe9V4CL9bnzeZrzLyl+yKLkbNyhU4i4EoeLtu7Y9JKJFdKT8sVmIkz5Vyu3rGWDljOcwY6+YNMeOX7KohYGygAVT4BEY1t9Y5q9QL54QRpFM4ItbwdNjsBCc5lO3FS8ZZ+BKoHVgKJSe48mvb/VqN8+28CTh9YUesI8irikni/4U7+7+3mXMK9KfdNbNlc1oSWItCAEMRlZ4eCpusb9AmJ2sbfPPNBt/0eEm8yIlZXwvKmt4Ea2UxUbnN77XGhp2CuTgIlZU3Vz5BWU7pWm19Lfg81AwHa3nlFXzaUwYo+nLeGlNOhGVyAjGxMj+K5e0lY0kvBca97fG+lYMGo7fmky8cfP2hgHo3uuHZM3d1FJNCmFj0nSdj+4mJ3dYmUDJFFCYFnWipDN/GJhpYGHCDVyl7ZYp0gqXii9DEn3GpIqlSImZSjlh2q2q2LDBxBns85oI7/PILvyImUAgrIAor8G1sBfbGljucXmJdyKA4vJGVtzFLtaoFfKpEgI5M1+iuxJf95Go2iyErRoEGXqlxJgyrSCoixGb3jispvtynrvFQr+rHxLJdxkyyq8CMLt+c05hU27dcW22YGO0DLBI0HvrJ1VM6PUqlp7Muf9c7y1V91o1awfKac/bvqg6VekndqvrS+rq9gEaVaSzCK0MZgXssGUUfhmkk+Rfn+HBggL4j2JiU8ciDNwkgWSnrOEDdEn4ZsSuwn0deeIj+J865IGlYefXmayxX9DjBXwK/q+42G4si4y4RVtsVCNJ7aXjJr+BspO0u3xCaCABvmVkID/V54pJfSbHf7Zie8cH7+7Hn/u/LYKv56zVr/vjtt6Ld3m438Xfnlfx5I4O7Mrgrg2u7u/Cz/loCr7/ekT+7EOzsYupau73dlL87+COB1zpvMHW7LYO77yG43m53ILjzGvPuvpWpuzvbGNzZlcHd3Z2r/38197ffmq128y225ufXWG1bteKVrHZ9V1b7on3192euT0JABtKvoYTNU9rNpXysHxLug0cdHRHKiE5wO24JnoPC0GYfsWKlHXih+gIGr7WwzT0/6ntfx6CpgRuH2luACztKJ7x8llQoH0FVbwmxEraXL24E6sEPxXpTj2WgypMbLC1W8XvlExvqnY5AklqiJdLD9I5n2yznnj6c2uQl+sYBktXF11LdRgMD8CZS0wWK89F65nNefW9VENddjW03SiN7bP67h6Ta7aca/oftPhD6iqWUcR9teby05ew/2/L4kZYfn3vQSNddZXbrL4RFx2olrmUvr5gqK0+vLD7KYnXDaLBVHmUxsU8IL3ph5RiCh6s0q4ZKlX0UYuEFyUZVMG+MknEBAnU8tlw0hLlJ7115XrkZ60Wpay3Jz+Mq6+2PhL6Vp60sFQAhnweoSzU8UQrIGfhDKkNQR5W9blJyvuR5hJxiFgJy3Zybi8xyYTL06cGWOsMzCYOqdoIqQO1nYAXjz0lFg2EIeuSuuzokWQlEhqjK8ES9PAGlO6yVtcrA3CcPA678ei+2I8bKa6mmBXOSi3QMgkcQpNjZeesaheKYxjN5OS/JyyvwFFJeRAtFWq3Usml0Q9TblHgDSOwikNut16LwxnVXoQ57SQ1DU4Wvn/bralyuFKIsChSQVoJB5JP+dn3CqOuae0uj4THVQN5SLdx0RVZwN3D7LM457AhYA+hpMbSjiDc9lMHIErkPYsVgpWORNVFJbQIRJpFQa5KhTz4d0JbrRuMWjNkqurHWdYhLtWHtRh1kwjdpb6pkPwYI46R61Jg+v/wtcX/77cp+YuyjsO+CRrr9y9h6EEmnu7/95q4CLc+y7bTHt4TXtm5anVf+quu4c2v5s8JmkJEhmQBCyRfuXWB5V8yEhyvL1ivEqu25Eqe3zE25hdHhku3ahRQXtepROA1uPvXuuolbGLKB5EthoHIKw4ihFSrV+1Sa665eAAfHD0y6iVsKNkTb0uIGnkFuNDCUgbkn1rDQTLkKynZGMG5DosqKF2Jj3w+YHr0nysv1NSszL1hx7SZ+W2I4XVnJfJJb6Qr1QR6gvUgunZi8nBgfJBGVuZwszTJZMpeT6lwq5IBxnPjLplQq/liUTbiIY2BMU2tQtrRB2ZIGZVaDcEAQvTIzO3h9NeZBK1CTNdWZPdUqh+8/4M6oaYeY6imVHUUFDtAJrcQKP4jJRJ6sCsFQt1aiGuiK6qmUapBizijb3AQLHn3ULe0zs/u8ssJIOeWTcucLZMoCsrD/0LLFkcXJHvoER8diLBWaahDUdkgGe2krvUt4tqO2TKBJUB+mMhK4NmMcrEq8JC5La7qkQlsC8ZPiI0A53EOF/eJ0TtvdfCPWF9EcpA+X8MhjfJlfwU1EscLb3XiD26agORV1mgWy8cv4yhwl8J3zGN/WUXK4ShzNfZI3GkzHqz6d6+SVtlJDlG3QOEWk/gGRbed227G7oAqmdz7gaEM4L8sksr58aX0ak4Vh6UhIJXRQ3Ib8yjcQQDLaxdtn+ji0LidipTRO1AsBhTYK863tFljSVK+w6lxLraQ6AtgXj0rCkvVo4UlYwRNhGwdaO0G8ssxc8e2aj7Trrx7zEayzwbT2XA1y3fe7jDLwUx1TNo/NwMGcwnTG85jK8ag3n4BUGwwz0D4jEQQuSrG6QaqbpNng5eCxhWU3SYwasSVM6EdZLvCNJugGsOxwP4qZigUenlacRx7YgwyrEbeuqFb7lD7/r2PNOPJclkQjFDbto+lRlCYoo3JYDq9OnUGMc5NmPZ7tg+OJj4XIubBjzuEibUd8iXpi6Nyk97sxv9e/e1lajCHwMesBH1mGwzQuRqpC+Z07fcjVl1nu8ONEO+CEwPkwi5Jb/DzmA2biP0L9ziCLelsZZ/hxlt7p3/dJT3+ej1livsFxIQa2sXbrU+eQIZNJBTEfOMj6wtELQAzKnDEbjfHrg4xMxyyMxNTBoXHSbDxkSe7kIYvBuegN+PBw7qJeepc7P9B7ofMjTUdg1x9/VFnBKWhPB+CuUH5n6S3fYflQvlFYhtN+H2ZIRhyBQVkcjSIdUc0vp+oo/bH1CBYcpT9+VhNZfsq5O0p/HJpej/LHShjlmHuU/wp9kyGcWflp5lAG5SzKbzPtMqgmfpTvldNlB3CKZMQZFr9nzfcXfnMbicfaKFN1R7/wm19MSPbVAFSwV8ZuWzhsx+QqpMtUn9j3Mmg6WUapjsoIM8SuUlV2Hde3zGwmYfWmL0rLhmbTRUl/N64ZIsbWK7azWSxfOGWbyFopr/jEdf0ACYF+nDIBRGKY57vwTV03gAsi/C7mi33tsSVeMVSErKc9m/06hoPZE/7mI2XHsmgPqQZlECKj4lV3fG8dEPdLzy9VtyitLWsb/it1NKCFL7DiiSEszcNR4Fcj9lfiOhHBfENdNhpiWarHFmegNkQMh5Y9MbQMhrb0eZGjmw+fUXGZX5GFanO/0QCCCOywGw3EiJww5QVHl5HJMhYyZ77KkhFxmV3ZYsHCuD0oe4QdanZKHkDNVIolSSpdFDbvR5qzF6ZxmjWVqbHmBKaJaPZZuBBu5lm4EFdk0UIcvj0gFqLxoiIjR1EOzyQ2B/F0PNRMupW6Ds1KW97UD+SpeMTG3uWly0J4mnJ7yDK4UxAVboYq4opcukMxinfhDU/wHK1ixPj970U0cQl+NzkGIMlybuQSGUplCFKh9ETIc0NVlojmUIYRII4G6EnkZ5ZzOGMASsc1b3QkgmbsJgrBmxDAYADHChM15Pkw6kNNOtzMMQJbysamISEb260I42h8AoZvBD+bY/jWCWdFzHVCBt+YAFO/Dzov4zRm8qlPhQ9RJXY58G4Ewsx8eZ5mX6WavMpK2IBrlDMAZ6igjg+dK5DMxABQLx1FCauMso6qDjJPgOL+mYW3gwz887hERTVvyjgAtE5zQJQojpuKLjDJauAwzQwcnvzb0ERIgkATG1wmWqVicqXYNBG7bBTFmCzXBoZ0IhAfOimHbzthq/e9yIWd3GQyykCJDPypGRAVLJOncVk8BnTSZxCsJKbwiQrq5C8a8zD1rkQ9XMPHaC4kv+VCN0kfs0g7R/2QZtEP0B2IDWxapjaHZfKy3J/hsavwkbwTnYirHQra6k2+wmqH7ybrTZr3ZZpc7mWyXPAKAv242fiIETV8jLkQPDuHmUUYGW7mKgJBYIiiZKBxRYctdBmhLsN7xFL53eQKPWXwKLKSRpGdhKRumYiuvjEZ1N1gOUgKBte1jmqOdZwNeDGMwtuE57kNKUwkgI7BhAFpQJfIQBOJZ5WYpDlvdjBJfWJ8iruC1AmBRBluchkBIGZQ9/Ea5JIyBrcUhYD5kI0rU4IRtSkBKlyPNXxb42wR6DrRXpQgUrrlQIwUg6E1bJX46thVkuwBrOapjmLtbiCh01ve7JmoKpi8MlThVFwJCJRoyMYlVKwiqiDwAHsVBmNKoPJKUoKNyrgS0B5JhKqNpb6/lAB3GIRkuPNuJeEQZwkCTSZDOnGHh2mmzyME6JUxGshGBYSpYgI6cYGT1RzrJqZpne3o1a22UkxcdbpNtD3VJWx1moskAg9nP0e9CKEw1LyBoJV8xpIBt9IzDCsAkZ/w7P1IJou8OYZlIwmFyVY8HrIbLqLQJe6kycogJn9gyUCOzKQ5VN+YsN/j6SBj46HKGFlhBACNMQ73MLnJTpojOwJB0I2dfCwQIDDY5DIsATKx1Zt8w8RM4L47NSnltouJlV23TP9WS5f579KsV263EKpstndZBHsr+hshOtRE/zqQfD+Kk/xrHCW3LpGB4B5DmGioqntDU135ZHdMn//vS9DuALWOot3u7DpXf/9++Vv2W/KbuPo7M1+TJXG5+QrNV2a+IvM1Nl/CfAXPI8v5d2TrhyjtDxAE+pvudzZhUlYcyKsU0MnyOvWT9Jo3ZLlzA/ZgvOcwx4J3Pp0dOgyeUcl5WIBRlwOejFmB/hR/8t3AUpDcFdoBfFHUjLiyiu4kqnFlA47ytzwLjTROqj4Caz3NMp6PU1QW/pRreZ1yHfFIKjD4bJ7apq3VFsinEO8Tyd6dJrUWTi3xPuq6oZYZVihKPTLfcPhAYVD5yuGBvkXZoERITU15sZIaAODQAqV7sWQnkrjCkVwMKrFCmbBtIlDCH6MwALmc+KXiS60CNFIpb8fgNDC2dRH9mFbCEihu/V7wbCp512m2FcfeT9iDSylo+mn1I2KX8Fd/cq8usUpV4dVP0Jp2V5R8f6G10BmNL8WVVtjhjQYD30cj5IzCh9EpsAYXhEV5nefbBp5v4TGSq3HMqwO3ENTjmNfHMZfjmMtxnKM3vCXNly0ntfY2GsPQY75+5UrqlMBJwzLO3AAZ9MtnWpk16zySw+8GMI0S3HApUNyyshK3RkUsonEMLBPQ35KX3rSouLS4sVkqaWEUIZGt0wVg2zGFpVLkMKMfjDnBxNq7T4ycYqo/QW6b5J5P7hOUNdwnhNNpAuvJLK0Qee2+75eDye3BhKVW008oWFUXv1TctBWv6i9NwqxUMIUtg4wpA6wzq1SoJZkm23EU3ipmgwppJUsduZMWNzGvAlpxdfCjFN4nTu+SxZiloEfpZEnMUtBP43p4Kdh7IJ7dwGN0hRnJko++o5QJN6MrHu4QN4UQKfAQeamjpAIKIVXIoLQPKhwrzCBvqTC/0gGPW7zuk6xk6MUVf8HV9by23iGCGK250rGgdJQmqHFjLA+IirtipS6v1UGeSlMPUKjjAvVESIgrKOp7z4Svl0YGz9p2lysTZQzuL3keTcDzRU17CQprgyKSqpj1eniv0ZrtngvnsksyRjJmoKSw/glApZ6CTZVPZ0qzedKX5yyZRLXT7DyUnoEnkZ6RSYRLBpwo9QuwT9QvrjKSU6X6ECXOON4cqz0oGMcV4Rg45FSrWCqcg5YxTDBolVKaX/Ir1DXuSoOuuClPE0Y7XbZBhwAYN5kEzZrsqssA2DSP5kqpk5POBtvsNLXLc9sJamTr4N3yKSjUqCJcrWAD3QCTDHQZKaOk+XWjIdXR8VDvrINaCpUWFp22jiPraxsoG0fITR5YBkPTyCiGrVjR38My2lJhylBFqLTu84yuBfjgifKWNFRAzy8xkTGSKtpPckFzGYVHK5PfCYpa5DvlmUrGDHQoQ8q/04WMlBih+McT4B9zf0EVcOKjpJRfTq6wjMvJFY03Yy/zg+xyclUaCUL5Ub4jF/xJhrdz3qNeps81E6eEwosJQaY8Omh9g5WOvzmNgu8hUcVDs9iASefo6XjMe1QlatOAc8/y1kAexrJs1ayqm6MoX2hB6dWpPqBdlBi3quVt1iO8UjZid2WFusoDlYul2L1c6fjkscGbRr5UIbT6HdReInu0qbV8mwsxdmOla4qfi5ubeKG1dhqoGDwxGdjgMXiqzyuDPScRXEVz3L1EMI3Q+AGa/zmmD9jdkyHLedAmN1hPHrSJrBc1MttERCNw4zIal8XaCnYmeTaDV4fVY8RkAcPaJMovsiLH7zm5iWgmvM/g7o7Rc+9hTj7H5GES8bugTXpcsCgO2nOfPBsDXMp80ivIsCAhI+eRzJAy8pCHGefJ16BN5Nc3aD166vxqviBuzAb8q/pFGJHFv/Ap5AP2vPxksfoYccHk14CDHwrwPJ6h27hgXBB5OOOAwQcMWMZj8CYtl/fycaqAlKZjvNXP0pE695B8LW9doAmYqu+gAhjUyoOX3ieY8nVJ7a5JlBswb5XQHmiWhKAaA3+4uaOAGyA8/NxNr1dAs+RIN0OmP8mwjP9Wxn/zg2FBewUFPKIcJs4v2/ftqfZ9q7fvWzAs4NB+HwIWnEc++TKWk38ekQcwK8A3G/o8Q2TZR2T5MvbJybjEkerkANyoALgTeGB3UCKfUdiAjT9oEx6zcc57+Npim4xzXvTMbEApYgCl8IFPYqsUkNLcpCzr7YBjyiWdrQDoDldzKeqjEgnDkGCN8cAnzKoRPWBCg45xlNjAJ9GAPrzPw8B9n4dszF0CnBd+w7LAdVxyyPsicLeyLL2DT5d8Gqvgp7FLzoCBosJnkq8CRLKKQQqa7PAYnIqB0rRLvkRJ4H48d8kRT4pAe1SFgEu2xuO8FnUeZik8yyZ/D9Pw1iVH6Y+TLErQWAsWnvspifAVLHD5785JPqAPbwIXpELoYdIlbwP3gt24pLMWuNvwXrhLOuuBK2ls0nkVuEoQ13kt68/S2CWdN4G7FUPs28A9YUXOXbLWDsCAK5ctWXtdDtr6Gg7X+jrADoDMJ+sv5LcchvWXUGPPJeuvAvdDCiKU9deVkV1/Y43s+tvqsL5oVwb1xcvA3U9yDlKBF6/K8e1AH3c78LEeuLtr8PEicHfX4eNl4O6+gI9Xgbv7Ej5eB+7uK/h4E7i7r+HjbeDuvoGhagfu7lv46ECBbfjCoqHsNSi7A4W/eBG4x8VIjkcHWmVP1drai8CFdzncOSkG9GErFoErd06XqIEOXLW/Ak4IFrhqQ3UJTkrg6k3XteyoskFJQS4crMZ1f31D3lyMAnelHqfFAEzqNldWgAquqrAXpRlBNpC39UG5X8ALgva6RUH9LZ/qxkWDSwyDoSX8amc/VaQ1tqbz8qhUe+stn44zYDYDKYyEM1FErcLfQGqg446/rQhlj6OytlVGD7AG2mDFFWN3M1ftg1zg0LPSLFAEJMCvDtokTkNJ2Pz58zDjY86EyovkwbITUhP3j1AMC8OAYwBUgWr1H+Z7rOum31DY3TAKh3+tCX+5krlPQtyU04FP+oPybFKysn0gfFB6ErSJ5E3DWQJ1FhlSVyA1wOf+TqzIKEb6BX6BWhF3QNW1iSoUuKhIT51k0YhlU9z8t3Hz7w980rNwWaRFOESaTt5HLkw4BNkC75URS2Z7KVYsmfC5T4Y4Cr2BT0bW0TRWl5k/eaKOsZDRwCcDayh7PBZsKV0jU/QZquDcuyHn8Y6V1OQtKw5QA0G/PVrkt0qR3+wivy0psgKwJN3U+CvStrFgRxJ7fDLBHg8GPrm3hi3hd3Jo2ySNe+pz7pMpQt8PfHIzoJdvSWedrL0m62tXZFDQZ6LRcNHvpZR74cYJrVGs+75049ZFOM13OVK3ch0G5zWMGq6MDSa5B+cDWdEFvxe1GhqNlT4jFyECeCuDYjbrs0bjzQb87XTe0T7zyceQwpl6Fla4o9fhModIasFpG8CbgVGXEnoJgksH5Q/ILFnjaUgDgYOctbcGSK54pcgEZG6v5Ar207DI08IYHq7UHXGt2H6BtsKa7EReXMiie3sYcovcM07L8fC5SSqD8X2wdDDCcmpBx0D3cgt4uvW+mRHAPRBeCVlTLyPAwLfJx9AvWeL7UgBiuiGweXgkfQwbjbNQvUlQd0mG7S8H4/3AaFvfJL71VkO95bPZyqBoNNScwxkIPDHglPULqvlnOCaE+/IF2dpQjFkuuGu3oz4A8oG1ltrAwBu/3N7gS+1voHCoAUDlUAJIlUdkTzUanQ35pfXbzbBCJB77coB1wrJjW4PMF1r7yIRehNAaeb4CVXGbqlct5LwsnwVAo+MBfUCVCTDeBH9G+heu6PDt6kATS3chjo/gpr3SJqM0EUP4kHql8AXMVBDUwjdKt+Ej5ywLEVDwWP7cC/xVtRQZRt9xfhustC0C78g209WPaUkfteV31ULX0H0lG3xzZeUYCBxYY1dSjdYwwUtkvLNMHO6TzSn8l961mB9ME3rJroL7BNh3NMxBJTdNtvFAdH3SNg4akAEHItmbyCshiBuqD8RUqAMcXkHhkqESxCTWDlMCMfelX4eecqM5ZDWO8DYSvuOhx4nNUf1ucVTR0Qa6XIbV7i96wPkRmsUnl5zqjCFCoZydsGSoY8GTwg7dF9RNEznK1pEgn28ulC+skC5n2XtuL5q4fvdTWLWANyWCZhD6EHB9cl9oi65PYUsB2BbM80lB7wupZjtBKcBOSCcFnCpLj6bZ7O3G8jOrHKEPwAvuAaOjx3CfDod4fkELNZmi5/Uw9GGa1IzZLw2E5kJgkzYw4pJP32h8j7wh8/XEXV5170JPkCEjnKCo3Sc3obc9AFchll39wPjhkg+Eh0UeJe6mB60mPUahBBqTHmsx8UdNB+a5LiRF43Yopazs46B8rUbLk6I0USUowZI8essAnpEanWQfyxLPBjXsQ6GcBW37RbmuAVekWzW8rWXdGtiOGPFwQT2AFVSA7zzHW9Rz4c9mXEqx4VomcX8iNMop4VGUV97/MbHBlmVMPGKmrRP57EZpfm09Y1NqxMtuVIwChJ0qqmJQZeodaz8Ct3wKnsIIq0RIZwFa6o3PolUOJCAeUJwDDtaNcJxp2X5O40uGsv0V7Y1EkNyfzVagT6j4DpZtprD5oiDl51B7Luzibm3ZUPm8alO1uDf9XvGChUV1QURlbAK026W63RWKjflqRdalZWN8A6aevaPaIP4B8gYxkep1gWjyeZdTNueBbHdZB78X5/L9MfAWUInQ4tN5VfdiHit27RzbX3HocRpWcbLREJuImOD9hdtvRqLiy0onAH/I1UhZhlWhH7ih9NGVGw6hCoMZLYRGAK3NRrUC3ObKivdoIph2d16BNX+FG/IN5xbfKNL21xWT1EeijdJExO+UXOkJCMXS7JbOL9H2Ve/Vflc4ETihTkJcUK0PF0eH++i7X50v3dI7eaxXsnomLRFfJL9U8zNaw4z3yzchlIw2VpLw2AcSt5JTvk0jPc3VWzZf9MU5Lf59QkZ7GjF+R0r2Ar5DZfMbJKlViRI8roSLrBrWJJvrKw8mhjYCKNXp970IOa2YDu4gJIFypK52j98N4RK39Jwl50rD5EaJvceK2DmvasHs1Yyq4pbEC3yUMDZFBpWd4O1mHMRV1OqeF7PZeaJ32nPQgRlHykyIniekPNakmnWUOKzRmBYe8zcZfUBl64C1qlCEJz078n3Sm4O6hseqtYN21KOYrlXlfOBLnuui0DfHg1SVRZ031ioDRH5+lDuXTpFBgse4ymK+ZazJYIWAi8IajRHzxozgeIwZZYQBmXtTAJ0r24R0LivpXGHRuQpCD6Kic2HaanSusOhcBhIRLfU+T3zbjdsvsX0CPMyNFsllbZlc1b1JkfjSlfZ67iq/ou6d+haQcJT+kLEj+BBE3qa/J7QU5cCM/gKvIOgIl5TfwLr3iYGNtO3i4zmMeaOdTyLTo3kkAvqkfJkyK2QV5XvjLrECZ0VSAS/LfyTDYg1SgPtUnm2EqGTSY/VIDhytOflegA+0W/SBhoye2z+4EihTQGvYq2yj2cxT7ne+Jy174soAWQJgZutpMBy8MuhXelRvSJm/MipWyDIQ/VW/fvC9uCz9OGIAaa3viRXNlUsfjCRS+SOWhoPIRqjpfoAyHybfhpWCqbiMr0oSC8r8GtJfY8vanCNSf67FRhbi/lJLyxX63A0wvoKn8HRqPVrD79QTQo1Tv4a1FNmog1DbAZL3BXXZTZoJhxX3qK/n3PB+mvGLdDCIuSOLgp+TmE3174W0ynDwfuGEcZrDW+9GtuiE6XjqhIVwehkb4B8wNpe/gmfy6z6S6YecTTh+fZyoNGl93svSsdMrlB0zXkgcPhoLeH2cJ2E2HQv86sFf+bpJKk4ka14p/jl4vDtRMmFx1HNu+RREfPCLXH74+DR24GFG/MNRwqs+QWLW00HZojjN6xWMtPKi/ALdRPn1sRDqA3o1kvqIDnLMnDFIPB14dgf/wGssY10sjrcKYbnqG0tW31C2/oTS1TeUn6UD7Bm8qq3GDJ3GwF8wx885BxV2+FGPwMQxhNHNjKN8KDvAQ5K+uB2UXKhm4TdMJX7IMZmALbiuKkcBpSMk8iAUtvuOoR2Bg7x429C7+76QJ5krs+LmVq7tQ/XG4gFyM6Srz69wbTJeOt9HmgrNODwkjAqTlRulFV8XJ1Z0IG0mcVU5EvNKpbIF927yvF4A9FzESJc8aG2elYo6DzD65KM7wTImdfksok7Vb/SU5PSmco9lkvxAx/gEaw/4XLq300qeUT4GEly20NyhJX+nHI5xloY8z5eMh0pp8VFUGwo7xXOLJGTFYCje34NJMR5gpZMicMSXxsqrPPgJIWfogv99QtvkuIALqXkbFNQJ9VWF0/cJEfS4oAAJGtNdTcmcCVBfhr+rq9LNu1J+fiQhfywhUwlR37PTSOnmJDdPtUsdTqb9tXeHiqWwmeNNluaBp76G+EOGKt4nJhPN58p1/G7oxejs37rSbkcl8/RMXL5PoD2cmE9RfsblJyPHxQy8yKH7+hw/KbecJnPbZ4uGsajE7aKsVh1xVkvIRVRx7nIQV+7dCGle94P1WoPftVzdq8r1bFmN7FqOZZiBsxwGgFQiKx/AyoxPZ/VMvPJQDw5sMqvvlWIrgD7JwHMyeEleQ1XXzDJUKZlJ4AswyqObKI7EtNGZzTx4CBO1vjOS0cw0yIi4scz1TS+zlfbB75LlwSgvHeVDxZXHEMAFDzCHtOb+JoTopbgKmNw6wY0RDhGNZ+Z9DZ9kUmZjMTe1x8CX7Y1D/Z79IaNtsqPcL5Zv17/0fYtHYIZZmGEGvWArYaG7Vl9LAdtxUvHY+2lQIprS1R2AK3b4vOVTrfOrHqmXyig4Z/JT1im/TWW01AnGL/X0PF5hJUai6ibIL2mbqHL6oLmUFGNdbt8CVSsVHbhDn6XSLL40kIQRz2Weypu2MqrA6TsteMGrMLKosgYwrtMKzHlxIzLOd2M2UCX38bOtK445kn5W9hKLZYZYve6BAYP5tIoL92JhjcM5Zs2IhTd1y7RSu3gF34Vd4a0oP5MO7dVzj1b2L6JquFKucfOmg0JsL6b3Qr4rSOD9ils+JRyHxydxZSa5HdIWXVwbfJXIYK+42BoMTqx20NgPQLG5OtH1QvU8xNU5aqPZkj0rvgHm8rfx6mW703ndWSOxPVncCpBYTZvaEzWkBiJxDXt4NWylSwysvbJM4go6cjuEy9jGaOyRjeB6flD6+YANDIRqqOQ4S9o/EC07OIexUmuX6y8Sq8WnfK6QGNcbbCR9+a3XIrcCxELdZ6F+gUeOLlXD+6L7JzDMHnR8AUcOO74IjgOO64rX55gvW7y1MdaR9kCrqMpw6hoMkiLSBNWm2ZhiWqmQxLRVAS1rbm2jqLe/hk4LXamhU61XFWRSL3NTZW4J9nkVZOL/IWSqvOD0MVr2QFJbGrhxW++jpFFxG2s0vCHtqEfQLcmR9XLxkJ5OwCqRmDfI/c21V1KWBw5xtIhsyFlPf4M3XHdz7XXwUpZc2ruqN2a2eKnXcS+89Q4BRl4OjDx7X9viZrIzIi0fnQ86527sqRnPeIJ2MkrhxNkKhvQNyWd0rfJszU6lTth/oM7ZWr3WncVKv1Szri9v7pfFjFm1o523y3Nm1Y5ar9k/dR3yzdOzz55JODW8h8GQdtoVi1HnNhjSt9UozgGuU438DHEvqnExAr4iSkVAS6OGdO0tUe+dA5m03jaU4aYLf11zmwMPWL7Kb0QXMCJDPSCieqIpY2ggBMzAWLKO3XjhwMYBfk3gCSo07I71O2DWOjEOT+08rwg31PnSbF8sDRGYxDcWRd8uZSj2QWu39axeK5byQu9bGdemsptlTHB5pU58Q85Co+w6HkL9XM5+0k8DI4qTYaLO721dILY2Go3lIEuGMm9VI+bKBmdfcsS+cHZ7xMaWPpwoPVc+hZeKuNkPQdhgPRwH79EoA5M48ARV74BzIh/lCQSRr/B+SUG+TvYtNoeReD2dB5u/ncAF+gIu0GfSzpEMgK7fwov1kaBtMlD6XHuCdsgteGEre7mvermdXF7gFXLAiPk+i6BMDgVaU3xibnFb4vIIL8p7gpjvW+t7EEPlXN3x9kSX01uhruHra3DrYX6z02UN+i+vs7GR+yRepR11HZcAwl/F5zTX2xtms8+b+f/zsptRjzUg29BvduzXmdZ8wt69o0OSN+kQe72xURY2izc28hmDgchWuWSASJjMSrJWxDXSwPq6U17ccOg6PsHx6JC2LW3ZKkphPbxtHnX9s4huJ5fN5kVyJUf4yszWsviuyTyIu/4gplvistk8EldydFXmW7E8fm95vPVcV1/O+5+Zwj0Bnhd7UJlogXOjfpze4cTKBaRonTP1OwKzPjJRSHcnIHRUmJ3zZeet7ePx3nq2V4O86LwBK9dsgBKN3EjITMxl58qsrmr0phThBi5Ir13cidXrRhPmXQtPgOk9OSqshyX7ttWsdWcoLcJrJJRWhRTw0hNsf2BmTxnRL5r0IhanAzfoc89V/HhY1RgEfjmErLPajfpZ6dZPP7qBAT66AXsByAmc6HrGCbifUaCsAP8SQeky+ndm+4zuc+93dhlf1UqQ+4qsQXEta20b6WdC0IOX+ka3L9VM5LFGSn3cXIJL7vBCJVL7FQAUux5LHIHWFlMuH1jVaQUzTitY3WkFU9MmXX6AY2m7w8qXRK0uG6R0UVFv0OAPGiRPHx88LZfEmmXhr8jMitW/cURuxalnXEA51vZvrF2Qs1ZejFHJ9sO0J0UkX1iWwF2L0pX2bDYdepWcJPY3PdYap2NYvcYXNnRPSnzKaSHVWQJ+ZZpI2yOdDyAkxx4zlADvk94iDIidNBgKmUpX3K00QSkS3YUzD1hpAW4WYjaDXQGmznoms6931H1e8p72eVcThvvIPFBE4Uv10GFHP6sY4D5kv6HkrL1WQJjW1mmaHt3ndF/XYzFmPxp7pBVK9633QoDZPjIuHrC5uBXCg1Jd6302gq4f4DE70AmXrMC113A7gTh4uE0ZnSvuA10BL+CgS5iN3EYDv5VvCn82282U9kr9nueDH/MV4Eg0Gme80cB9lshmSUN/46C+dt+jvCTRenyIKMZ7kqZa4TXnFOud177fPeP0DB/OM3crfNHuv60G+aAdvLZxxsldrJ99RLcGKeGpOZC4H5yBi3b4u883d4DFZLb4ihqaVHJfVLt7Fnv+A+a2TjdLybkw71VOYvu5SoXkPXPp7QnKg558X7jFxuN46vXgNNInZeWSO5Gv+hnmxSSmYEE3ifXzxHjy/ijosSfzflHH7Ymoe26KNbX21ftRENG6Vjdb6WmnHkFtGiGWhE8V4Eeh78bk1PtRWEt0x9D+knYp31pc5PHDErC5Hg0BZiSiyqWBp2wf5doL+V7JskRV2JIsRD5MIHf7ch+x1vcnSwIiCVXVIiTIcotvr5nQ3O/m1a6CLxCb89GVb4dUhEeKedsFuYHN7OhyPMMtqYZUE6dZN6O5Yczf0Hb3pnzF8GZ1FZQYJi0ligflx8ubK/8hMwIUMqlIQyZlTyalkIXsFJ6WY5AYsJPNZt5QttzchTM6wbUzN+s9V8z/zhtc89A9VYgWjdVX9YuO73eHZesyOrRal5Wty6qtG8pmySbJ+svBhKr1OA/1/ORd4yt6SPPusPIEKDSP+w9D67IPCoo5HRquZSn2M1M+LHuXq0xDEznP6dBCqDPrRRBedTsCzMcMjqW80qaVDH88JQBoyMd/8eVcP4OTSo+6gZBPBUuIsg96TtolzlUW4GNT8+a19CEOXaryCocVJJ5QKW3pToRXFZYoEmmoniMB/0rljo87GLzDQ8GFioVDlNIpN4w3jUclUjzZ3mH1kKmG4AWUp9KVArvdwK8MW/iVXfnz3KDzvNzaPxVwmZByUSHnYUblRFh3x0hTLaWKtl7n1TkHPXC9eqs7rnWyyz271H/Ht/VgIS462NnHzftLTLk+EwivM/sr0mC7cXWbixNuiYGSvvcF1qB1240qcuAv+thqNLAVJJGXTetNt37lke9qf+UDtg+h5gWTSu+DmECPpWARuma9nMIfQZD2G9/vnghk9Vc40ooH3a5xn+eE6wmVa0/i6ImgJ0IK8kXVCdeHgdZE2QKFKmWCH/MM32KrxdV92HBg1Ch7+zwaJCymD6iXBQ8mdUjdS1aZXW4p2qRqPu9KsR9kpbavn5YqDWhRdM33noVDryymfM/P8+f+fE4OBzRpFYl8uPc6D4e8V8R8m8Ux+P8mP1eSj8FTfXySRSk4wCSnnD5oDm1wCN4IcnhNUBJ3J1kKd1YVqsz6kqg1FSeGGWc9fIQisI3ZPpQ+BBBb1PACN+/DgEj7TqXrBuIkld8ShDLN2JGJzSYpA9Il1uHA+3lAKk8XljXJccUxg3mcKn3rQ3C7cg0Mua26G83fS7ubqfHGp1bBlIGlks58mnk+5Ac1aVHkgau2V5dIlqDkfRAx5JZPJAbv5Bh00Kznw2J1FTjMQ554rE9Yv/L0MutLX2jN5mEh+zxlZpffsthdW0lLtgUEKzH4cwcPC9JtGp2yru5+tedGj2DBz6HXJujn0KtYcgzsjeESNB8eGwGsLOMsTxW/tzoWuRmLHJZGqaQAw2BNKFvSK33Hp+Kpx7q8NoGHKXxk39r1oiGQKTbj3/F9KihVNpfm5KnitF83nzDEq7xPj1rn3aPWebmqcZBue3QkPPO0o6hobdW1yCQCVF8vVehI8n6p46S2apzXk1jfL0oU/rm88JzE+rCuX3w2efCRt8bwDEtvm4VD62ZzpM4Kcy/66p3EpCzKDzBCYG7ryCj+D3fvwtxEkrQL/5Wxg1B0fxReSb5gWtQqMGBgwGMw9gwsh3CUrJJbpizZ3W0ZxtL+9i8y65ZV3bI9zO573jgRE4Pct7pnZWU++eTIlQtV6JH4M+JT1OE72Yfo0MLgY5nUFvO9iTc0bgF859IbJzceb6TAPOitlRvdNGV/jvlNOL3SmwXlTRlFMAk9+EwHAbp5ZTyGbkb41k1H9uhkrBlAOqe+Qioyz1JlED+QuxEPN2ZN71ZMG1AgPZsp2VgifdE+HB1ntrZ0uYoY5mMIytFTlI3QUNDknbNFELdpUBGdMRl3430KOOu020/lWplLpT5qqCnKWVlEu/XGNqSwQe5Bt4bc0o/Xr8me5qrkH7Wnt6pXNq3w0mYsW5C1+9e+5hd2aRc2MEQtrHX4PzwUC32jmvCK7dETq0SLlI3MwpS148nY8/Ukcu34QvwA26zhyoQtX39NxWJCNUA/VU109JNqwhXbm6SZCXyv4q3uxOwr1aRZKdvYBDeAXtCVA5GZz1BTCDbfand7E72Grse1WbNuYryvbRiUoHDSFySSeyBscQPxkHfY9cTpq8k1uNNSNh0l1xMWKq0fhZVcSLJ4gWgxrTGgLc+s0yqC3h1ZgaeHz7mvOefP+07MdDdTZEwyMacOZuXcWiZoNGXeLqbFyxc9XCam9utq34zfL9fjKgeUe/nLzerDMJZ1DdIWJKvsl9X04epiNZNBqNBwFHFzHrIzPQK6B/f5oQeZ9PYdwIZc5V/OvrJDq0RDxOO+Fl9ndLdX7ssrITcuWkDO3AFJ14Cd8TN7KO9R5gWvyySHHjN8ZgMdeofuQ4fQAXaPOkOHK15ih2lm/9TQpMOUHfLDuLgzkkBK190SRvA/TCUPHcDQva3n4yEhP4VH2b5/27yzz2Q/gd71Z959W9tkn++buu0/PYPONp279bjT3t7e2mJnabafZo036OVOe2N78zFcJQmwoeN89CypgF8Zte+mtEkz3SS2475z5nCzZ2CIWOF8q5+c8f2rZJ8dIrKP7aTszJpUoPOy5IyXCfRMeN0XM/DFIPkx36e55n95wDl/3T/SD5lF6hwxO2wfKcSyM6/UngVADM4fuJDxB1QQPnBvPKDrV0G06SR5AGnDzqwJ2jRBl56yj6KhQdDQ/XGiK6+rZd/QSAvfQY3vE+vSPfp9A356q3cAnVjhfD+8Ej4cYibw6fASNPjgPmPqBmI+Bwm7pD1Htj3swS0tegyl7ipSKnvwl+fSpV2FXrc548RJd7ais3eTey7fH7lmnHUuRgxm+OrqwzNbtagCvbA0N8XOvJVR6w9nMdDps1Uc9mHu6PnGznDunDXMnX2cO/sgk/Z9+fva9/Vn5qp7cJUsrasFRLmH5SQ5S5ntuQXmoD1L5/NrmZylvgt2Ffkm0x6KZZ1QUy7sZ6CUF2Mobx/NkWd06b2jTz0ba9m7n/aO9M9F4/5wHEuPM78XAOOg5WW3LBV0NuyHs2G/YTbs12eDFRz6eKKF5OrqQ6hCVJCbCPvxRNhfPhEQKs/5g76Xi5kjN/KD7J87aHrOD/A+DPB+ynxH6SHe10O8nza3yy9afVgkLVs6urqIF2MoTnfGfuP46udwhPWTR/pn8whfU/Hhq7FDR3InHMmdhpHcqY8kaAMA8IKu0KzyMMVhMKGsXliWG8ydeDB3lg6mLWDHDBZ2736Gf9oyB1gmFhiO8H1ePohe9sO+A8O+kzLae3rgd/TA76RLu+HIfrU29jtLx96W82IMpere22kcffvks3FyxvSzR/pn8/i/9Svc6oEPDMzK+G93+RkbS37GgQtSq4W7bncfy6f71iYzBrb+m12tc/1zLPtJLvku2zWYbfjDqYfoDZEc5uou2/8yll/NnJ45k/TNrleldnluc5wsZKu122rNZIPKBRrlLqi9RTKTDKqdQjPM6D7gM5mdOnA9n0m4OZMMvg6jN0b2uIg3TelvAk/GG/g+fPMB1HTXVRSV71pP7HKQtLZtbJfoULxIdhuqt0trtwu3dp3ZJiwf88rscpHspvWCc8mvk10Gj/ric+q6aLVy0n3uxq4+jsgkl3RVjGWmL+iezZt6Ng96NseezT20E8asbkd/R0CvySF7p8CEFzfUuWf8RAXR13hE7mx2SCjWnfOYzSTfR/dEkpI5vTIDJpWJxN6kz/yVyf1Omck9M6nezfx+p+43v9+p2+f3O9UwCu8UHYV32PZ3ys1v066/OrOX98dM4hwnTWQzGc7zv7AO7zHXb62Km/TL6iNDsVGf97N43s/IvP+ZljTP+z9mwbz/Y3bbvH8ug4l/mwLUau1bTp3X8Nu3pdXC03B4vnN28L+sTRl6KnugBDrps8A5e2b1J/zLHjoZnjbNA9rV/ji9QZuG7cKU7dDTIKnrjteGD/mOQ1Y4Z/h//GB6a8XwqLCDClZzvbSlxi5phGDpK8Qms/DD1U928HgWH8HdaU2fxKPS0izZ+QvH4ob6Oq0ADBpWRTLDe9fQkmHcaLWWHph57cAcPhyejnn9wLx0JMLz8ZKBQOP6vUZjJz6Wh1+s91T9GEAWqz0HWGnrFS0QsfpsoBcH/rZr8cEtea06m22fzGqfP9AWzv2UvQlKvfMkYWp591HCPLj8LPH3z31gpsPznbcx2Um11U+aB75hGegBZjuxtSx6kMHgpZkKjrxOLjs5C64BtIZrCUdG1fa+tZk/MP6BXRzLXWNw36UG9138yikGE3WfsF26MN33Tm1o0Q47lb7Op9Ll0TMZ+tSED0cJpNXJR/irk7KBCvjKPpjwCxqFeAPpyjUndwwuRSQHZDjXIfTaNaxE/RpkC5bD7Ma4eOyDGg+io/At4qE0aJOT8G/C33dl4lLj8Nqg1kiHmVTLW+J+11ph4Dn+Stwk6PDgvmkeeNzgR1x36tdQ3nmJgJhMskqcYqYe9BrprrEf0H95DA5hy1Uhs1zQF0sTEOJ1YauZfJetrgay2XSXjmXDvFVafcVicbLE/NKcG8xQb1SUbAMYC5AEgSEJhR5sAEJQ7gqhCdYi1oozQdJGgvMnGGGvj8Et2wrV2ug82WhvGNSe7RET3AYBT9JWzQSBz0n0n2ITTI5JfSWvrsJQelqJGrSW4mK5CJ4Fhd1y45oIYc0AoXNJqvpEcxrUcGpwhHqW6JBknCoKge12ugBtJ/5aOmd6hSNM4QXPMwDBaroUZtlAF9f5WMnElR6+UvlXKr0D4tWeogtK3LKgyngBFXbFiPqKEWvu9yKOh7bUNhJDtIOec4p1rVM5ZI/StWfxa4Ys91vI2vhSaHfqtyuLgng26ZkoBHJ0lGTG/CY88FR/zeKZ6bLU0tailuPBBwBm1CpAfeoecgsTDmMEJBve9Qw7Az5jB3yADe/pfwwdt+2ogh9khirngOV8gK8dBbP7yE/tI34UTO0ZP6pVdoVDbsVkZks4qg3FQTazJcav80GKKkiI3b7kpZ9YvZy32RE/4ANzBudFz6yTY66B1a1HlpllnV3zY/AU43VUnPrJULaOQU8/zhJhfqU3x5oZ6Bj+glSyMH7tlEWNx2rrxdjGdTgL1uHsznWY9mSGdX3LJXvDZ71jrsMzn8PMtoE1JK6mA0CQt/yN+7JRgd6GlFCX/K3W5p5LdsmOPXL8kr8No63Xs7eGMcP823q0tbm5/nje6W7rJ9pxkce8qdB+WGL2lh2bQbPlXerkHrQ+JhAog0XQXixgxGxXseMAHGtcnxvsmv693XnSTdk1L72AYNeeC8pd5F+Ov2bX2gN+nJoglmszdMf3GzpSNQKAPXLe9wN+xK/ZgF+mmZsb1yyf82Ncnwayz2YUMFtfy/6+jpG45jM249f63WuyZmO5wK9Z48pfGEEOcXhH/vwO9WRkHfEBK+tLs17KESs4CcEwBRrem5TtqznPXWB7XiO3uCTy8XxUC/C+7bTyBFJhpD1pgRgkl9Uo3JrdsOOWamUHgsDJlDCQbEwTaw2eYO88HyUKCCorE+MzcTE+r8f8t6RNsHinI6t2ThX7lLweQ2TEp+TPCYD6porLeYU9jCqGr+8ngPDh41Nlnv9z4vB3/rnf4bmp4q/HNOJnkrIPyeuxwbxWrnZ/xpFHLxUNrPUS/FOyI9mOA/q3OlCHHxgK9WfFiTplvb6+ci7jdlXD99v0239WnCpvb/H0EBXZXGD05mykzx2WGKyf3P8rafabonT/8NeSSvyoGnp+gNlxPyQ/KvNxqb+se/pDsmMCwHZkNCPeuXgD0++Etktj20xMyLoH3wc9SacshiEqEqHHlBuTN0Wi0vn8PfyT+hQXLv7PlvMETzxBFMka5CMVar8YykInSSiuRTEsV+mwNj4JC+eejzp4upKn4uTHo7/wajU9lVUui1Vt0UhMcEer07UxOPXWou3GaQnmb8dSBuh+E6EElqPx5AoN2DiwRs6iabpyptdwzMx3/OR3F0LYVMXtrYX7lKuEDVnCY4qxEVEfGk4YxdtMGHfCS/PvZ/PvzhiCc19gUqAJ/v9yzNvsObBNHBkDwudTCro7konFvTnUWrdDoWb/uvIyuJaWwU5jHc/uE8IoDyRXOu5NZ1GA6PYvimRRaAjn/PUqZA+yY6l4AQ2vWNXE7FTVmZ0qR7a2t/aaU5LA8H0LiB5m08J0W8FVIoAGRv/5AhS8gv8YJaZeacq+I+azILEKKI2gqANhoolfepP/S32asEsdehNG8rPkLyUXwfgFw1XVImXaiL21jXklIXRredgQhje1WskrTI1MxvXHyJ89hDQMIAB6H05hrLHJphKprpKeV93Np7ysVaqj49FKAGvqNtX5tlaIhl5EZ5wCtYiXo5E8MdpLsYbs6fYADMmhC+n+ghFE2PqKD0J01yy+L13AaFRDVnDMXp4aRefFxJnByPi9OnXw9b2118i7dmVUnyT90v7qsywtxev3DwWgvyuM4wpeZombC/2X0QREPTHVqVgSIT0KsLsRxIHselz/5RjEnK0RLnKCx33gonyr24jlLEPbo257c91qZC3+b+Ks/+PKIHt3xj5GLtyNohhi6LZLHFJqdwn0zZS5oDic8IuGlaDn2nPBsW1mHvp6jSvXFTf1vmTewOD+xDbrPy/9T2rv0H352ckDEcuJz5LL7DP8q5VsyT7T/C1SH/9fyiBqCJpENKsgsL5ec71fQU/oI7ju0eV1ykxdbHJVLPczCMmXsKQ9zj8wQYg6lKHv4y0ep2nmEcwgbV4iJWPc0/E0Jr3+khpKfe+by3r6XdpL+IMemP7KECzcsJEIpXFCjKVWqugB1zLFmJdRomSGM8DIjthieii8ivzcodKfIyr9iKDSjzQqXfLpKDnSuHRWcSFZ4ptTNQ9eygMDpUcWo/TiSxTqYNsKmAk+jwn/RchT7R2TwaFqqePm0GZUk43uGgw6XUQw//Vt5piu6YbzhuSD0cZdWBqR/VlRY63yIh0ZMfyqEuGqusWYGt1yH4QbriCdHFxg5sC1c3GR0Fgxa9NeK9X4REIsIcP9BXKNauNTxeNPffnqn2E+1jZRHCYnE5GFMmWkqWBwNNklv1R6K6NRRqnmD4oe4M+KQvzQSbEgv5XExFaQ0Yp/kp4SDh9++JASdqqQIZpEqvltDc46GdmJXpFQjUuSt+c9BFy/lEHk8fur2OVwWXM2xHQioE0I1AgOQKcp5PBADq9OZMGVs5B6SVIQB0RgCgSFpMbpgOLS+B+0fZpZy/ai8l/lwPAnQjsJfJxT0Wb9HGkspwoteq2/Q6PPeG7CXgz3Ca/YEVh2jTHykh/ExkhozuUKNzfQEHkJ0uIySyplfqbGknmAh6SiemfslmCYbKeDFWLPGYTmSP885isuIWOCloYChy47WNM/WC7Kl+LUZjI+WAv+ZpLe8n9QIya7tGbSI1BDPT1CpZxB9YAf6OE4Dp915zF86bKp+rTt/92WsIGz5s34gF+ynBdp5rr2Epa2IcM4RlPXcQ/G1ZQ+mbRaKinYZcoKHhXepyVm+ind5GPT5Ms7m3ywZn79t9t9HLX7mLT7Ett92bPjaZTuAzcT4ZdO1uc+mvMiM5+aMTgqFrFd0J1h2BHK0WcT7/ZKjQBRvdo6ZGS5orHRL/ABC0WMeWMR+TA9xT1YL7/ESo9weRu+kni/IpSPKNCd2FO3iT3VKPYsP6NypTGgCbYW4aLJRuRFn6o7enJeWtk0nMJpLClYbiYLULdoF09PDx1wipRpD4clKgiIQ/TRsnYsh2d9b5OzgB+QImXqljH4UjBBenUwCncSAQIYu7fg59hkzcpC9spINWkDDYniYPjDlaU4ADF1f8CsS17K+bxMY3ZuTbWC1uyIiJmZtkNnGj22SF6O1gbjyTDRAoiVYH+EjCisxGRnE3FR5lPMuTmf5/P5Z6+mfY5mF5joWpqjSzj+kvbGNns9SZ5AyveyKqY/DNPAgp3FBSOHrD5y7S9j3tiAIM9iPgdR3Ok+TnWW0I+jRLvaF45Fw6MRRj5HhAsfXN/egCMCaWBWGRoAtUBNOGI1t+KkalSJKoyTxhM/dF2GGo++4PSpPn0kc7xbFBdAbBwmlhdZa0ktISHHCHI+tp6PAkzBy1FEk6soJ0HwDjUjB1SVpBwkgtHRxCRDo1Od0DwG3a2zMDqrGIFlki9jVo9uSo7YOfJmsi6pSkWEEJyXeyFNbpQpBzhg8aSoErB6pTe5QocQVBYN5QYVBJc7hLoiXvV0eYON4VJjaBohPFaWeQRQJPey6LJB4SyoVeRwVKdeJnVQDBXQl452XASarcgCFp1vp972iMvud9B0a4HMm9raBa01+2vINVzwG+smLO1WTHZTNi59ejXMFn5P/gyfRxD4aAK2gxyJxOx9vRZyoNXYW7PBtX0c0axYo8WDmUUkhd6i7H7iGfktgsPtIAXbBytoAQGr5qZGgzD/jDLaNQXH7Ed8QqbfAHBj/ailJXfVOeNpLW237q0dshyylOytHfLcraQZV0kJLE8D5OHwSu8gydksZQd6jsxscP1BeqMKvHTgZ7YnNMsR3VpSgjO8wM2NlEHxhYECQyUKWwFTUtFYEokJdcLlZ8P7VZ1m4Vh/VKQhX4IVX4W9m2bHtngSeDfyNsMGMgYvPZ+B5LMZEkCaepwUhVrBGOO0UJjlVwYqSJYoP2v0HGI4QVRgMTBVJkdHcswLv8g8Iw4c861iUzVQQVjWBteWylTVaDvQCJEupJmikeXx2cjkAfaL7Z4UOp5fZRSeuElsnPd3n0u7VvaREOq85vtLbwwyBDShnnkFUmMB1YTHy5dAwFnwazz0mnSxK5xveyK7ktLoV5pb70Vl0mNZ8k1/1o2fXyCXoqgEA4AX56u7K6vzuf612jdgRvgoknTWPyzM6+ZVD7T/roAkSYCFWGh70Jc2sNG5tCGwq9Vyqygi/CsgJ/rJrWdv1LD1YNYUvaUJpvgfgcIlQQD48wBmt6oKBMwWvCjCR0Hs6i9BD2BLSk2kBBzyYfXcDkKbsrDvc9CIvp2Gn4f8Ewo0TlqbOAsN+wJb3UqH6Nh/jpotOy9GdcvOi1GAujSGnr0R+jskfzVOlMLfP8l+lFrxLjj6VYxIza3DMkfwc/96nOVaEgte9Uy1DReMUcpxhpqOoOT/VY1j0LtfbtGzP9HOLqGf9TGUfRGsYJJ056dTmw3HCBOy2I9GtUPiy3CB074HOr8ePlbF1WbKtxmOiNjmenubRl+Eo/960pDG4gawT5LppLWZYkN5UWaCQSLyrKImgqVKPi7eRi1fqx3eSE91e+IQtNZ+SDNt9g1lfTDmnobm+jcCi/jrkfMIoJ8m7Aqi/o1jZllUnP2kkCw+Ar6eJJ15VZ8jCjPCGcIZ3PNo0q5PtYJwBAWvveRwn+HBEHq/R9zL3sz9r6tEsNg3A97hMu03VL4CQcFEmiX3aKV+lLTjndmwvo2T7fUn7a3NLbYdcfJMCv3Ip3GCSyq+/zv4CW584RvG6VX3DTj4wfIjo3EZQ3JhNMBabVNf75HZ5Z/0J0eioO1EizMyr5i59PtpclPIkVkGb84vgMEspWSEYOP0ePnoHLFBomsqjEEyrNOGOtBlTaA1uwy1h0/jZIN1oz790PDMRvTM5xq2LkoaKrkVKnBQ9ZEjlLfSq6xEM03SrNKIs8VC+yWpNMPPMgep4pIFRJj2cuSGe+UVZm5o7vsKgrxORJWghQCFsWnpZ7ovI/6LqrqiAG44v+iDkwmuwwrdbnQdVk66Nk6Eii6/iokvna9pX3xpf4XUdeFS+iJZ9TXYRz/9nQoQxy8t3PY21MHk3aqf62XDuX7JbvFFQK0F8cAXsXnEVdjYktqP1x9vdLa7mvQYiNZlq7vVebLeBoN/O/Z/Q5BILdEZk/z3oV7qxros0bosgwH9vW4CmAHgo0r7KqtBE4GCHmuPvMAznV3HmzFlis6VjS7W8p5tMabpWv2RMGxJC+hifDuidgct8l+tXfRerV1wk4J2+59Fv8i2jbUSDsIzexCesQLaA3kZwFhp1aYBL5OUHeCRGM4F3gCfzNggZYNYBRs0HD0HdbXsRkP+P5wmAybS3r4OYjhiLyu0vqEqZi4Ke1GrbpfpjblR52p0RhB/UjOmj8sFfCX1cxV7xTFdt1qz+Lxuzul8Rs/rlPH77WkgAsrCTyA8WmoOo81YYD/eSq1q+S/YIIxNH0evZBV77/WXt6e0dWZW/oqYMTzWUIRdYDNcurwdqLFXxVCM9wR78b4Z7vIftcC9X1DkjJZbNwu/lWk3Y1hHispR/wN1VLSOtZVZ3ZZ1OXo0AGH9SgYLB65nmktUXfLhyFvC9PTHN8zZ5MbNbsejWvgZ814CzTnBgN1Tk31wz+f+dRqhkeu5hG8cW6YPMulu2MQsevChBT3JP6pE2XPImdLk6ylFZ+SVcSuwM2F+wYHm5gShN8AWDUNlDIKOwntBULNkJE5D6xDWQRlfqbjbu60ip+hKh/pBI3gYQ0Nw/w0exdAdQfJwMxW0UDG0BLwfwRYE+jJNtztqqPS+aMjrSy6aI8J/p2U9Z+XGxpWp15yLOGjRRm3oPbBw0KTCX0/xRtXkxXQpI1J3ss+jJ/V5dgakL8btVoauathlS+KihtiXKpmx3Gk8Jhl6ydrE6dVqQbJ6ttIx/hUf5Iz+QjOWJR1L+72GAWUr7QY6/yLcRIQZry4dL+QMbxoy8dcno1bda1vUk9Rl4MFmgU7RTdmtrqLfx81xIITBWcj5nNhs8ArBdhkV9sWEA6Sz7ZL9WoOtWhoRHJ7n0bTsIoIJiojamuLY3Z8O3YX3DgS/0eT1OrXAe8muSpl9HsM/Nrw8O8Kr9hn9l8EV6j/enF9ISPQ1k6/FZKikufxO/JheVeGTk1IW0Kjg6p48n5qfdluzf43MLz0X9O8XcnB1qln57YWRLAqbgUFfI04m84Ufk5OX33F01UdwndoqDc2P19Oy8m991BqZvrVrbdDm72cn9hF7Zf+iGp+Py2p8QtqkQZWm++D3gRwVssyzI7noHYg114+YQYEfaaSDHN5nUEKudTNZwWLTcOSKj1NfmVzQIX1Ph/TdqHFIaXl3HkW/jROYpOvt7WUH0tr8aGxP8B08NTRNooh2nhgF3OQKH9ELFM1byw+bUnuN//6xkU7ruBd1UdZD7XOy2m0PIvkaaqED/GKvtAk0d9Tasc2bBJxzIH//WZVTNmicpTfJS53Jxdref42s8wAIEXHKta8Lu9x9DwX+e2+RNQda9ITHZt2FFxX0O5IjHqAXYPQbXBgVsUgR30EN96R0dYkcEkVdDi2dc3axFri5mmlK5FUtAYt2pPg+eBvV27YIk/mlrEEKSPYFdlVd77okbJ6WiDGyc9M42O6NL9IPg2vndvQNWBOaoDc1uJExBpucw8C2RtA27vxloEi8YO9GMRipcGCknwAUFRRQpBZm56gNFTqzKvBbjodyUo1HY1m8L+Ro/L0XODa/gXvuFTBCJKKF2YV9wuE0zBb8EPzMq8erD6uHqwfHqw8BPH45fviQtZ/ikeshX329+lAF76QMLh+vWqjX51NIsmK/UhzHzz9cPV71SUiiM+Bi6e54VYS74/Uo3h31FbI7Llnft0Jp7Cn7J+VV7QaJOfELXs8b6Ouau7LNEKXuRELF0JC5CPf5N1e1fb5uBWlYm/8KBaQWB0QvWNJlTPGbsba695bgkdRPWePVHdb4BZsW99FQvo2W6hiTolHHeDVq2t4vRzVl4cPI7eyfRnR/fTW2+8jrUcNe4IZB+2d/VopT5/DvcDCqhQPdS6wTJ/FyP8sXAlsbQMYaMVntywyDU+wcrAv0wcgIqN3RvRfvn7XF++fyxdvcGYe6M0Lla+nqeDBa9KZFrAXvjPQ+PfzfOsdkcb85Jov/1BxzYXV9pzNkf2Pe6Yr9r5l3R7V5d/RX552PL6UzEPxPFJD5RbIYIPD1ntO0GjZMU59Mhlg/amZsoIwCpQYEtrKGgAy5XtAY2OD3CSxNgaOXgjs1x8Ko4Ddygq36KKtY9zVQZUi6U0hxUr2Z6NEqe94Ex0qwXYq0V3rjI3PptBPPE4MZnDjSqKG1qAosf5U2FJ0J8wusmbpeB/JCiRP5N+sGjA0d9t+q4+60OLFcW+FsW1ZBbfhlwhh+BVawy6oVkvLXVqpylRJI61arlHKVUmlKU3epISUEYHktjRfSejIXzFjm0ys1fD49v5hO5KQytDTURb3kmUTg5zOS3KfVopl+xuX7q0IeQEe4V/sr5wKDy+dz+AWUBRmlM5gMaXJZDUh0la3WTux3/hgrdSBP5HgmkfMizAN3y4OJzm9qP3j028dnuy+P7/3du543n9f11kHqo2KtYVonlX2KRblJRSC0MFh5tZCj1fHklwqsKDcLB6EUv+BFcBngI4AkhSDG6gtkeTW5yk3GM51E/QYBpog4RYGiCCF3CV+TKSTtc2c2/GL5lUvI5NcQ3SCGoGO+DAkGx3jxZDopp0quSVQZ6f2y6aW3Nq2Uw/vLtenkaHIirk7zCvXOnkoMspbduBH4WMG2jn158m1hfacivSlldTg+l9OrioYimJjuBQWJXA0dmtdh6KDw56RokaglRStdNMN27kyvJkNR/MgcYUy/8qvOUlXqOpa317EM6jisIwhQjDCkKuTrzJEU8hvDtGzM38pLFVKG6e4FDW4thkHWP+TRZNJ8n9ivp8PY2aLQdQwRHC9kMZ6ZnWm3mJ7r3vOwmbLuHIeIVB3r4Z1ZdeWoTAp0dzU15mqYGIfQQvtJlO/zXh477nMvHV6Mh8/xgBYs8+SOUqz4KFfC1w6c3f5AYbKuj7JKvkAS269pdqDWxHCYwF/GLT7jQk8dnee2VqlELJlxlpuwP8tWVxeBd3w3DslQ1nCx3n28tU0CvEmAR4xiEA0oBkSUEdJ7siUdTLBfSgaAEMU9ARSh2rRZIIlXdL2jvaKddUvT7Gin+qJM0qyJYv+1NErOa8nXYfI7Vo7Nx8y1FRj3tpgyWlGJ4MI/x30VBGBl2KDGMCsV0uPYkRRfYbeDURQpe1fg5gzkNisdw7vXzdyJOKjJXy0fIuMrpwVb0gVRfAOC7rISkxPLxFDIqvhhoAGkmrDqQ+iojgnzj1PsqL8atlWZtkaNrVEYbGohlLoNwj3OYCDhFW3KcpwwZI54oJEjpsJOS81A+1Tkm1skMbnu2MoP8Qrne1ewdn3u0243ZTcnAk6BYpGymUiOKxM2AQhO+0ZF3lhveAP97pqGxVSaQiHCCsFULJlDL8HUOwYvPqDLOchwooJhByENbpmy16AtbOiJ3dVzSstGn1GxTeuGAZzwbUSuv7bkAP3Xgn8pvmavhYZpFvGXWUxL1e75KoK6MZw2LtYlM1vysvWotEttjqb8YZGooJkSmwmYbLdUkHezMvsGEEiRF6zMQoIyOwuswrZsi6HSaj4vYjFX3CnzDzwX3MqBAj96UqSeCk41jbPy41zyYpiUKZsOk5IZtBw2usT5DxE8BpNR4wG2vnHcugqSy7eTphgyS2hzZUVjNJGCzXF19XOAJGjbJWCUFchlqR+K4BInQ7pN6MqBpdPMORAJI6p4CssIQZTP2S94Y0aUz/zL7CsXX2ZfjVM9544D5g3m+hTc8ZXlrIC+mXEgaiKIopVXsp8YTiagFFPmV5olkINj1modX2EckhOmzHQKrifTKaSto2G0JYaUQ4WZhL2Q0qMIp8fK4VVSpK1WEajTRE1WOMFEIb0M1wfQTUhVDmkkCjbUNSm0JMXsqOPELAHjNEC+NlgEZWozsbqsrJZ7zw57aik29AivfCxQlNihKkJWQEuHbarJ0B1rtrvsXDCV5EykrZYuletyHRufHQUfyOp7H1KFFjDf76ow0WZrYxJFhMp65c8FlhLXcDxCOGnHR1TqoRG8YLZHLMFtq7PeaT/uWliqxrBqeg3XLr2hGAnOag0/L0jNScLRmkosfJaQ4nYaqx6Zk3DGdJKQ0gD4izfHs3E5HozVuPqRddixafUeqgdGLTjGLd1TNLHjWJ2AbOw40yCQTGcHWF1CD3mD08yK1H7hKVjnKlMkmBfzxHM33GZeAkuecPi4kpdzwxAxN12kUSfImW8yNAletv5d2DCltptB+Ak7EHYxoWTTOBdLBWNdFpGh7MZVPGszBNG9hzTqBvPoJNDeGOI8fHPDJ1N/61TH9mbAP8tmAC8K5pLD2HBXL2ZrvawzsTH6I04v0vXx1UiZKZgBDWsjwSMIl7A9uupYU3jL+4YrA+ZSTnaSMFcRgCv8Jz2LLDBtubnp6Gz/OxPW83x6zuNhHQq+c+XtvAV3NPuI1bgB2Tepsg9y7dgoc2hRZxcwvkUdIUDmjCJzpqhNGNLBMx30Sw9Ien/SxB9+0nOdBogATMdhsCvfHSc3sEozsy1YmZJVTrwgXBeSlWCs7ghBqSO776P4NZJYUmTuxTC2J1BtwaaTkPxf46SKRGvqld4uG1R6M24gGaUA2wh9Gn6QlbwmdXRUuz3ddbpPmKRE3wZcUBdhznqBFUdbXLwGj4X2nGqutreoTSSSH0CyY8n380Sy6wr1fTLfbcxtC3wa+nXK2BhNGk8xDLlHCukcTqdu070ZD7NXFZvOZDFS0+vsW7UgRzrEP/oq66+B9wmUQ5u+RfE/ThAiX99rFXsDsO0DIw7SzI5JiKz4jnChaO/zxWIx5q7pz0W8PzvkfRGRhxWEaNl3dJmSgYUjXRUc4SvuJmbPsxE3cmPH+4xXSbrbzHB3a2hncFzd3NxO/beBEPbAxnECFKbkiWpJskZxNrJXSHWjkar7AU9ezsWJPj3lWq2Af4o1N4qWD4Nc4jlDTpI8ZQjx1r+KHh6WSQ9YfstijUwhpsPC8zAsHEf7HAG/M6WH+roCrYjMYdg0kBrQLw0bRdh+suVmwcItoT+qxHauFkIiEkJex1mE2p8bG6oHEnnw6zjktzDHDcf1RFdeIUd0pdkqd55sbHe2UsfaaaPrQpscuWqpHMPp0N3eAEwR5TvWBSL5eq04SrdPNcGb4JijOKFlNoAIMIb8xbOOWH7WUc1nndNhIxu0rlUD6bPSLM1Cf0+TNN9ay8LVsrhXLYumWs5i7d/WL9Z87Y7+24TleHTBFQCcDD1ry6Xmy5xwkPD3MslT4BIAkxb4U4sG0i3np9Gv+b8NvtNcMILZ9F/BR0VQ2YIVscOPV0gz5o0ahU77xwUz36zFpYOAGJX8ZsE+YHdGLQYNUP/Fb2t7H9ud/TZZWg58t8GKondi8mlqHDkpEn1mF+my7zqr+PKvhzYaZ5khuLwdOZoWsu6DJE83eeD2plcQsNiw8Iu12x+cz5PcTgN2yzthfzQ9kaTs7jrGn1n6YKK3E1OzpU7Ewo6GUT9/E7ia0bICGZbuHKzQLtZQx1AErre3QUCstF2qAL9Ug/luHBxRZgI24GKSKDZLe3Y96JRIB9zNbnYUrfQclr+p7hGd8kd0E34vkyPjV7m8ZYL3juzqufz52chmkQILAoMd3TFJqZv4PnN12fPzeTKbzyE+NU9brckw0Ral3CYa1ASnMcWhnQjH9TlyUBMRs/kcsgUdzOcDMGQ1dJiXCZdI5FSfYMCgquZzpZ1mAybYMTuAyKN+cvTfXM7/qxbx31lnEFn984s0WnkY0RNufgdpw6Z0QLaZnAk++Hu1ADoig+4Ot0OTYJLlNRFxpEVE7mt3xC7jA+MxkRcH0T45IPLigMqLA8ru+14mBymYn2/ZDJMDKyxmPy8s0v85sQA7xqVeugMqGgZGNLDjBqVjmVhAOXJdkyOkjGsQD/N5cEomaTWCpBrBndTJlFmzTJmBTLlukClHVKYcoUy5ZoP0pyqR9pODO8ZGj+Z9RqX+5FJRVJ8iNVnkAFbQOHaPKt5HJAUfXSKYmr5GljV56x4zn76K6TmWyZKmzgO6qsgJoNMHRpYHtDXev35/vxCTaORO8XrdJF6viXgdMMGP/h/qEi3rnd1VMHPIZ6Ju3WPFfC76SbgnKC4Ih+MyRy+trrbpFsZ5mYRHUM/21E+cWaxm5PTHaH3TuE/hDB4etRtPjkxadwda2Oz5nZo6vg9rdCsPFK1qd3OLEbeus8KD/L0oAluiDogk5sTIYNhuNhSSBHsFSY3sLduSWLavRgm1csxi3FmQk8WbyVr/VpCzEwAYc35WBV3wY3iH9RdzSNUnSQ42ITifpXhMozK+MbnHSidLSB4+4wViyEUNlrHAngy6hytyXT9LnlhfbzQ4l/2XYKvAdHj3Mxqv/L9tNH4PM6pvTcfr3WyJFdkC3qzB0IWccrE2snlAy36CHQtchGhpnHkHiLXwe+vjbGGMaph9jClzBp75hgn/c+ZS0QnX3hlz3soa3eMpJAJk1EPJcSnk2lEWPnxROKeCgF0O5wj7DsrMLNUm80GTyZzkm5jxAU1M6NIPa+9rSizmffP52HD+w65UAKk1WcxN597Dbp5m+tFZMDh2UAQZFPTsKbnaaBM2+A6VzILhcb6jYHjIoNmRmrm6CtbonvrPDV/FyQA674Du6PdFMtPjkJuks8bsDlxK5E9c7qWsWJ5qm0a+Njwtq17OD5jwsJ7OkxRqhshPvrqKHKhAcsFzwJuZaFXByulVcSJt7iWb6xi8oH6kF0vdGPlyN0ZumrIvCSkUpPs5QRoR9G20NUfioO7GIJdgi8cIBXRj5DpSIi96b6C/5nPjzYgqizf7RHd40q3PSOfD5oMG18fsb7g+cE36KRr5QNKaE8RKpOaVMHBVhjTgZtoyAY6TAVsmtgKniVgrrwbQxF3MGT0I/mxtbbY7ncedLjtwiF/49gGbpVlyy8paJgSbl5af+WRBzWzTYtMQdzXBNZYlJSaZNuoDK52wGfCakx0SP2gHPEyuvnXGD7T3fbDIyjQrOWggQNfm9ZMZRUkQXaXUKJZgMc/+mtC2A0jktnKjCvzIdlQV9Lxi95F7webqvsBjBQBsH0OpJGIcfM74hFzlmDjZw4bTLHeJG8i23eh+pzEl34tluIKGllQhosBgBxyggIDNQ7SC5N+rpAtEGiYXp4tLC/It/ijuhzzApRqqimTdEgjCXeCDwdBn4DB4TUfZsSQtmnsuZS+uEulSzQadOihCZ5t2e8fbvBvXWGm9GZc7NolvVjF9hnHB6u7Pj5UoMEgka2NIeiZYJcaAFYJ/9mAMS9QWd6fFt+dglcuKRZbka+TzvGL5mvui7iBywRUBOjcyTHHB8jX4PlfmBxTES/iDFgVZC3ymk7vUfEEzE2MiMvgy8Ph6tVB3otfi2YwnuVHlMe97P8l53urMu4HikmZ5i3cYpqTOU48IRSXhXPZPRdZmK7NWK6Cui4BuMtNJO21uYxmkmpY+1bRs1K9aLZxpMPOdBuFeepI23QzTLMswzbKmRamnWQZ1vEoN8btOtCwbEy3LONEyueA/IB2N3ULGSZbdzMfgQQO+MvDwUkPDfbrrzObas7g/rIlynSiD+BHfa5jj22MdS8zw5yVvT3GSxSYpXQEUFYj7EBHWgdBN2QBkyUoHN0sAkWrafISikyTcGI9yS6Zt00CtkjVWo1eGU4aXd7bY47hLXasFvGUb4n5xcNbDIpIL3Zi22fdr7XHpvjPbak2IBv+z5Az2BQNmzppEqIf6RqhAY+ZQUdZM3Le8fTVKspwij6s5HLJEtSgODlH+aQADBgpWqtA2vkHTfVPkpUPV+jAAt9TMiopgIJ3NdZ2lJk59HkgErQRIT7pm8HB+J/Ybv1+TZAFIok74Odr4TfKlcDYvHZePwSZv6QdbFQq3/ko7q2e+XllJouTXAf2iQ+01kFquZ6LyQaxgwEPLphhPIEhpNE3ZD+C0/CDjeHWtLKYMrGBk3v7SfazjwTazCvIX0Fsbd5VFH+60MyxZw+5jS60OqQteWNfBKM0osxrevbvNNJLNp/Xo2Tg2u+/VDvoi+pwgZ32nMAfHel2SEW1u7scHur4zrVnl1VkhlYfw9N200wjmHj4a1P1JplHrtU1R02HHqw8Pk2I+D9do83OpAfkI23inJ/RoW3Umldg/RWiKy1iDKbWGYn5HGdFTrQt4RSJlQm95PSokori9yimsDtAfYV97ltH19tm9CAMnqNp4NmwQnyj8wpmKGTeCsjFewQHedLwHham5gCnb0zowwi3j3ivJm0IhtOzUDwOiy3+vvbG9+XjLxEu8P0kqdgrmM0yMm/acVLCdRuVDZytzGXfCduh4BzlJqjUTqQx+29TGyXhugsCNeggJkftgp5hom4OJr2EVn3mqQFT5wKmCN9us4udFdNPn+V4hIDPYr2zSZmRSBZcH5AbSpUAxJ1ExLveO1efgnd/dKxus4qNlr5gYyopPgPF7PpfMkZy1t1jFVldTzxNY6UnXzsIYFCfgAjVbpTbCjoQh4IMldJ2ogbkNGg9NqObddRg6WLJ3SPhlPG3bwOxWG3YbWxZKx5IXdho4VIDxRZuzYM9y39bFKs/1enN7jdD2KxMt0WodAcf/lw/yK4iotgG8wGu2SMjv9sLJ4hRDXVwAvWD0JtDMauplU+oihFTSVJ2N+T096Pmm4t+H7ohSm0gCU4jelBCY6WNTN+DMj0GpJZqylnzAbtaSLx05SANjkluZ1fokA91gMB3+iDRDuAzP/ibOEWf/+nDvHVCCTK8nsngxPUGSL3wxkwtQlrSNTq6NxkVZPUdVa5mJrs0U9zGLgoSgcNVTvdTEXHIbD/pofQ5WOqo7OeZA9KkBQr+EznVi1/eMPxEiQbbWncIF1t2ya8a6Ln00n+J/ov7jYvWiZaeN9f0aoVp2Dq5SZdegjE/Fgo/KZOp3qTWdOeilnoFoxP7yh/wKxrsvV9VXkI4YrAvmzF9lIkLYrKjZ/km1o/0lbkGNfZ1skI/dBikS0i2tFm4Xgs40/swVGBVAWqlnhJ4DmHLtWpmXwJ025aUx9Qo6jdLsQPLSHe6XmIZgFsXDV8cmbbip1jNKJ2FvJk1LIJDwAPMpC74/Q/HZ1Dbw/YkVb8EjQ+KcanF7jJkaXJNg9sTog/kcvWsp0x1d2nlTxPMmb4opxFuGKRtkoO2S3QKZdvpC5xP0Zn+8kQdI9/VuzVRKdDALbH91Sk6VoGyJ0NQL4+RGgRhjgk7fWtbpkivT6YofzBLV1NuqsbdDFybDAM9MYt9K17d+YnvuCadI2xOH3T3v2AJrZi7peRViWAH0QLa0NzpuxzbR2bfv725JNi6H2te3b308WD1hrbo/+2KbKiBB3ejxzHCc+Fmy/ItPMpdsXL+8dmwxgHER/iM6tKAEvB86i/Xm2RA7Xqu/mwKj+wxHx0mQ4b0ed435GE68dTcLXMxcfEi582gShdy/l8kHSfeyEkMkWUltbvsgY17j5TXwxsghBmNibEMhR2jtffiQFfQ8Rp7TvhmONBcNgRbW0yOMAlXaIAert2HMizNZKBuhHSiEgcABRS7FJLgRoLF+hPQuJwFtj+si6kGfpYsatBwkMddss9pXpkQRBYoRq4jG+mm5RD9N77O7BdO0+yQzR4nghUVs3toyhzNKNKaQ6tptjOSIWkQBOkkFB0fwCVmoDO4ZKFcrY2t2TqrHjx93O1ssKVvr65ubGxvmVFrqMy6VpOcXSlYyde9ud550nXV8d5ikTbdMcyf8zzG7vDIxch7Zo4t/TFKTD33qNlyTgMgpqx9KlrmU1aqPVl4D6inw628Yf0Dtq8R0Hzd3ZSeHzeX+FffBsZb5rOaaQPyeO7ADaZDJmKMzLnW7/eokSQnkxzu42IcJuK9IKl7humHlXKb2kOCcO8aab9zVmS5lrBzdh9bbSaYbwoITJDLWOTe98d5pKc5ak4UGTWq8PpkqJS5KOVzNVFwDETsUVFMNROw+cNQ4FThAsBINVdK/g3plIrS7krh42ZgBhCC/fEojrQQ4uzNTYDkz8c2pTztsHT3egaDmvDTDWQZECXC9GSaAt0bhtdK7k0rvWNAzEor/W0WTEpcV5DjC6XuQYYSiU8CHTfKj3Botbs1Oz0zQYWBysnuxMZhYxcpoTEYTsoqN0Sqi7f5YOutuaEiJb3hOpADGKmqOfsGXmMCrpsuwTwW8V5C5hUmFm/g7TH1r+8PgYmDdOVhxfLP2tDGPBiGvdpLirVYr2Z8kVdoXCHWkz0VpVYlxhFgNwcBhswFSiDC7vkpgh4v7sbuV2QTE7qzTmDfSazBYM8+ikehPvsSAB9h49d9n4P8qve6dplnRL1ZqWGb7veavVLVdINWelPA0bcaNfsmUb+yO4Xl6PEquxhpt4s/F/rxHPVeEXkMHR9fsxLZcbxsWZr+rRdRGesHWlqcC8eOykPxXVyc9FyoQV9DuZ3lSWnXZf10yrICze8djvOkb/L+ljWjqi5qZVo7FxcA5QuuMlT2FlThZASZIarL57WOymlfVRfaPf1xfX69dr69Ni9N/dNvt9j/K2ekqpAyjTq/7fqDz5Mn2P/ZEleP/9t6RD1lTXeSXh9L+ZvVWz0WVr/4HqqgrdFKML6qGzyWrw/FsFeIlxpOJLMDEyFef6sf/+fT//MP8WoXE8GuFPJ/OJFpQkoKaU4KipIKYr6zgjtJyXKLaB+6j1X6tAuYFdjMuM3h2AUir5mcQcnalqvGFkv3C/eQrbVQY/gQLRoE/uP47Hqf7Vqq8rTaQV6EwlsECLYOip/ErucOv5AEYITdQlM353P7cSgvI2iAnQ92fuV9LHqWSG21zo9WyP7uP4XfoKM9D8ErOcp43gFfyOnglbwSv5DF4JW8Cr0AhBrySx+AVewdrYu3FQdx+T9pFUwGFGfJ/2+UzuKqq6cRgQsaTi6vK/LZzS4Mt5PdKFFKsZoKvrIg1cVVNd6cnV6U1POv5OD4/xSfa7rKdDBA1s7CybRGKz7PC20lkQGN0hylXUdG7BaJX/qSQtTN1xU3TJoayRomLXj6yt2kBq2ExYdhPHAIqLMTmjXcA2wNLSfNqOsSAICnf9flXmrUhOWAc0IWhDZOcq/ncY/0AgHtxUciyfG3DJf4QxUTPxpX2fP4jp6/jRm4siXBgtxQtoxIS3Ooleii/V9AyMM7bagR75bJNUgMRVM2CsVQDo2gZwbGDCeduEJ2vN8zIT9fZtuNUL5KYl+MIpBWZxpw64PAzbdVdgv6YmmO6wZaitVFvnu/pXgGbrc1FdB3ykSxD38W5aNeiABiOsNIVPHqtyAjZoSMrDJtVmplfGi7RyIrXwCq0bFw76xkO0F8d11pT66gRHZRkhp7O6uYnw/lQLpsPDTgM7oDdZX0+1D8E86H8T86H0s2H8j82H0r8rJ4P5f3ngx1mfak2M/qJz52peZftuETRYU21Zsr40yiGkokGG4NYljb57ic8XJ96du7xOFp8kVNH12pZuXeWBtjfO8oAKylCsekxWIs32Wppl57WOIihLWUPNF8dsc/SA2fs3cHjtPc+fSqSW1w9tWXtPBzSeS/rDz2Btf8h2ZHaVxQuKxGSMVPJgcuwIXAWUdQGB8UsuSGYHcv0GWSJhrx/dvdG6uc2jY6vg55vgzwXHJGqPhUxwiEDhJotk0FMbEBmHrBNST0waPMIIl7aEGTBHGqYWPcenGjKaGrLM90UQbsADZ4iculNheE9AUbdR9RSELPGnrt+Oa+S9J9VGYSsd7dRPPgm2nVtXcgOfLCibdvQW0XKgm25/jmoxX06ShfbhuMGMVrC8nJ4fM972GqtFH79tlornm2dHutBfnb/P2jsI9GA/8cegE5xFuW/1iE9QaMO+ngM0pp9DJYu0JYgELXXgFPkReaeZPopCDVw6Ekydn38DlxgpElgkNDd5iG3TS3m0BdMhjE6isYf4Fwr+womWaZandsmmnRGIyJsusbQ2HX2Qr1x/H6VNIgEt1UsYQw2l2MjIQq/rH6JiX6NF7cBMNhqOWNXaFmufTDN9IMqdIKRpO44fxVJKgAPRybR++o7DVuH4ndtT+b4sAzEUN1dRm1U6mUAQ2CDldZ940PyPjiFebdsrTOWNWm5fVgttQ/XbKvOLU2OGW167T6ewven2ne03PLukRymXkzqNA4u1JPL1iO48niOccY673NkSKdGbv+lxHzKTl7ppu19Pt41TgF/WDSFaWtkfPqqlp+wLNVtnJWklqUWUmOCvrv4O52iDw26zIbzWcB306Tq/4/V0yEZtJLj74Ra1h16U4DGXi4v4wX2U1X2a9FMuKXrxZnsyHrxy+K34R3LYj0jMzow+9YmJc7GMOYhfmm9ky2TadhDUTBC/dqTDIcoCpbwgxDU0I/C8u4PXtgwjSUdtCfinF7hpiFIDgIC4Hes+CKgFIYjCkR+Q0AYpClBznfo7RYGU8n0Rhjezp5N7KANMkh5OZ6UVU9wYInJ10zqZi4wMQd8lKTlANOqztA1S28+6kxtxpI4o617ruppw8L2le7o1xdN7SujrAelbp/ghW2fCNqnXRIC28Jm3LUDAyNJKnjfQEs+ayJeB1yxAz7DlPAHiU1EdoTNLNmAHUG2XmH62XQJ4qldlxzFXXIUdMmQOsZJV6C7O2itoukIsUIXI0y14zO4fZSJJxDG7F6+pEMXRaEsyxKEPDUBXcFhqumX4thcKFVF1FuTc03dF1UDua998deinq3OkhjHSa8kNVnWFr4NRPZ9QZd8O2u6543H5N7CUidFyRsBvQXJWK8uIK0oZHUyqi0XC5+JzjSypI38VFGWZs0rLzj9YtBeYi4OIjegj0SSNpU1Gk+EUj9uglpaU0kYOunOr9GTngSjqf1YuEr0gw0V0BZG1yMhG/9+MJtxaqma31fU5rF3KvykK0F5NwIc90bwI0kbPApqrSxO+gL+z/G3vvJRVmg5KYuTUlbcXkvj4XZLKxj1l0VTNkTfxI+Agrbdgfxe6H6q7vXxgyDFoPdI2Z/r/md3i/x+3GpdK7O+U3+dYNZ+Q2or7QLr9bSl4r5x0VArcy2lQaU0NhrtJPeJj+5ZeNamyZOxwvmW/9nZDsPJ600z9scYl0FanVqPGoRvLw0aB3fbijP4dF3LqMwgorsIQTdYmA6ywRES6C+Mksj2E+UiSTjnT/pKh4KohpgR1RQzolLYzGRhGOkSnYUZ5uzf+yyo6cSxacxImqT7YDqtnlvTns0JDHlMppMTNT75RrOYmEt8tyK0QMIkVBO14TOHOdJDxp4tPaeIY3aCGeV6PZxBzgjWeJ9E6I5/ZtBUQ5+rqL9+orHLW+mquaSVjfcJ3C6Qw9RzGKeJAonl4YLSxr1Va6KqivHgqpJlr1xTcnJa5YBg1GCCZ/Ymeu3KL+2vaa+qEosKRzeGZBWKOWXEXBGLucIkHx0j0+o3DKY8LOD/z4bW4f+HFN8+yoA2t2+uZR9lxXa1+4Oke5s50Cb2bRDE9a+Cf4d4g8/I5/YD4iPxSbOt6OToRbWKWX31Hn5TwpVMroWPMDkZ0osvJ8OFngAyu1EchzlYZSkkPDNayO9jeT2fX48nw+m1oVVB9saP9mvwLP07QR8TuJrXCjE5lc81izNkmlLghZic5NPCkYiX7tL+aATsWmABxy0RHxH2L33XqHRWeLDC/dTjdqMI5PQXadLFttmMP+qwAfzvgLfZEW+zSy7ZscG8BpsK8uL2LlfQkVxqm8SlK2YF97BkxvOHZcrgqUK7m5ueGvD8oUjZJRV268AM9JBfep+zma8pS675JUW8uPVzzC/ZJb/u2WqOR8klnhh0MzGHNHB4qlbr4cMDrj0rM56nDK4WcPWIa4vigNuCKMmWXc1a/bzkx+yYXxpcPe4jl/x6oTiQQz3qzOcD/NdkXNJTboZzbLCwPl3UtBR0oXmgjQ+0gwewQb8W/AZHWA4B/JJJ5ubpAUyfTC3Yd+SQ3JW86u16sYIpJncJSSc46wL7Zqdt3CCeGCYlGUt2JZceKUs/7RgR4PsAWCS6qjNJUFNAG6w5iTTQcsxTGp4VA4+7nMlJVYaUkVqUKt7uqad2TvTUw4dpyeUX9RXxt6O18fmFMufIN+cXIUFBx8Fj6VVTMSRx1XXz7h5pD5CKAzgxSimIeKzYoaUibRiWy1vk9DbSOO2BwX4JGW3yFvJ0iLXjMJ9GI9WwWdLJGxTFnnuDvUkXC3qIiltYB6FEklVxHyCL1DNP0j8KuinCtY7H0FutxOj7r18+e2GUetRR9M+d/RefVzP9nSjGdg10f4TnTiq+uhrU3mCZ44Oj+XPDDOnjLPxk3OIYoLOemvzwDqlP3FZ+BdgexYWg2WlgkVmdmFh6Iu4nE7Lba0glG6GyO5vZlUIiAyZaG63Wnkg2WWhawilKHtLONhWoWdbECeevOlG9O33m8VzJjRfPbDVumkaYpLRX1VDJ9bLMHC4h3v8+U/i2Wi1Ea2uj1Xo+xBjg1ib44q4FeSi0DYZdBG9iFwUSJuAUrYyvK4TxufHSh4omrBUo5/puo8mi0/TAwth3MOX8LW0O6WAq7/OEmXEMXRHaT/3ciZ7dv1+3dbr2A5GhNZiSL4a1Rzrr4SNHw1rXLzPYGxYeR/UaOIKms7XBeGJYI1TKjmc2YXVknEUdXzWag+fzsWKAjq54tczr5R79BvrxWBmL6TfZGysuWPINcoaDU7lfYDMhKXOwc24/NuQhqeuIMRBUfZO8WIQWrFAymaepLW3YGE3TC+KhvDND0wcMMZJXEu4tJglvorkQUi9ac4MJoiLiw5Wkrsx3PVzPvGtEoiuJMF2ZayEjfHSRUhOGiWptVeMC42RdqKvumdvTKkgLXTpTtNYRVA1N8acV0AF7gvvAn5SU5qJyPOwXcAR+Pj0/H1e744EsjHm0ZmJb8lyihLPo3iwaNoPuVvZNzuefgFoTUF6Ouyia2n0Vu0Lh848eZWQXwAVEdwVFNNQAN67SmHmKVMKcYfZgYUyr3rUyWwKcd/fCbWeqIyB9rT8Lmvo8ZXsQ/z+tLLWb3dFpaR6j68pkdpT9h92H2B6lDJpW2PvJngztJXuayyPbayLv2Gtk79iTadhHpB3BmVdh8jetpkPhe/IvvUeF6Ha2R6TftAIsx55kz61O5dpjmnN/KhIWDMOvEzgbZ8/zZC+8E7OM0RFQS9XDqeatqA8O/Vqs5mw4dee5Srracgrz4LlKNlg4+cPtFB5K7AIRodpj4wiaXBhhKo7DIQ6ASJeU0+1kzde72TfJE8G/gXGyecMhXfFNchGHpTgKLC9uXtCY3CYuf3NAaiJPTGp+HKLZwJGF7q6opOHwm7moYv+VCtxKR/+dii3f9WFrv1cdXRWrGWyXDb6ldTvV1jNK6hZsdY7gmjCj8GDzkde/PBumrIpZByIjIpdrJGczi75HbpEvOrbJ4DCysb6pt+aUtvJNmCEU2gx0cdPipTjJE/tYomPAVtRaLspEpOANFMNhIlJjOzqh+hSE3/fEWpXLSVKiX4JgXE6qMCWp0yUCH5c3LrZ74qmyx3Dx8KH1Uasv4itkfIfUVuCjNVE5vVnNdjAL1evxKLlWcBH2mvRmT/JZvNU4s1Wwm9z2pD0dGAEXPtok1rxljFsOcTgo7i0N9QAQyZ/DpGA50P0H+gkrAv5Twh1RUHUq9aHDNjAvNtOsb29vYd876iYSiP56aNndfYJwGIudKrKmvh6G/kxfNbAVBwdWecuB1Uvyk0qXPKowcSocBJLnKlln3i6csj0BF1L2XCWb9EYk5uNv4cklAeGvnAMIdwF3kLGnjbEVL02HPRN+fGK460uauhit97ko5HBNw0ifu6eW3eAueF5kGHN8IipYd2kMb9GLYQdNjz/XtA2LiiAYkUC6ephITe4qgosWnFIs1lPMyuxGLHfvQih7oGDM52XPxVoKY/GpxpWSEG5YgmHLxOiVOz8OxSloK4l5IP3S/sqSlWI+L76MxVf45w8J/6xNxLksL8SJPDp4g7De5SGa8HguSue0SFbHlTwHDMRqmiK2voxiBSGqbS2XYhj6fQpWrl1eyeKHtsxPi2QVHvrln7+Y2gINFcTFoRukMG6QXyVgnAGiEvij1XjybTXT5vTXeaL/Zqt5IUcQBwrdkoiHiVqDK/M58BvC0OXepj7j7d7saW5F6uzhQ83Gl3+ZfWWYvYo0Gb8LqBjzRTuN7F981SSH0hdSSIoWfqGQyn6gkIoG1+GFhjdMr+h38I/wLbzU8N5JMS3LaTE+HU/s23hpHy+F3yA3UsDxlBdqfCKTGetY/3+1WDSOsB8pM9bUn1eEUb2yEqsZBmPCUOGfbPVE2x/D0TIX9YCZsfor42Q/atttbJykzaurD931hs6DhWHfnqDiT7sLrjS8BItBFtUP+6L9O3zZXm34ACy/R/LyCsKSzSSrqouXcCH8hrvcNO65KErp256LAp1/wXjri/+VsV6ibm1twylgUVvPCyochT7fvYNgf4eqioOD6QtHmhYghl7pBwvNvWSjVfqKhqL63ei+Z/essMd/F0DTX1bTbEnF0kxwD6GtVwdRMPFmoKKPRPbKn9nnPOoc1Yd7FRsaE35yg5U+9dlNWcMwzUBDXl21J5K3sbf7LRqoN6K+c7FkcQuwWSVr2Mk1ZWGJOz64K1qtBLzm7aCRW02N3DAwmjuDkbtIRlvr1FqbFYkSFrc1PLa7n5c2YHmnYjsVH5YQmxYGj9na74ANh+iLfvzVLewq9ogYf/aWWh4Wpis77B0YWGumDldXuTzkLezzncod7h2FDn0gsqE3acciUlMFtfHGFk+Gh0ARKZdwvA2+LGkcYAuiVJYFmKzwZGl/r3j2G1liWNB/sM7dblajqrPnF4PLvaNe7ICPFTtCC/0oGStIzo3W9qP5fODGCf4GO/yB63QM2tFH0MjmvXaMqX/Galz94GU/+Lv1CBL5kQvzDitRRltxMgAvA7gP5vMSrWs2yKfi0kAQKg/vqxymD+849fxmwBXXoA3UIQakgmWaY5rNH8oZuvK1Ulbv3WZOUDTBHeATKS+U+LHKVifTiVxlq+Pzi2lRCdBEsnzN3Ob6rqF7oYWbPPKDKHeArswxv7Si7hI08f3rSb3ktH9pi9HnlJl+25V9bHvStO2Y89UBECqKyWp/dTVLVlcfHqdrVTE+TxxyUy/0ARtQceQBsI7WI+5k3cWkgUTUlVBa1NK/VFxne0l5OoM07dayf5gn1xDVmB3mCR1tyB17n0J1qUh+B04t83sdpliTuW4+rxAIQzOABFlXSBZMZ1qgfCUVwdEYvpKqEVRaxaBScsF/ADoJ+bpsnLt3ptduxWBTZw6kho47RZTCgGEbi8dolBu5TmRXzdvYefLfEuXOOWhN0KHiGpRKbIP4t3MYarsNjECrqzHLjrLQoNWh01x6Lz1aB0NtLbQbGgY9eHOXusXY1eTFemzsHVSDLThCknsIbixYrLbpozJFyyinkQE6a5LkoIAxRVLSaqvmjH54xvIICGA2eLOtLLX2HZivIML0gA1uPzV0tjppFANC1I2jdOG5MtcXVQto2kmwOX+00X7ymBhbUSMx6luEx9owEemyHpFuRrz3bugCrbQ71+erAAWS4noKWUqM8CW4Ul8N9EVba3/dvb3M4KjBLsR8GHJe+u/jznhzZ3sasuvc6j3aYN43kLJyEsd5ZZ8q4j3QkybgKOy5LNv3chwFnoh6ed3H2WdBiRNqII2wPg1f6GbLvC21Z9fbGb3mvExwbdE8zEXsHcfw2WY4A2sacBfc5Ye85Ij7Y6CjBJbj4laoU6GAiR2dhiLZYEUd5+SfELzApHk1f19ZBzjVHPLlLSCoA1y/wmUGZQcIBrPlBRFsQczWjNbGbfEDYPtstBbTHMe4mpY9aM9P7V75dGAtS+XDh+n5KBl8Kb9C4N3yui9Uq5VbtFSRsmuRaNrqGt6n+zg7RgtJNEF9p8NB3E2+3ECJbv0mMKTb16MTUPDZ3ECJynjU1xueO6o/B664JYvEv7+8mjUgjntpQWUXiX4qqLdEh8P8zwTwy78dwC/vDOC3YfTOaiEpzbmyV78LMDiR+CXTKa6KDQzEgX94SWB/c/0QiKQTMJHKSFIZSStzWVma7qYtrNMGItZle9jO0L3bvHeRB24sPWsg6KpbBR2pGytbwFGA0M4nLN6ryIOhHtP0heT/Yr/Hix7id7Fa6Q2tqgw3WiMjixrJHWwcY8izXKxNJ++nZaXRU3ZrnoW78CzJG5gH+qu4Ya9mq1pmrwK2TJTleCZ1pPGLK014xR61rQAdxOCCQWpOV8tGYukYddaX3YGo8VByBZR/ebCVLslWXwQGiW6fFJX9qcNvs9ueSYKbkCj43cTR2jcvFs2WtNJJ/YzbL6Dn40j3prl5UCShThhpKOQdImVJnUqtn5QYnXFHBZerKR56oNiACwZp6IN1m9+6bt9N0JU/YwN054tkOzpg1IYWCj3ihDq0lzcP6FE0WEFRMKQFy9MsOWoYNv8c9HfrgAwO6fI8HqawMeTFgyUv2rEKXlyyP/4plh0ebpe8Fn4A3D+obwWjI+ixsgu9Ekyy/SIRFLcQtzh+/mDJ87ah5vmwiVDBF0JnLvD7wYSmLQwb/EIsPbRdDm8N7XO3lwTLu1Kdv+KFWK59fIZcQTsVq8GwbnWguDKWnqPBWN6701ju6xobyrvdTC5Dk4kQiLKMRc+xXQn+QrAXwiee8MW+EJAAzP0dj7W7QSbxhyXYa0LXYHbWOK7zJsh7PJxqnJmNJYmQ17zqaTIJ9z7F3AlahwB35enqtnzkThWDsXRMVEVjouyJ7Yv62tsFeOarIUStpwts8KLR8mDWbNMs/owhxI0zGG/d3AM2tJlhQ91E1gv0uUqeUFRQOAfxjUjvqF0zMjjE+cmlynMghA2jQmOWcBuwvp4CqQ59jT9aZ+8xW3BQGzvR8BoZYHz0f3aAl9pcXNyeZLcrsM9Vsh3YWd6P61aLMPODqvWtauq2qrYy8eIS48UrA1qrRSDqjtmV94uxwuaoWJMxUk7Hk6glU0bdcd7yMV+3HbnAAGslm1eyF+H2Bcet5ny6Jt5FSyHHC2StAFxBiJqwMcvazCOD7hLQVTag2Mqpwll9YTJeg1EYIQUqvbGx2BYPGdD1OMBiAeU6sm8IjisWegNVM35zKivEwO5OC4CyZw626paDyc2FgFNRYVgkrCdHFWc5ffS5OQGrJz5XYpooIKNWC53H6uP4dCKUL8KxbOgicLMqpkpJIGWERxcLNpnRyPQ9cVGLTN8TF9meuGDfJW+zfXP6OjH/DuHiAP730USTHWLw++UE/n+Mvucp5Lt5jW/Dr6sJb7NnBW+zs4q32Qf487Uh7Buaj+zhi5hQ+NsQLHIl7/yjzZSxEh0YR+Pv8NFj88dnE5lzAoVcw/f/NCy1n0x00DvB2+xFEUFCX1auo5LvsmUJEhEI0u4PZevRUGZ7a4d2+/1QJGk2PknIKfz3YYJS66yyWeyToSSUizoVyrkZcslPxr2T8dOnvMOSk3Fr/cl6F/wKhoXxZMy7W93OxkYKHWQywp5VnpDT50L5UTmOStmQOQryOJ6RpDp5ZTUtZIHeh+PuAH5153P89wmSj5yIyYlU723GGDiSEs/4K1Ah2il7BhbVoWRnFcPTkhA6BCzRncgd6+w+hs24Av3tViu5mmDCuNdwd6PVir/5exVaO97SECX40FZTaOt6F6intUBaQUNvq9N97MimW3JNfr8YF9IkKNc1nWhVHrTxfqkpF7LXmnkDCFgLLgwfFyaES28uJ63WijBVrlgbSXe1H8lSwoKVywwPhUDDezMwKd2U3JbQgRJWOoHbEQrqGg5e0GclNO9AnkxnsvjxYlyKgTJNaBWpITHQ0i/nLr5M339k5846JELAeZ1nuZ+gffcrQyrsXO/DFULXjdkX0jS/FgaqMHMNWw5YQcuvnjDgoXJclZtbKcuh5TOWY8uhNN3QY+icQXozu6Wxc14wmDZgqN5wUrfgwwqkR0kh5kMbktAfVrzIhtXaxVWZA1JO/UiGmOFnUfJcp4GBNKxYETcGdhA66U0w6XG02260M6tQCPQHgM023IGzmHRxM3Uk1OjObgF5LliRVlxeCwt6eqYg3ywuiEMV8j/90s2MuGw4tGxmt7ny1rtPdPB3UrW2uk86G5ta/FQauVU+XG+3HyEtbqf9tNTBwmFN2HCcCN0POFdcxYAlQKxV43M5vapei8lQSf4yT/41JEEYkLt4WDFVsr1Cf/Jqwj5M2KFiBVs9zItpVSk5XGWP2tDppU/h/K9hcuvL+Hl8y0C0DIEdDBcKEy9L/jX0VhdmT/oH7IhdsmN2bfmZgnY86rBLHh702SXCa+bz5LLV2Xq8vbnRRl5S9/vmEhhXbKZBw+eA0MWszcbnp8/9z50flSyzNiuvygs5KeWPN+fiVJbZl6/sWoyr8eR0d1qYayttcg3oVw4LMSnH0DLIUX410R8ZZrvVgl1iOit2qYXiW54UwagXfVniaGdJ4aYiXv821NdRKLzlr2bJJXubsrdOCTpRvGCNOwZ/m1R5EHgDVdC9rfsaehpvQXebpQXjsHJglZ/FosqT2os0feXMOletH7Xno3cMDRKCUtoaBtnpmH83U+IxNkkVl9MZG1zqtJAev5im9w8EKinVRQ/CCTDhrcU8rcyqBDgpS0vPtdIx4bLuz4VJCmIZtMNzq26Aq5migBbl87h4FMrPg1hMjdo9glq5H0plYV8l+SiV9y9ULf7vZwWDf64mYDzQ89cJfdgBL8aTU7vl8X9XTKCycy2Kc/tQinAP3N7R6A383mXPp1iseu2npbOSrncenUIaJZbzztOnRU98Kb7CIoev5wul9UB1kmiSO1/vSRlojFuGiHhHJO0UdrSMNvKo0LrhSUgkLFGNMOqgQ6iYVDInkr2v+B9Gsf0DaJ3YtVFvBwIJ808MEskfb4CKlgYlamOF+bDR1+mhchJGQgWiDqPGH3Wwf2sy8DfUXhA927Dw6bpplgya2ADAlaiqQwfBuULCoULxP6rEM69rGNQQ8nk3HzKs2qZPGh8m/Kzizwp+NeH7iuOZY1hxe7rQp4qkam07EvQ5rwD+3LNsYXJSicmp1TUsAW3bWYXsfQzuYaKFM0r07ILXM0qANtd5+rTsVXMuwXctWvzfheVYnsIe+dsYsjj64fjVnPGFOVjtrb3mBwKDKvcmGsJ2Pe4nFT8ZJSl0xnqK5BiXV/TiRpoNJIfredHfzgj3gz7jAUhgOjgDxkl/DaMhg2PfVtaBrq5gRNxgvpa8w94C4OgYQVkuIx7V0CHxrTnk+MNJLXnjSjuDAxLZa4ay/6fV1vAe3aCGEP4cHKhQhYXP/VllKx2SR9cXv7f22pZr+tIVfyAySQj06DvP/DvPuJox8pyAVf9a8g12qHR9iNoG1AO+wZ4+EM7B7ZQl+6rVWd/odh4/9ieRq0l0bT7f96DFZyrZJ4chX4/XEUvjd9n7Lue8aywb0AWs4NCqXrIP4kHO53iYrZCuzxyhzfIHL9JKp2eCtmRPZmDG1fvSQOpVQiSXPgqcSDbgHytrdRoY0qNftjNcyznfaow9DbOtbmW+v2jkMKhqUMwBH6ANOlj33yaIdxuwA8RJXE7SGzj1hDGxVueFL7Blry8W4xnW9bVlNXLANlyKRylRHW3ENMY65FKpj3p3QuVNFg8fUnH9HUyRMOVKBpOoCBbRPrWbgAxIWe6HFqqkrXwnXq6/GSYnVHE1p1MjuIPRF3r0Sz364eD33dBXGtv+cLPddtMg87L0rjlQwd6jN9CPlY+DdLOgk9X7HFS4TpT10kwGOHyNkiJNb4LX3g+dqXVRcWLSwhp1Wy389wksGd1KmCiPjclgwQod4l05a6efi/BceG0Drm2G1x5nWKt+Uq8VpIJqbOHjtBHgicNoeukkcm7lsPmHnFIB5ZdGlcKSw3N5fwdYk7JZUwr1oP/cOd0ybVIY1onkA5/g9MBFefcOnIvpRPIDNi6TgzTNrO7gAtKaW78Zxes0P7UVPGUkxmtJRMaymLUuoE+vZpYF+R7rlSxKWI8C12MJCxQMmLat7ay+KtlruuKu6suy1Vp5dpGk9dX5hnjTziKNjE1V2otJgkIqIMei0B9DOkHoe2LAe09ddWgx9u66BkiOT4Jd8VNN/hFliTSZJQ3D3zByNHXueM8k00EHySh+ezP7A4iU3HD+NtT0JSA++IOTpILeYNhJUMAUWLz/Rtd8m/izRKg7V6Hu3LNIJjPnjaDbPcXwNPgCkMLcBNqOotqOtX27Q6pjuKH+AT17TyTYg3r3/1jlAOCPt7b7ybnUBL6dPiaXvZwsUYRWOllyqDg8xDRNr37vif5n3dIAo7OZWI2pr9lEcqzTjGt4tIQgcJkjDj/NxmVCj0I4FA7FbDIWBI0wPj38AJiwTEM9haDBJr85DbEzuFgCLn04HvQ8QJ8cLllFH7L65gJ+c2nkgnukh9bmttZoN0lbZK431uHU7K2Q8yY4UYX1cbB8aOdjFlVPeWZwaknwtvnHW9tMxZnQVERglrKVylA71EgasbGSNFZytYh87NDYLTvJCIFMXjd+AenjsuMaOPdLIKDXn/8dtQKdFfN2K3yQ88EaIdz0jwIMHps3ACmGWcXmxuKBhzFWzPlvV+zDBZ7HbaWZcyycSE7leAoOoQq8QxLOmQpcQgW4hErwB90O/0pINIG52E88gwalaLPX3hfjaQFhdG02miXFmNW9cGVucuAsQIW47+fApOHrs7792GQBbCBIMbfmcwG+zr21Q7a3dmhx1a/WLtirtQveZTn/LhkojRuav3JmMSE2FcN3yXN8tsQPiMXidxBgCusP/xN5Qp37cAMG93epreXgluvpE9UxRAN+njDV1AaTVXVpM2CNBc0wSu6rtYseNsXIcqMD6+YYKhijof5asBzZsWOkDoAuCaUwG0CgYUAqbB0hs1YL/gspQFqtDydJdG1taH6YMH0205bzgVv+P64SuGaON2vIesyO+GBNTtBdckSdvUf8IGUNRN6zdBZxd/MDNguYuzlkX187H0+SIzbTxkbLIO31vks+i2lNbAMgvhEiG5vovcFNE3B5pybI7zhi+GZv+Yyy2poasDe+drYL3qbsudTd4Hqg/yYjz8nJkL1NeyvXa/J7JSfDVuvNP5/Dqs/5cwkvv2FveK5H/ZBfniQz9iZlZ/rXcy25D1uts1YruQ4pxzvz+TUhHF/h/BAjI/1lTSuON6b4E245+vEVzs/cC4SGHK/r582Q7/NLw+OA8wtOaODExyFMdKHMFpGya8tMrxQ+XiYpgzb3k2vgt9Kf2IfndI8kuhbMF5ol+P2XDfdY8A0IrkS4zCUHXwOf9a75NSFf6KXXlIuvA1MDPGnJjdQzPbtmSo6q7HqtPAGAwTs5qlg1vXAXDqcXixQNlhbbrLsqQjjbPCdME4xcUoIR7MIdfvll9rW3s2YKJuXxnTWoA4vvHU4v+M5aNb1YLIAifOVfBfu14P8qzI7oRV6JIk+gsFGLhduleAU4gy6RdxMq77p3yTsXV9Qk7tzNn5R2DXFot7Tpd8nXSTsEbcfGfI7/ruv2sD043sSNOoF4yk/D3l/dPH+XfDNL8LufJ9wiNsY5RhBSV3VqWdvCyz3reG+1kgMbHSuvEhM+6/FXt/CWQs6PO0lL4SFgLK3smShxynKYLxP+saSmQZqyCseQ7rduPGlgagH5FCbGww3ebdSCGFiz8qfCTvzcTvwZF1/yr70iMeKc3bjYro+VOPmmTQIn3xawL5vxh3IrvY8vFsmJaq0bszdoctpQwuKOZolqdbc6T9bb1kRetjaIvfVF0X8nHj7MEoulkWmGP9H5QebW2CjTiUTolxwiCsr4bioHxuDBbdDjneuAXDeGNZGExubS+2F+CZUTBroWedBNdMjJ47xsdHpfFz3ADJnDiLxKTjD8zo0lDh705Xr3n6q/3s2UV64U/7OwYCOjdxwrlvMTpCaLZz06PZarzeud1Mb4fkcdBBc7+zxMCqc3s50hxnjZY1wOwJvvks9wGBBUES8EH15yx2JoeBAWROFmu/fjmXlGNEVmhp0G2OYulxQ/rqwNYFgkARMF64Imf6Ydgt004K4Ds2S3DgYyGbxUkEppPcXykKvbZ22oVkIPp3325gqPtQ73Xec18HGfHlfr84wZM8iprF7IYjwzFrzdYnqOw0k72JE8iCAq8zn0abgLgoCzToCVA0drCQFn2noArrdimEBmX+gxOMZ1Uxp7PwWTCsgwibAsYXpPWPPkYlE1Efi/q6V/AocrLsAeJdG7CW4huedkZsQ2/PFRVj0NT/T51Uqd7yFBhBVRct0bzL+R9kpstErn8wQce21WIqUnRhAUswhVgDwJaO2FIzqZIMVseXuo4QNP3Jg0N3Axz3nshW7BCc25m1v834qhwwb00KFsqVTnV0lem/30tU3mUvNntVrr7fY/MQm3LPsUEmeQRtmzYs4VODT14wnAJC0ijjTRSNnKydPqBHwKkv+q49prC6mqL6TpjGKzYxbFNiVhV3reFD+gB1KGxSvKoBr4Jtq92ylqa4nlWI0hp1fSwktSeERPsTR/Yrcb3qPEtLfyHax3NtJ00TxRag0fmYab3ehBqcUgtPDKuF0+GSPkDnqhC0xXc4m/96CrPJgUqS1g3D5NwOEEeVy8A+mTBWb0r0r+acJl9mnCP030UzAkmJ59p5jPkx1cOvksoYO940OWVi6LVqso0xuoRRtMPB5sv9Jhgl+VEWWGNvu4TKyNGlqqN8C2P2hCutJwITGIIidLrVfwpPP0qfaeb3TnMn3YSR91WNHiZevfSd769wx8mkWr2+6sd7ceb3T65Pe8kxX9Yt7N2ovCKC0KWj7ME4EYP01Ax5GQA3FrAm1G/SJrM3EL3HU+j5BsCIpIWVK01j1sFMqYz8Mie1ECVZX2cKiJtB2CU2EaqCnw101RcpwhRjlp9/YMEOXlLNFA2j2V9jzmCWF6RrWIh4ymzGUFB4wiOMQKjVfR160K42dVmel8uLxEWe0nHvDuJZgbQhpVvzA6JZKOwswDIDAvF78bt6FWuObzHQx7sS1JYMJTqZH7+AaXFzacMFZ627/LOswHGJcj2KsRup1e+2nRs7lz9Swr4JDZefo0ZwNegmptElYlyUwLcmjerCVc8+AhfnmRzNBdOniKYMkQSjznME1b/N8zbZ/eB/mJSeqGYPuH3aDqK0hzdeukk42TjlL4mtR3GmJtcJt/Dcht5BSRbu7XH6BusPuaJzXEjq4HjEnQtl7VeqSYMflGb9oaOBRBrQJXiJGmzuLtTPH30wDtCvE3xTgkHt7eWN/c2NzKFJcnkYSHhy0MR/BRAFCEVfSg1LGTDS2t4i5RrFr8hW50H+o29m2X7CW5E9HhOvLZO+9piXYHEhVNHpTWJciT8DowIgTZT/UW7UnjhZ/K+7IvfnougwDSWB/E9INYYigGNOteXCvLzxC0m3PVj4Yw04GFXsj6roTmBk0zwQQrVBLBZnnzfJYQ470/qfUflMmbKRvO0gzlNnnvgwH57SnudkjJjyc962qSfDpmUxPzMR3DGXt7e9seg6djhMWDhOTSTqo94q/Jc5ozN6KNk5yvlj/OB1O1GlxzVHLY0eQGSQ2dnY6BZY7qlOd5yIIf2rhDIlqTythHK2mC3gr/YUrbKnhlbBZybTyEWLIy4MgdTYvzVbwHijyhfA2YqvHcqA8bu9Pi/IWocEu5hSOWYrfOZzTE3vihVsurwfkYkHAY4UZJREsLEc3zJCkh6+d8jlNrTWibN9NKDbxfyaKXo+6dVDwnz/ZzcMdD857pl7I8ogf291ZTkoEo0axC2slnbAHQ8ME4WdXlrzL3wyDqgWfXGmX/f+q+tbltY0n0+/4KknIxwNGYluwkJwfMmGVLdqzIr0iWE0er0oLkkIQNAjQeFGUR+9tvdfc8QVCWc7K7dctVFgHMs6dnpt8NmQODJYujvBCJyPLg/AbSxcM2Jftz9cV2FQMOTwn832bYhhhLpDZk35DPBnMIpzvzA2ctcr+fZ17MbuQNDBbq4KYWDNlcFLN0HOQ9+sFo5EFW0dh18AmJoVmNHV72FjScQxocWDpuHwX79lFkbOj7FZPClHdhNhVFkFcXKEuTJMkHoGg/ZD8/K5Vo7kOmZHO/ZPxZef4hu2CLJf8l6xXpy/RKZAdhDt56U3h3vnfRK9KzxUK93v0l6+UY63nf778svMWSddKksztd+tXLwvtjBI9PkmgeSpVOx2cvC++9+/6oEEQA0ddj9ytpjPBLZzyMMYtxB0ocpuUwFgf4TJ9R7h4l+BWTjtvv07LAD0/jMqP3V1N4Ni4HJ6UcwZfaB2sIh7VPB3hr0Lc/R+43mu8i8Tpp8iotc/EMzOw67ByCneSCBkQ/lyLrXDhlX4pwKb5e9m0aQaO65QU9U3n10FRDt39bjT9iqHEwAw0LBFbHHy1cgpYEdksBt4WHaOuTuB6nVwn8LRctrc6jqh0MDV54nVbHV62Trq3D9Cq1MIb6qpiLpGyNs3AqkrHuzW0eAYLP+OtuHdJRfIRHPjvvoByLFkwk4w7rfBLXi0zkeYdhJntZsLMI80JYQDHVnmE1tx0DFmvE2Gx96E1DtBon3HOaR23j39bBmQy4Y/dADiR/sQsKORvyTjhMs6I1ChMIE6v+gnSinM5aYxnQR+KUmC+KSIxbIhll14sCf43hfxBjtOI0HIsxHIHyJ0T6148Ej0VY5qKFXcF/UTJtLbJ0isMFTz7ZUSby6Ito5UJ8EmP8AyXzIoxjeCZ+rQXkngTCMo3LuaotvZXs+bLlUkn/vM4QcatIp9NYtIiibI3iFMSxyTKMozEOuEU6PfkH+8MadrMqJclnkHdqCmChJWXgb0f6ODe2gGiILUBZdXlI2XiRq9e3qnKNzKQ0E+XC2ikI6ANq7z6wn3zWn92/b+tyGFhnqmuZnRiXSrqAoDXIv6O6Y2BFkHW7eS/KwRwunMozPl0sxNjztQdexpcsd9viJ6i9yLzcN1aTzyLvzK/qJfFGzviw0sLzJo0Uju0Ok2D/lzOwc3pNhEPTnhflRb/mWY8vFT5qh5DdzuXlsBwOY9HpK1H8eu1NSW/wEFUtKt+URUN/qonS9/rAoodr/r3PpjMP81Ap+WCa8w7liX2JkIJdsoumD1mYjNO55/eK9LTIomTqPfrRl2TDQwu3/8hkKNW2OE/zC/8G/4A8Khtt5ssCPXMbqN/agQ+k1lIL3j8BGQU++D6jn3soXCfAFLyenC+o5eDrF8Z/DAYDamE1qE+Zt9E5dFU42b+mM2PQKUUE72egAFQiARJuvV+69sQ5P17W44LyKK1AeGw4RWgXtZ9Sy9seles1EOCdIi1HMzwXwUNGvwHGQr24mgkBrJaHAi8WDkBGLY1WBODCMzgsXkq0R/3MzShcFGUmgB6V0dmCvPKDLcUhOPYdGr1LS7bnyPvM5oLo7ArJKqG7b/zjlSpC67Rl+jKSAWs1kGW9CYeddLeckbkp/Pm+IWKoa4ZFhxywW7Risj7F/52ZwBwzV6Q55DPt3Tmk/oZYrdudbesKe1HN8ZnSftXzsuEIxom3BA7PnShYclHHbEgh7eHPj/Tnofr7T1CPZXymXfllHjXDplZ2oOnhyJY1kGFYxs54hrYNYI+jrplL/utIxQYBGyyNHsr+ahixYxNJWCUi1zQZOFqsIo9km76TuEkSKTJJOVIoneCKj6Z2xiDFIgTHnH532BWflxtFgByGMkPgFjaK0DWP36i3cFKIjJ7rZYlhobA0w7Io0gTNbZyhh+VKFsNHzebQoyGzrGfcytYzTNY84fCtR6Dn6dkirmGsz0b2WIHaVmMgwtt5KnQr+LyKCusxRobCPFt9wiMdRuo5xaU5Wtid4wlFNJMsh2/MIMwRZj3Ldq/4zF7n1h8jEra+l3+PR8EVL5wif8KrhYMdRJDJ5jV1Bq3fc4ZKZ2dwxZdO9VG6uFZg1gtAfENwxROn7DQtJLMlD1VZPE7z5g/6pQUg+c7CDfnGgpLF37kvzPrIN7RfDkbuoiBhGlhor15d8WtKT3hkiFGwYWwfyUApCpwk3bfg6bN3/GhwqVxXLnc7B2qqKOS77B/BkaGI0I/8hL3pf3RPz6f8Ixwgb/hTS/H6FGzkwil72sZo6vDn4Y/y7z/X6zf6Nn+nf3lPeRl6H9k7H2qR5OqIRFC/wfun7A14Dhwon/OP/KM69fZ+PpL0ZLfrXSLRdeVdsmMSCMXszGefHWHWpSXMOqqATqBbi/wmMYEfnIkcwWU2roSfvW7syi5UFvUywLFekjN9WXa73jHHXHAQ0IQITEgwNwELDxKBgmponHjH/np9fD5JLgwZCyO8Wq8vfZzjWY/sZznnZ4OzAN84FJM/uHTtbS/lnfE71guoOrsaNA+pSJXV8RU/Ycf8eICjInnfsREtHgg+9o59dsSPccHh04FYr49o4Y9wwekvuoscSzFk4F0RaX0MZslXbc6PSctzxJ+N2FNek7684zXRzUcJ8g7z6vBuWCXo+YgfyIZdwcs7viG9+ahrd9CS+ErtjyAPvSufveHH9ptjnxHSHXlP2cfdDh3A7Iow77JXECdxINilC2j+hj2VHgaJdwa4d4IDpbbeQVt01rNjautItfWGHdWaOoBNd4TDfcquut1jvwhQA3rEV0v2jl+xj/yYvQHTOv6u/7T/lB95T33/ze5u/6nFu97jH/v3+vf4kXfP95/u7lJUgL2f39x/2vff8SPvnc/e3L+vXj+9/6bvf+RH3kefPVWv4TuuJWzuj+v1R40u+MJYl/o3R/ydTkwnW6fWqiMyqEW+kX73r3Q7y5mHYVXYEXJMBh8PhFvmQLBjKAS2hGpTnwzy0Dvx9Q7glzoHeLdrftfkrldc8zid9RqfSFkBdQpiXDqTCNI5Ahyngn8Z9ZX916uRd+lDHK/DkT8V/HJK1gxTwd9MkeJ9zt9Naa72cFgb7OTdgbR1x7CloWN4M5qJ0adhutKjgZdZOI7SzuCk281L70TZMb+TiedxhH4wFfwESd+poJdT4G1PwEDsauR9ZlOBqGfi0XW7z8HxDFymac6KSoNYuBYsknI+FBm8raU8QuWNOuGj0rtkqiy7pI9+JanO5/XFUmSoph4BtM/99fq5Svj4bBwVYP8KQyiyUuDeP034czYs+QlbhNKRqZHOXIR8WPLTpB4Gyab8Tkud07i/Scg10IGagIOa++wXgKoBqaR3avwrUKmvplbspmaiWreF13+El8S09E0MKQWvutBS5lUVvEnG6aZhrYllg41KIHXdWkVGEd+sJaWdGq9mQvIftA2GyeASI6XA8jV26Qe4xgouoHL7JK4PSOH28OG/mupJ/UF/Brj+bgR14nQUoiK58ykFmcUwWa9n+NxQczDDPjfG0u0Ok27XiwQ/BXu5wFvE/IxNSt5BfAY3m0U8WEgFZrCIbUcWNkzI0uU5H+XeCYOo8ns/PzcUjTSKfD3yZmCKuo2omQmLqnle+SwSg5nA4I48EgGM7skI2MDIMuIzBSBHE8MpTAcfpwj64Bn9lWuAo6vJ7WGoM2HG+lwNtS7flzQrHV1b5/DcmsJMVD57bobH5hAvCjILIcYv4HwvXLV5qMRkRBgaNaUBTVDUNHN2EJtRXrccKgxJzEJkn+vmUBikzTXnhZBMQAzlRATlkvrNJfWbGZo3B5oX4xzmJpZpmeSzaFJ4OB2I/waSpVIFFDTlFG0sC/nMWC0r84q+idkj7QDOL8x0V0uVM8mVTbSUj6YV8Uc6goLVBAXx/cH8fPhPrbEXpKw2fSxn9Wj0KnkBCSnxnpsBZK18hZDcta/EPTFKhY1LyomdcF7JtVGMohsA0UkoT88ljXUpF2EpF+HELMKQn7B84J0AjGOW+exEwXhmr0WMOa18P8jX6+bCakFUSTABUtGzKpXemCxwXOV6YaN9Raf59ZI/+M/sP5PBgykbwu9yb29vb/2f5fPnzw8fTI24dmUZc3i2DQfKeMEoAy0yeplYxOFIeNdL9l//8V/mebjEtKy6veuZY3ta8BXISRl2A8ZhpuRrYbv9arlqrC4dsJzIRNJRxiKhNSq0mBim4+sORSNCFV+YibBD6XI6nfV6mWCGt8Cz6ktSwdi/QzOQ3LqAmx4Fqthqt4u1O51dJ6J9ZxSHeQ5I1wnmYHpELzpu3PtOEQ6PkrFYqUJFCMJesaqXG0eKdc9SzZ0vI3H1NF3Jp6toXMzk75mIprNCNlrLcNHBsH6dYDVCAyY3vzYcgkgU4ARlyCX/hgaHH3VjFTWWjVSfkNc8kJLWDlxx2EbYWa9Rbk95z8FbWzoIansS7cZAlR0jobDZGaHBdCi0TIdu7SXkZDyEZm6OTU8dUtJWJZDUmzZ6CYwrhTM+6LbWYOdjuAzzURYtlJ023Ftkq/3dk9YJHE2gr5q3rsK8VSZitRCjQozj65Yy0xn3WkeT1nVatkaks4Ti0ojH81vzMCnBn4WBGj2PxgJMt0HFicUy8bkUeXGqSsNNBWm3ZZP/+Z/fZQKC5kCFIm2BRhWPCogbuLDUXVi1FcpBUZnWDI3jMqvnMM5THCa0Rw3VrWB63/kdx29li/1MTES/BvqA8EkxQ/JMoCzrLCfLrVxGXlMfofqzZASsCJSxHhuLvkITG1WSnhoL0pWuCkqloSzoB54qKkzXYku3c93lvLm7QndVuN34/S3b5X9hY6QJWeEEoeX4liYow+bPi1rZUylk1YUnwlOiQiYaCwOx21QeDTZE7chKpiCQyePrU1EcJYnIXrx79ZKOorYVwkFCo63PtfW67XUuL2fFPAbKOfRrPm2QOBJDZ/CwR8VUdi2yzOypi6fdlHITM26CNaAcEDiy2pxeGRfRAg5i0VO/eaid32CY1m6w3srldbjGshBjaqgQ47/aSl4uUOVy4DK4v4cZ6nYDp9ALjJYcpYn7WUkCkRtxXh2A5EAoyX5kloluNbg77JO3LFIy6LLfruIo+fTCumfueFUo7G/aJk07gjoK6MaSmyPetjlen3qdWVEsggcPrq6uelePemk2fbD/r3/96wE202F2c26ulk5NmKCAvBBxjACzWPupVWBpwRdAdSKWIsvVG3Csz5IwPhF5WmYjkZ+IzyVY++ubbFTmVmuwoCIDl/3FLLS23V2RaLB56wGcgu1HTV8igdAaojCO06vnZRyfjjIh1J0b5tfJyJrnW8hPbdRaWRrnLpaZJ6OLklG43ce30QiYraNE/nC/noh5WgjoDay2LQLgdfoejHpQ1EBUD2aD1KqcVAljkvRVOi41iJN6vXSha4HxUn6UxFEi9G4Ix2+S+Fo/OsuX0WKPtdYqXZgHEc5jVJsShAsxP4XvnSD899bz66vpKq9AVgKWT50AyMH2XnOboKpv76PK/t9FuTvg28igS5ZeqZ9gHab3XZj8BfzvdttR/jp87YV+t7v/Mw//0uiy9OoU+5dEOkrP/gI5rIYyaOow+MrFvkgXpCaEK9fR/mE2a+F1rMcZcgWqSq0lPPOejIoScf5XSOFwt2MylHUa28tGxP98S3uyTlN7J9/a2NaWTmfp1Te1lEOFppbeRcU3DqrAGo1tAfX5TU0huVpraR4/hb9b2/nj1Uto66cHQIPni3AExqWreRwModpmay9DoBe+ubUYqm22dopFvr05+dttL8o7AaF1tMGrI83yTqzUJWOJVju1tAVe23uoI6v763V8vneBDDWIj+XvN8AYn+/jbxQry9+vUZMQ818XaDYDRm0xbTXYrLap2XF2q1jkdj7//0ui+VZJz7dLcZolN//TDMu3ME9/B03+NVr7G/AabDXTERhbvrlKQDogsuIa/B1J5wmozTWa7+Nv5OVzHkMorPz3qJh5WtANATViZRfK8oHaMPf/GUgTMfCNNZ5J4ErdlhlWz+MLshFolh6ou69uWZhBNs1G0Y1/o1pyr3srL0sMIhAxAONqCvgtABL29Wo6tt8Cx9Bg5RjaGULstsNbaSZ9FNiGwkVRS0ZZyENgHC0d4oZ+LqfaVknLDEP5V39SRG3kYst8StQBGuprsgBpPRW/LsSsNBguADX1GUwu9tEiroY7ma/8m+Pz7ELGrsODQSWlV6cZCBrDmko0z0anougEeV1Vqs+J20+7WiDHR/8EY14T+lXI4L8zFjOZ+jE3kifZOQTDxF+qEOR3sMrIAuprX2U2oK0HwiwEp3QX0DBcgjUmlzEYhjJ/mEwBlmZeuB2koQTpGebxwPh8LkhDCVIUnQU5P3Mk0kgyzNyXI8W8D933dd7+xP0s2dSssZKUESzrPd1t5axZNS+ie3DJ9QjZmVnK+Qgzt1D+nBnLMRWWvTrSCqO+PCzkM56Zpci3L0UOV/mSx5BLYSmHq1Yhl6uggbTcDqSZ+9HIjkK+rM8xZ0s9x4JnLOYzZomY2u1QhasaTOH+wxeY/kOnjtfv0aTbgYnWnmxAJeM5Dw1UZtuhMlNQmW1CZeZCJbwNKrn70aBOHZa3INGyEYn+tb8Ng2YWdKdEWQHt5cAoXZCWAMAw3A6GIYU1js+HFyysgWGoTj1EQRLuqd9/Qb5Xm8QQU1nRJOyBj6MwTqd3Y/7gUZqm6kfwe6pTO9Ekw2OGIEM0ZODcGPaBEY1FquU9YFWEQKRMRZ9DO1XRRHifQ8hTVOsO8kzdekE56FSEEcgE3LnZRcR8aIQrKE3T12Ly6dZ+aBK4VciWNtQCuqGxzY619sxY3Ez18Qdeb/qmzsK5otAyI5W60o2BYRAIfAhmJ9sR70Qh3skm4p1s6FT/hhv0ZBPfLIoyL9E9BgZ9RoPeGPGZGvEZjlj7P0kO6AzblzSjycKFRgBbWlyqFpcaBvrWXprh2nHblhuOPf9zRBbRBTI2GZG++Ie869mygSxgZ+bwvaJZy3i58fkVkgEbQLjyu93PLm1wpddf3fgb1/nW83jIP+t1DTf7Wq8lfK+U8/9ny1P9EvWLyikkPL/EIcPgLy/YRmtonXzVlsKxzzUkvpSTIFom41f2jlZUz1UjgXPivq8TOGfu56W6nq9uI3Cu/iKBc/UNBA7YrH7WGAwmqwTesPSElSQQ+J9GGueKz/iSXxoUMjT7EElz1kC6d7vDZmJ9E1MMyXLFh9uxJDNYkiksGVJO7JwQBOOsh0BQDYmuChuJrkzhxvB2suuSZ7ctXdZMds14pucADOJQQx7C0uOQC75E0ivkV+zSpbcgdSCmW2uHbd6mVK/1EhjgJdCP8eD8Iuh0ML3sFnrsyl69pV69nM64xgNQ23i1w83PCmDLrWtqULmRyliaGB+VpghDGhM64WZEAoaN9GGuFjC7nUK8VDmsmxbwyv1YH/BXd2B+V+owJx9hTSTmKHGrFiM0Yb7aTh9iZku9WJc8Pj9uWKxjv9u93L5Yxxo4x7fRje39ZnLwWC3UJS3UUC/UJQ+BOr2SRGoTBQtOPVfgKqLW6+pbSNlLTbReNpKyl18nZS9ZyK5cOhZlFBaV9m2kmE3yfRNZ5tCHd6HR0JeKFh9W/qhh5Y9uX3n4LCFxZFaxrwlAvYontIonDat4cpdV/Fa6kFw9v5U/P6mtZjOVCGA7EAZuB6IBcAdCTkvRipvAwyKShDzQ2YQ0AM8IgAC9M4Le2QXbbOXMX68vIc0h/VHdXenf67UmU2l2LoEKkf/NXN41TOXd7TjwzuDAuxoOfDZT+ExT+Nw0hc/OFKifK6V5lE1/VkujCeIh2oPWHQC07pmM3rTuPFE/zeaUvJpN8VqbVqds1Retziqrh/Bx6dlqioXIQEkPXCnoTp4lRRaJ/Ok1+fvbdnoK8hDjuoD8Nfy2ul4nk6YUHZD+fDVtbgERgSYiO42+gM1x3ouSqIjCIs3QBmzJ856KhoJ5YzBTxxCuNhrZjO9hoUzkizQBX7QxC3f5fmO/Q+pXJ+aAoJLQ6snjpbJMRmHg0B3WZz50h9U/wzF8hst3yIdO57NdfvYPb/jzcrAfeMv7J/4Db3gffHfAZPP+/ZAVu/ynf3jZ7sx/4JnJPdgXj3wmdnchH7f0p0QXSHhSZr8PxIN98aOKHpeEy2gKQwI/m4R8VdC7tOlDD60MouQTs8yRpcrHH4jgBzRwljkTIH0C0Uc6ukZuR6f7SmQIixFswvtNxePDvb29B8gEyh72+1tKonoSImfgf3B6yhoPa7jf2rNx38QIpKS8Naa0oes5MqLbG7fyne53u4U0wBTRNHlDgpvBnp2B9HnmGHALx7paekcmqTwHtAqu0BpAo7tr/lhX3jkfpRbP+rjlLnJSx24rZBLObishlZl0DCNe3avjE0S6lSEUyY1Mht9R8EFHBum1tkgX6FvQwfQE97IBpEK7l3GBdL8n28Y4GNDisxmX88wF7vC0dOLgD8xrqTtjr5eqyigWYdZUyf6gq+me3mbpPMrdwIvynSp8oPv4DLnHX0WjLC3C/JNTx/2kNLevZz93ys5AR5EwO/E1RLnI03gpPIpdSEHS/R5F0Hm39Kvg2cyA/R16lxgA2JEpiPYQtvPOVezGpOSdmQjHHVPgwA0kiQe+zK0GekwIMXxqslNuhG/MIVSQdZb8hDI4OPLDIsTot50H9zqU0L3zoNvR0Un2jCkiNZb77NcEchFKeiG8f19H+sfaupl7A/3rv/WvtvrV7fjh7m7fqYr6e/9D6Inbk0H5tWoAKkj4Uq8GH9iH0LNiPGc87k2iLC9wNn0TyTizYYgZrZQjaj87j8KL9RqCq3ROD06O3r7rqKd3H14+Uw8vj14fg24WfJNdt1X0ngXDh3wmBBwPsQPSDPQUMxm4MebGiaMBEvDF78c8N8GwcTkMprxzMEX0hcGUcBNTYicZ0QAy2/Yu8yLMZ2J8GOVgBgjqcBh8b0zPrPYM52kiOhDOuvZho6n1ugOaRzdwJ5mE+NJBI940fJUF/MAZLOQCcIYL1gKciiCnzazfHJTU9rNbDcaFKtKwYZOE7iaxLjeiZITaAehRcq9DLlyA/urXf+tfgP5id7cf81AvoJU9O7OTB1hoisme4B6wF0vmKLBX1O8XfX1G6CymbhkJFsBseSlbZhgvnj05lD+fvjn80Al+z+D4iCGCjo7CQ/e23AlUmDZC4JbADUEhZ762JXxVs6ofXRZx8cYSdqOvvUMe7WunxJiOwGZfdkjf7L6haF8hWryoOrBYR6/fnsFOF9qnXNrVyrAU+tgLVbywKLyoEzzEleNn1y4DzWAXEIJGNqdv5L7Fb6BgUdQ2TIahPrMaCOEid3sAtL2/yMRIjEUCbIKJDAXyQSDlMaq02zoZmLc59/Ie/FYsl3riHRkEmV74my2MsjTP0wxoIdUQvnqDr2TwCNmE9aGhJTIilG3gg1sbXzXDTxqaISbcDSy1+pJTbFwBsKCA1AU0sDwbucPKs1HTZEDwr+aCOOtMBSIT/H2w9LvdbBMjyHTd73a/ARtrBLmoTGIfOw6EoSLVLpFB0cgLyh5pp7NLL9FduF4PmnJhgMoRODEyHbRdpqFQY8QT+bDwhHMYMu3ZS/vV8u81h8rJ0sp8hDPqOI7AtYOmzfmjPtwLnvNuH1bubodHt9sGv9tbx+uMQM/ZHIRWjgd3bD/922Mr/s2xHWUO/4oO7IoYtB7/26Jt326p0narDBA1XHIInBKuMbkNTAPU3MC5maYvnRw69biKeDhYQ/KF9Ms+gaw2vNBkJpVqa6J2S79+4Vk5QUNu0/yQWKTR/q9z+OaVtGx8iVFlwcS36seb1nmNJZk75tC6Lw8x+Q0hCbGI6BFvL+tNPQJlX+6CfXJMfmSujAIpItolBA/MRoH0fqHXq9CLXWjKvyB6X/54ros977itEwtSKBbE3q5aCoCMp0hrTO4Jyjxqk7Pyuuz1hUy649J3Gi9gOn3FT2xyQpgSSg1oY2/0iy0UYPs2WhABsrtb1UbdfEpd6gmC52qUlvm/O8l7myxZE8PWBABRm/ADPeMH9qRqI22e2JNZ3UaUT3JMOKBlWPOYrnG+mRS4LWqC/O9/eOj7tZsc2UPVArKEDdUebVRDHkxVg4emat/b1ZpTX33/w75jIA9MnZH1wl4K1VWX91Vi376/YbMLAR+84nzvwu/HJSQuQwlqgSFHXoUL9mqmE8aZlB22MFGJI+F2hUyElJ7BTmBhf/HALecr0kcYwSTmv/TG/V96Y34zCZ4sWRa8WrLD4GrJDoIvS/YyOFyyeXC2ZH8EL5fsNHixZK+Cp8vKjPKJEVFN4t7EAxPsBBKkm7AalqTzlZVhbQYiGCunmko1+AP+VHGpICfG4FcIKB1M4l6mYPc+UXIihVaPQfiDJIp6YwZ5NXOjEL9HKbmxeiuM4LDbLRQr8gay9PVz/h2Q8+eZiHnnu12x+13n4hyp6c53uzk8faeExbHdjJfv8u/OLRIQysdY3mevZhhfOIcQyq9mGDY5h2v6JhNxIJhFDAYxg86ComJh73MpsmuKdg/2C75JSlXwsJ6RBJkQnxWQS5AewKLsV0zbFuJe6oULCFVO3BpmFLWEWrhSk7h3CNlFrmZeZ5zkQHlPRDGadWQ0H4v//SJv60ncO6CAL1AJSHWSrkMVW9RxqGm3Sdx7KX/3a+sjrOWwlmGRCbSKuzgPcwArrhSCtk90LeqBZDoTfDhFg+oBLQq+ybNRLgpZ2ynlu4sqP0VfRL65vtQUfnNbglfUEMRdgaIKZ97AzU6DlUR2vy5oJ/Yn48e4RdzoosjYZPxP+FSdFYhHmHdN8FMP8UeDR2LOrRAhuWsgWJgDjsU+OyswAWWGhskbKKcTGhWahYW4J7Vyv4Rgh68K0aA3S32Spf5+7D0zyDjHXzL5EyEWZFDS+UzNIdALczvMSy/MAzV25qDfHB1xG5Aw9J3Twaw0s1ZZGcujOepVmn2KhQn0GiWF+wr8qKORgJfaATmfhZkYO6/c7zUsAQmGxhTUfSlMcWdC+CIq2Lw2FsTbDh5f3/z/e5NCkVQTHvlKqM3j7cgUusgUwuQ2kMn1hHlhH1Sn2w4qdUrNEy/0e7M0ytGv6hRlPExt5YKDuFK5lsuQ4Tm65WXo5NaeqeBRN5IxCfaYXB90T6ooNnjzhvOXPVmL/0C8jFlqS9ok15ltSFPkCeDF/KyQg4LQRCkG+urLiOPb9mr/V+ENEchDC8jD3uUCaRup6DG6lBN25t8Meyl6dfMTBj/R8pmfVVBvk4UiJLXDhKvprvn+ljrSlrq50sPKZ9bj92yWezPw6vCrGUf3LRduOirbjI3SMimCfYY6t2BZsVzul5mDPC/NQfTH7QfRHBISWIiD6A75HXEPs0zmDQZrS8g3yRs2Qe77LFM3QZ6N8FS/TkZBew/3tFeodc1hXeOU7ulsc8PIzYZ7JMM1zaw1bdgyEOot0yCTh6YGV1YDFyKyTHSMEeAsiD01EHv1fwkxRnOhE7Lz/w/8vljpIuhI8nKeCp0yfTAGuhObwAMnr/NF3/8IiaddYwTLPo3EuoEtSHKIFodfiXvmdHFobPWZxNL60vUKOCjpNcSgmydevnmawgkvc2iH67UXOlvVAhtlmUfI7TmQi2WK7RCytPqBrA6k0B1q10T8qB/ZEOdvmaD1oQkwICUw85fkYTMMQOlpkiDMMCJj7rJ46zUUu+0Ya5jitmunYhkCTbAZCM95vnkBQSDObred9S4XEHZTp8PhGZPZKMzthAQGECSCUgbXCFckvWg18a4iiLh8kS0yZxDzegrJPoO4p3+zuRhHYRD38C+2BJECZGvwkwE/k2Uie5vG0Qgquy8qRQdhpMtsvf689HIG3rJyRhAqsaC4fw2miT88/Ak8bDUzPMN0uFZCkXrxfznFrc2lKDoV2TDu4TmFYRrBF7SJA7VI20ZT2MKKfuIVcGrGTXtOHqtbN139sPof2nVbBDTff4/peKwYBpblx3d1Svw7Uw4RVpUztL21Ty7Ogd3vXFgi6UM7/eipd1MxwW42yShh7W5mftJMLB74s6UUFbUNtZXdpX0BgytgcP4g1NtqP0D5bjP1G/ZkO7xgxV2oKxWK1SWyijsRWZt1gdayublYcXOikZszQPrTXtBz0Nc5rJWVa8paUYmW57hLLjq7lpLjbGYrjXqIa7u7rDDnFXdcUEgY5bLmMilWfcnw6zmiA2Def2uZAJ7nJA1AhkHbKppOQ8mPhH26tRG7YoVdFAtTHoObCGdfJ3Ri4j7aQD2TvbhunLJegz1UjYoh2w05MOKe5Kmc+0Arh8zpWPg2FEOLLqANBTmp6reb2LxHcor3kRkgOZcH0OmFfbkQjZX1Q34IsUqYlztkWpF64DHCsjvNWW4WbFPyZ5k+vrfyMks29G9mipdZspnmZYaVS/4BIDfnM8u97DZYZo3XAPL3eP7jpEUzWzzwrIZymBkkh/UDcFa0YIWsnhdKvPNZTGADqeQm2DDt/C00b46Tzq1JN+zy3JliLh1c8T5wyMttJ/8jJremL8N+aqmxQ4p5NXh3v1cZvkJu+t+yKhsYrreQqWplp861qECqCMI61/Ekjr1t101tW18w6zyxXn/nYyZGUjYMwnOTX1FGMMk4EH6b2QpNzkUMPo06pVwUDinKCx+c3ZXyFG1NlAp+kG3NSi2YY4MHMXN47KogiOsJYlp0u2phmUvZJ39BDNaN2DSbQIMf+23hWFAwUSPkrFq1D0XtBQYjN9YqWIWeC2myYgYY/18NUJO4VjXzrjC/SVsyz2s61xf2HTjP1Y0nGWullco5VKSHPsk3MEwMIgd8ZKEUXsWKKNRVdWm68CStb8K7oxe+JK0pyElMBKY8T8BRIpyC1YOH+Qn3+rlxVcgVNmfosYl8rCctPbPz38WFtBXH0wesOhtssOpGbFBMR/Q6OzlC04rtNvjG9NRpu/DBHLE/42J31ldJ/wBCM7+/HCwpdnrmBzT1GTvPLnytIA8tuZFeoMbjV56m9cTwaC+P/PmgfhmgJrX1uCWtsoKa9uY325ImljYEPTAsAp+eth1jv71/V+lAe69uzqVF7ebIaTfZ7AOF4Hww9muOiZPqQXPjuoVMxN/aMjykCVhn0C+8aWR3mhbMhBMNTRI22vi76KnooKxhrjZ3Js+XujPSXlWXdhfE6jl6iutk1MzS6W8mkqSalPyJswIVa57ZbaIdnJZD6MFoFykjn7NYoLYntty5onbnPqL8ZJYm3WJ9UIqy5e6WAUiIhbcXbg62+6/grSe/+j18J3KMAYox7xpvftlhndyX0rLjBE4rlD9Broft1OlNwUHUwSwNttEmGwcRBWDwOagFJBea+RB8IbOyIt1H/glAbbB4kyyJN4hfZc8PA66dFeBtcAj6me1kcXYnGvhvIn7twVeiZ1Y759ZdZr+WVwpGSTav5f1CUlkFJMngNi39I0X0GaDHLtC3ccTxrTwvRPTAOzZJIRmZli4sXU8me+hgCYdjoBEtgWh0CkBqGllkvYaf0Xx6AE8DO3cx3dXNXipoSOR02dSLgCQhlK5cXafWq771W4YM8fyqYj+K73cLvy/H9fS6EHm3m6QSvEnKf3z4w97eP8CT0pe87C2DlOnQn6fZEarSIcabA55vncg3zMQzM3icpIMf9oKf9vb83cIYr1j1Ylu8sdGg7fgEm815zv2qClzTqUUu/UxnUU6zvX+fmQeVghheqMWHd+t1G9/VoeabxmzALHN8xzY+aPoev9SwQHD3bd99pAkLgCAi/rROXC5zRRU72xhrmWbaNp0sd+RU73bgCWS+7k9LYIZlL2yR9yAbBNCSluRnqT0X2w3snrKim+aWyDz2FX8G7yiSHqpMYz2Gaa5lv9pUb4PNBoYOj7e7sm9o/r/Xz37OFTmbKXJ2xnOIWejNtMmv9k1ar2c1KhbvPLQr7yRp0QrjuCMPPqAsG5g7kNyHfOZXIRiB5HLWQNDnNiMsFdK3Gd2jyouo2vU6RG8Gt83cl9LeGYbH1Hi9u8tCferCa5/lWw/esPmjPHhDn93GkOYbDKnYyNVODKlgG1WFw5A2iAYI8/8I+c29e3TDBy/Z2yyFcGMZMeEHaZKXc/V0KdVv6MAUvHWfH8KLYgZ2yQck+bYM7P5Y2jF5KfIOuCFGmKmL7yvo2im+Bb1cRMn0IBzN5IaWXdIDbEH4rPxfKfQptkqn1gtMxcLvqw7COIbQ9Wh4iG8Aum5blE6Q6wGZBwzr9lYVm8+jwupQNf02i1LkX/fovVgtInL7hoM050e5d39fYpNIijCZxmL8MkxETr3kMxHHp3S6IBxFRh8QY07ECKKKXx9K+tyqiB05b67CbG49AiDdrqgX552EAr3Zc4eJHC1MYE+On4z2KZUgfiAemA78sUiKaBKJ7G0mJtGKh/Q+Tc6SUVhOZwXS8DxXrw+sl5l6KecLc6UvM4kUkLpiTGhh1sB660wA7C/RVJ4P5eASyJEYi0K8A+d/SsyoTkxLszHbxFqMF/XZ8pSFWhq9Z6oAW4IN6T6eKm28Bdf84fdw5qwK7xHFo6CBw8WvkDoD/aTKrQYbgL8owRgVhCcHisp2Jl+4HzOd7JPmeyOzjgYhi/JDMcNQyALkVyOoDqYyv5UgeLbkf7/Z6iEx8AR/nTDhB68Ti/GZ1WJs898ghhQL9ZaR7jbmRR6E9T0GopfTmIzhFuE1Ut56xHEFVL0OF0JWsRmzAg+HetPxDCQvH2MMPFn4LLYyLAKBjRYOH0P5y7pzf7GjBQgXeMzkaRS9sYZd2+a0YsrQl10DvvWt3zymRHPxz8UgDgpLtRdKcZvsmmHHJidvtys/WITBTOUJlMbF+4/QdYaSDe4rU+RfYfo//nN/76effvzetkmegcLK+sRwCPrRGtsfd+vpGXiZFPxt7mnDFuy88K0sggbwcsZ03awwKLHu8f2ybujxqveu/6r3jsgxooJ/6S36RXZ980tvwR+yKNVVqkmUQJIv/JIxqJbbWtRvbfynb2jcKgkQW+Wqjzz1Qp/yUEpUeZ/JMLvXOTBixzOMyK7J119JHU9t+eFmjjFdUtZkRff7bvf+/s9/LnuYF+/NBEhJcrTJ3QyZGZio575yuXTC0sHCkhyo9YjcDK3jh2XqWHK3Rc8+SBTF9yn2Mufu8GXMahBUyLjNRH06pdb8IVvWrsE1f9if9VUQl/2ff360f39Kokf3Jjrfv1jzIZt1+X8Pq/eg+2HeSnR/VDxykfN54fm7P+ztsaehtwc8rrFCbj3aR2Ffa/9RsAT0zdhDH+LuGvRdMnwHjgCAw/BQIZRwhVmmef3a+sKHXInceFblutGGtSU5tG6BMrPaezJPncAMGXhesDJVPgTXdcYFPwFGXit2hY/RlEw4J1fBx5KHUGe1L6SHlnIfizl6MyiHnf1H8pyco8K77eYsBTd/kzJcVoEjA6tM715FarQ1Ht4FC7U2S55UA6e+TVUG0gnN9AvnpKBYOhQAWzZ2DbllXX73fVOIGycisAyaRgGA5QNlNZC/Ux1ibTNv9Shd6DROOupTWK7sFsbD2HlUya31k8yQI5914iWdsbuWcnsjypSKXR00p73GjApuFux6hu6GHNyLMNcpnhZhqWGwMImrFimoezIHdPKd1ax8oxuG5Vc5u2UWh1wbaGfCTmIkhMmzRukalbTfXroiLUczZxD4xsAYH20gL9O4nLuDcB42covrAJVHFtg3M4Q3Zv/elt/bwsRhXCpr83BSiMx6tnNAyzfm46SM4xyzjDkDnVC6OfJjC/OZ81GH6wmcqKnWg5yMDq6k8dRB4MKkJYPnVVRYj7EIl8J6pkxMBsvAR81+NnhNj6a4xB6rgnxjqqgXS9tkH7OemNW36l/NhEYU7M2eC76wRy8bt8vIV7KUBNNPMi6VyHMMBaeCEC8gVAReWkepC9LW29St3Moiut6+LNSHR7KoGOm6P/70/aMfvv/hx7qL/aOH1eYbvG6yFGSbX2K6Wg7l3zP5973RnR6bny9ifn7B/lxyc0q05PnQsvZbS+20ltljLXX8tdTB13IOipZ1RLT04dCSx2JLH4gtOApb7m5q1fdcS552LXXOtfCEa+GGaekd24JzujUqixYeay0aFu2LlnWqt+gs6uWLOCq8Tqvjm0v6WPIdtbtk2ykt4e1EdG3YOBKLDjdL13eC2Shnm4U394C9Td6HvbEA1tkrevL90djx75qmhV4kO/ldnOYbH44bW7PInz9Dl9O0IlSRC5foJWERSZ92iDcLHOvNME4hNPWbJCjYOJ3jRxBFBjHD6GOn13kh5s/jcJoHIbNaCDJGGWUPFNmQB+f5RWVpyDx0+yx869Un8HsDl+HAE716B2uQfXDRq7fLcsttVNHx6JZ0H+LMkZodFAMWL/jr0s7nXnO50+gjgfQl5n+G3peYmSpMKZst/FFe5Vj6sLG0hT+y9BmWPmss7SCQisGhV1fpJd6H0tnkz9B7H0rzLZnSyDRqWm1AK21CZjXPjq12j29tt0Ex/OfM+PYC2SwXjQjlGusPJp5IHOv3SE3HQIWiGYOkmAs+B9sSU130NHryguUjD5R+JDa09TN/gCFgZeKi2lFPJGVd8Onf3zQKwb+F/HZ6jTURHt9OhKu+K7u2S3APcy2P0EVcJqK937cd2etbrL/3s3Fol8sGDJV9ZviKwSEIgkjH+to31kQxzCIvshK16KTsB61KWXKwsKO+MfQYaPCxNvCJZWlxG9r2fZbYCydPEOYAkrX3+0Uvn0WTwlOxf9t71lmgzWsQTrBs6iy1Tozn4Nuurm3d3TD3vkBiLk9eLT47dD4e4sdD9fHM+XiGH8/Ux/ehVnP9OvNh/1mPVoBQrVEzc+TguurVEYBlKfg6pby9xxJQtqEN/2U+mgnwYzqQUj/P+vYagtTGSvjOni/BlRaW7mOdP36OO/xjLtm+j8jm/aVOrP30EWQ9sjnSwtlRMYqfhULCYhc4XCk6PC8uWAh/dvcvwBj1vNh9iHZgTXlscB+UqReu17F2XdXhzsi4UQt8IKGWrZsEGmQkvII98llxnz9ieeZl7EbKYsBRDNRjQc4o3Tp6m2De9RA7D8KKDs2qsqWnv6JRu7F0hKDmyq0g94ZwcVkod5rjVeRg2mmOF46DYKc5XisOXhUOWhUGvDHf68c/v9BGdbHSQob8RXweX/RDB9sEio1dbKPo7v090wommnwBaftY7FSnpfX/RAvtzU/d7otY7Vc6VBqNte/dw3A5z9NsfiIonqG0TTNZfayAxgphdCTl+DwEhAE9q04CaGGNk/PPn63Xz2G4WoaofXNVtQyMBt1YXKAweaIzAKIoMwNnLdOfv+SznimmWy9T415vhdSDr1CD0KkvR7p0gn/QtPgSozgSvoaAr+F9/shnOAnbKfXezLNwD6x8bjLQ0R0hhSAWBUwrAkUi5x0E+P1CK3s69FGW9G5mqCfMgk3nj0ZjoZn+nPNZ5VcMKa8ToPeDzjxMyjDuMGIc9bPtMlN4/o0h/jwQMMpzAyxQLUOTmD207c1iT4cKbMvIzxDv2cxLyXytj/L+hsjd131c6zLTKc2scvKn8LAAuyG3JbQMxSvf8xlAM9iEJZtFeZFm1/BpEUP62MrHeNZymU0nxqZMbVLMiGgH8rJGtKk1V2NEB9Q7lczL0QgkVqCHuFMFqZgvfOYsw/7enm8f+CHeTabBxrha24Z7a+EtI761jjXoRqyy902K8mFUg17iDkjCGELucFE9y3uLLC1SWLdeBqrhjKfp5js7OjHdcZvNOZLlmtPD3r98pTcqFH6ykNRKH2ZgcI2EulaS+hVzhlYmc7QlcsamXtpO8sYOaGNsRhreAAqtHkJHNJt0hfFpmpg9ZJKrMEMldUFxPkkoG2plWUE8y+8G+g1iRKewdUAPs1ALEI1ATWTzvaRdRpo0AP89IlqCotp2d3a7Beksi5/x6tSMA16q/Rf6VI7ZHjqAk54FOaaqQnD9PuNRbymyXIbw/x00QJ39f/Ue9v7Z2fAT/Sf7fcb0V7//S28SJePDN69Q+72JZCrO3JEEXa5wTKY4o/Z1kCmJrPYVo7Nt/PSTj8Y0FMC990lcw9L0PqZR4nVYx2eq5MMffwILVmNMx1dIp3OFP4NrT5AhOrzkdpRJYSnTBAHo3pLfDEu4a8CAJ9hjEliBAgOjUYvsbTj6FE7RfEodueN03mES9Q4llyGyEzEJXjGMUDSKYpG9r7VYWaTB5eXJsycH7y4Pn71/9+bNy9PLX16+efrk5eWLN2+OLy8h8jjB+nXOby+Kzgqvc2ADpSVKt/saLEoWizQr8ufRUGQ+aFXjkEOxBMDs3Vv6bFHw13mFMctvtK7luYrGgbvBrLwyh0P1pos+D/9lDpH2Pgt5B+LbhGOW8WgMtJGWMxQ6n3Rh7awii0bFKwxARgYaMW/voS9XzXDFypVCvla171CnZtRiVwGrtNpnqnHQXB6tn+2PVLpuCGNXmfGmEmDyxdGCZZ+19y2Lk1ipGcmm5d7MZwKPK3MW/wEhyhiQPmkKzCR7HvYkt7+xQtLd4vY1khd9pwMBLWB5ojGk8hjLXH1qqTRd4sW3LVWISxXfulQQk35zqeLblgrsATeWKt6+VDMe15cq/spSLXlTCainbZTs4kNufXAWdA8APxioVGJsKO2TYDULbXPz28xTbkn1i5aFYL8B1olggoNWitqaBlv9GHuAImDtDvJLLQeK0bIqZmHowdqz9+hNvx2DnuUKg+RZpy8E9jxEjrWcmysyWhGJW86VUneWgmkbnCr9cs73TVFpfd2+2+EGkfW0B83tRXuY9vDw4JnDfuNxdseKXuLTEedF/g0IjdJYkAGfFxkNc+L5zMyOhyvnGUGTrzhAhB0lfBSHeX5jS6AkKRFLcjBX0RelpXQ5BP+boeDuo2W2WumXXmKCNDptYlS/xFf2eKe6gs88nz+ujUDJn0yFs8T04VfVLMwV9Zp727oEre3jvcrp7aaqtXVTVaxcEVRaYlWIZJy3jpKbHdHfKfo7cd+FVF4uRObJYe3EPOGPrQxJlJcE7r9uV+YoqXMItI5FK+Iw78TEhtxS3ussozwaRnFUXEudKYsgcQnBTdZqpOobKgIFXwMIzQScY3B5ReG2Iifqb8CNLDeddYDgddTaoKdhVCiyqtpoO1G9xzxhTTUTLwLcIOBGmE4ijUWYdAZqsM+BYRZjL/IDiSn4xvMr7E991j0huaWcCXaE6ldXg3nKn3KhEsL7KFdt+f0anilxEow1gi3rV1bpWtBS1XF9LjsimMbpMIzfwZOK2jnomUXUQZpljOuqYl9S3KvlimUrbuVoCbyERT5/bLGe8MJxxAgS/thxzEiQVUWieBnGVhPqldWGLqUaMWX8iqVyP93sCJ6t+jsF5EY0Y1Gm4daq8KSqjdXZ0zuQ09D5XNWGrhrq1d5X9fE3tOt8r+rTcVu251mxLEH4pytzm4xWUMcZ7R5JkCcr7hwTdJN0DkWSdqKkZVbfNJaDgMew3OOVOwUpFnFYkwS2gmVh+zG1z2RTQ2Z/6naTx8B9gTpk/4GlFHg6d/qC1Fm9ebjykl0vWq/3/PuHoL5J0ivPZ7Zo6bf428f4uvj2OpkcH25Sip5T8g74fTCxCkdFkDIMzgq7psyDCYT/GEcjkEWNGfqsHIvrYA5RdWIRTCuOjOYcSYEU/4/IteVFmAP3eZh6cxb1KLlo7mu1kVZ5td+GqsqxuGZzqwjQIbBx0StFHisLHvWiHKSeS0FS3hIEjCG+AE/VxXpdUloC/W5hmlQupxJWU+s86Xah5VOYFyYJma7Xk2530uY8kq4bFmDWa3CFhfPTWsPUgS2Bs8T4Q2UepBYkJ2xeFihQAGCOJRDHRE9pYPWsQkbdBjMmaCdec9E252nijbfCuqEOs0srGKUAEZo5zQEaRqC0J+7ED1MbEb1ooNcTsOB5sl6niQ9b34AqsbbXr6dvXvfIJzeaXHsJ8yJW+vzxs9Qr/YEtIyj9Xp5mhef3MjEuR8LzUjbx+WMvPZ9c8PJ8csFSn91UflBanb0NnX0C6oJo0N5TOb+StjpgIsh3lnS7kXa5TRrccCPzzhlc5IPpQXbtlfwxdHleXrDovLzw/aC9j0fZbKUkHkbY5Ca9NGfYZ0QmVkpXYxy0joiPWPD4hz0dkT0iequV8tHcS/xudzT3IgoGmna7be9Zim+fpbBw9Upj7qWDJLDnkvi+FEuxOU8HUeBOlE35XH1f8HQAp/mTLAuvvakf3FT9WBStFbiygqJWFK1rvte//nnavwb1EHV6ytPBdTA/v75gBzw5P71gH3h0for6twPO+Qf/ZnF+esEPmJcOrn8eB7MVOe0l7BRcj1e7uzofUSXrkEXKB/1LLthBWy+YfvehbcnCsaMPpjka4Rf+ee4dsA+s3N33+1jmC/vCOT/A3hUbMeacT7vdFed8PEgCy3/lS054h7t6vXbhK8HX5ryGQ/Te1a/TgErIm5pAfg/ALTiYALt0wU0NNeKCQntcHzg7cZ0AGxLZF3fHoMdmGkL0ITEDNGn6VER5YuvmYQkjIxc0URZoFjwy6K8GjQ2X/nrdLutpYDtR/lYVfzPp+BqQU1FYH7zEN7DU7VunwMQGxsY2LNJTPIAkjmFkj3NCEFnYDuE2X1lt2SoqoGWzxKa6IoaUrbm0U9rX9Wu77BH4yiyMT2chjMW5yBu+I3YFDR8wdsGAThA/iCyP4ZU5VNSBcd7r9RIWXSiGqux2U4kAj8tB2stR6LzvB6lpZ9rUTsSgpa+0s8fuY1OYuyDlpxhhwrNM835zCKh2QlfJcziVBzI1bCyhPQBWLqq9DEyV9Vr/5JyfpVheFutlAmWiKCJBycB/vYryPEqmLVklaH137yYxxEz13X/5fqBbtGI7p3chw3q9XuQH7bZVcblSqABnJJqITPougo7FJEqE3gsJ6+TRNAH15o1IwDMUhFhgPwBqBpieNxkMeOT5DEw40IJj0guHaVaI8aD0/GDSwCnj9w4r2U0KUd7ae5Xvs4lf+SyhbCsR95BrhmEmyIBPVnKgN1F+KrIlMPc3Rq5SIUukvkT+TcKjqqp8e6EPUo8mnrBIHw32XvImbAy9JnzCIj6u/H4pCRHekZYTHVbK/Js4vspqPvUm/o2EYpgD1LySTXxGIpJWqbJ52i+gsDrWdQE+4Y9vUu9G0nFgqD2J4hiS7yzREXcCgPImfsVUG/Uq9BZqZCLM0wSrRFSFvLJWfGQxQ9crBRqw3I34Hiv5lD++mQJkU/NzwlcrfYfD22iQkNni1A8mBJPSm8L5w+a4bnK3TnnSx7anWvkki6f0Z6q58wW2sYA24J9a9iFAPZhKpFj0o91ddONacBiZct2K7t9n0Xo91/ZTrUXFsCrY9+RQHzbGArocy57pBQxZqd+CMaDT67SIJtfPJYyw65JPEdOeQosN31P5/VQ2lOHbCZ/KqAPHggOo2bBRkAWOcv8rsiyQppfq9/7XJVspZJfW8qztxSYTKlfeTe6lm9VtNhfTzf5/JxVTg3qDM7Uksdgcvf2qzGtTgFXiFoFrHiRYspm6xKSq2CGJiIfWTj+1CQkSFkSJty8e/ePhP/6RsEfiezvckUXBeMlgoFYMiRX5e3AIMjc1BjjLAdHfpTUExyvPkWYnCrE7B2jaH4sxFupIAGbA2xQ8GchfUsodxeA4nQzkL1vd/gsOF46HCK62ku+xVJ5WEw6nPxvTXWLYS3Ouz/kL2lbtMXpdYLUnCL93qffC7x94T3yWoAIIhjvoeU9AtDSlYy7iEK5+oR/2K7bChy+pLZKEWDO9RGBmCanh6oTxVXidd9ZrB5JILo/C5KQEtcE1tgXrYdd2y5ziDMaIyCki4kRnkH7h+xU7aCyAVAl+/0BHgnUjvsAz7Ql/fONhtRWO6wXMHEHxFny7oKVKpqemI13uGYZ1EGSS0aGi7IsE0wRaleQ6snAv+gruIFLZGyR1WosaxoP/BX8yGCS9Cdz+dCUf+jcveI3cOvQr80bBgsZ6qlJpH26MhUbxiSfk4D0YeAJlNZLwGOwFj3z2Un0+FHF4PRicrpgQirN/6VBjL72SHfrBS/Y7/4RKTc0WfrJFfOXPn5wPVtitT9gCMjrrdft3/+bAO9Tm3OXuLi7H8zACvISSbL7yhLBWZeX5Km3OB8+3lysaQFvBF8+9cBcS5BMlULK3DiN3nGDOFA9LxKBEK19+xwyBwVQXoucFfD0FD5zgmqEnDlW+9vzBF8+H8dHwviBViAfKp7kWVffHIi+y9Fqd4ijv/WWk+B+/Utf4L6NtRdjHlE7ZKb7zrUPXZaaMzouEWGF8glcUEEN2A35FnnnUhz7S6SvXQlnr5Xq9x5INxNp/sBc82tv7x7545EsRtzVs654wSuzEFaXLIuoWEvr+sg7Kj9YlcJMmz0HMGEgZmGavtNyOTXhEosg39GIAxsLhgN69SjMx6I2jjJwe2VhLL8HCeNBbQKSn9fr8gs0bvrwNs3COn3H/T/kNlg/OL5j5HJxfwNGq+PkVx3CBmkG45loucMq/8Mc3y5X3hRGrRswLPlyDEQEcgr/NjUySuTPz2QfZ+Bf2gj2Rh5QShrZqZ4tqvkdENu7OF8oc7wvN3JWutOpn0RdfnzWeRS5ngt+M4gjck6Ie/TCycEt4rWEUvGB6CYIngw6o9a/CbNwJwLYXf4GpdxhY0lhRhGqzt069DJKPCOCY2EseQqiu1oH3yWc383D1FtdEiMpCit/5k8F0FSw0X0YL97snJ85eMiF8exXVJ3pkL+B7hdZKk253rAB1o2RhGN5Sz4O94F8Gz1bBeM6eKCQZ263PK3bIX3gpe+L3p3ICH7wn7JB9IZ973XAyGKjeIE09vX7BF3jjzM/3LgaDVN87GsDjuZeyKa7x4vFet/vCTvRr9ThlL3y22N2VydYXP3/RrMi06hvwL8A4Ii9ENpAo+JwY3aYSPW/F7oQPDUvMCEcDhawVK/3AdLmyTUXHIAqRwI1s4JaVb50LKnCzQh715jFc2FNRvBarQgPOi87TCxA6wJ/Slze4Jelb3dLnlg7eyjyfupNBz4vAeh+62bO7QeO2OrP1aW6sBnaS/k7U3wn7O2V/J+83EqjqLMUwf/Sz5ElPeszKo4NpWp+evUTroKTqepjjKZ8DG0ytoBiTVpNeSB32TgSz/A1WFaP+qBGodeaJWXLzAWRG3JIfaT5kPvesK0yNB49intDfgVRuKyrbukGrqShagEk1DsM5R6AMdgxmjpusCHyW9ET9I1wE9KWygWdCA8p++E2v15PAR7lfxZJB7xKoJZtlojdyis6d7AxaXtsWJLpd81tlaJZXp+ae64DsRypLs76OFRaQCf0MrhpMXEx/KHTW+An4jqrVAVuPOnFx03ZRxljsmjFaykFUP44xp6HEI8lJK7MfURyGRWgpC1slf516tRkzZWShpucsVObdoM9QKXNMabt1Z2JBNJBgHz8pGLlCwDv6VfmsrDR0NOObeSoti/zUUUlUKqDtgZKEwmoZ6ojTryOUrsKiQUSkZF4oaj8v/KB+DfuVoSlxz/f0M7NiwXk3xHSixBKRGl2+pbuEi9cg1wO3VDnHWnu6lmR11TPw80rX7O42jQp5CgQmfwzKeI3PIpHhm2G9KZJwZWx1a21NRfFGNYeBxDz/8d6gLc1nVPeBs2FssbaK5xnlSDlC+5X12+nLYBehCAUusz6hlZz17fFehUrxAizc7zDsLdAB8wY9eLQdgA1P8KGAzdBDBzWlWgn/17vDa+mAbCFPRF7GoDKW7UpINpwsSmolPWyPKBQL7B01pqfXxE+A3qMZrCZgWnsvSKypgWp5W/NQtv107jUuEWzcxLavqs0ZjPZB7xT18llaxmNc+DfJ7yjCU+ZZg55MuuvdKFYQHwHe6uyDnSoZQw/604Ksu3V4opL0/qXuwvFYLa8+iEyXUTKKy7GAi0iJBc1HFHgrk8cN1lKdwAmKh9XJpmo/GY9BLI/bCZeHqS9w2AGpCOf23UamrhtDWWwADsLMIehAtFgnROStouWeob5Ic4mZOyPPl5RBqM9AEsfhGRi435C999TdZhMRXwMLXXy3AaZhO245IKUueWfUeBTVrk2MTjTuODdrbr5K6WAV6b2j0LNhVymg6evM1AIPOWQsW4SlSk3fPLB2/T4PB3JQaChk1Dv1NmrUSDToObvB33aXuZGVd0ymJ7W8tvxG43ioKTewEFAgtIk41m66RIxpVdNGX/HHq40a/cVm8wtNqFS2Nu8JKBdB4AjemCJjKQeNUrN6c/F19aYm/fdYKVko1BVNbPXWAoQK9lRBp3HtsPSnmqOXJL5h4Bzanng4fOVw6Kl36rNT1Gg6i2OYkk2+sfm1t2DENvjBwrsmTZ0zlxtbJBJETLbgEATbRl+bI5FxBkOZ5DuDiTWxhc8WODFNxqM53QR4cNEZfFy5tDvyijXyZChm4TJKM3/Qk/Isb05z1FozaxDeV8ho514mKZfAjTVvkIP5apPqbS+TzeNCNtUwNxL/Ze7d1ETcG1YHbJKoZp4nTOkfAsBqiFVEAbDTSetd2u0upLLE7BYiMw3zFDvmljRfIMtJaY/6XpQjB96CrbT005pcGKEqGv6WGdFswYIhGResKqyPaoHAa6hNweCo1IEtOa4XVCcOlkUpezDXq43PzMje65/wJbMUJVYB6y0j1Qn2j6cgqBXUJiB5jjroUEpN5p+LugOiZdFx78Zlx6sWnMitKG+VCR094/9y969izRbmhhylySSaovMFcliDnrdwcNkuIooiFmNdxCaqZZ2F1I4s8LKoYQy9k7q0+qmv2CssQnjVdOHU4bHoN1OqVSUdNt2Vlr7MhECLqmGSCNy7QaHO0S40EJSFwKZ8o9rJbO4SlKsyzlJiJ1GUiC9jEMGGKuVpZu+EpGdvDKfACZliJLQ6MiOvjI640ajan5I+kaX1rtheAR+QZqEqdBg55Xu93h9zryQIuZIhfdgFCQn5B1b2YMXxB0oUqJubgWKSRAg1OQersX5BucEM7kML7USKCLrdm80DqgHQe42wJQdsB49jrpoepFIsCGphmBChXqBUxHJl+jasCC0nrM6mBuUG57q7b5cCWYhler85gbK38W53v2lSk4YTW+nk1O5x6FAg6fp2cE+M2GjPaktxLX6po4uUElZV1bdkh5G13Xx2LHpDYxlVp+xsy4VemqBskwCFGuE6d2CxAXRKSJlSRwWoSVBXaWcccMz4br4BYRzwfgCJna1iH5gdpXcjgoQbOvuGsIScbNXGVayDJV+fuWNEcVqyIT8bWGhjNVxbs33dk9qWVkfzuSMvkwaCiq44xEPbNha0v3hgc2g9s5JHmqdgKS8HTe3pCWxvWBep92Amv9e3QRNtHB57NWCVg9QGV7BnA2xjy+7V9+ce+2Y0obPxtvUoB+agNOYlDUQXKicOGizBHCUE4IujhlDy8IjV7IR2cis1wk5JFi/4AHZHrzC+U24JIBUfFfkVqkPkEvd3Yv0r0b8sHcnOqL+T9Xfm/Z1Jf2fc30n7Owvlitp3urqR0k2cPLcfbKfUBoMu1zcUIutp0QRmtdLyEXm7zz1HfaGuMym6mClhJh0iSj4nAXVNzoR3MA9zBbnVNmGUNvlOG0flMmsSHlYDtYYdsdpfa9pqwhZ03+ZMvDPU8Dk1Fm29mnRKaxcstY0jpFfjKqXMnqLE2FisnPakMg3vBUt+4ExICrktAYftq7lRynK0ur3cZkbC14XXVFbBwbca32ACOs9WC5TOtGStVpG2hqIVtmSVVpq1wpZyvm8Vs7CQYQJzU6gjvVd3PlrgtyDtkm1R71ICEAJztL/kdWGE4lFFXbG4RR4nuzlTt665jwHntGQOXlS+dgVq2DX9tNudmu1Zsi0Dm3mOzk7v0dRs/Dbn5Xr99aXhr8E0o/5+vf4tdmu6WgFZ97fYixq+6HFOPV+bFNKbJU3y7xol+P/RU6o7XYHl9pQWfx7lRTSS8NlQim2u77DE3PT0lSW+2osy/gq1Az4j2hGAxC2sNFZRCS+1xGLzvNlRnReKFCyrTW2Iqx1LqiILR5/UNGxnOTI/XIHvwQ1K4UqWakEc1gLxnZeCcq8HfzDgm+RVO2rMpiAmAuvURZFitRBZBK7bYXy5kEflUXKC0XgU3MsGUbD5tuHQ0bm10dZEhBDFtjWJwymIBCD/mVz/ju/77ERMYulmhHMGAtdMQytLFzJEgwVjXPAmBaRUiaC6nN9Ubgn7o19pMRUhWBNqbTmjyzviXqRRTLI+nmWFuImTJUOXTxKX1+Ymh81cXU/iSrsHAxDqmC687TRA4vvVzszA+KPn98mOWGE2jcNBfrNlkkEPb4E3JKpYr72IR0bd7LOogoPjRl+tfRWy4OtnEsYus20ElZYhUfrF9bqNzuOu8WrJlX5vJ9lQ7u3uy9tlstXe0W6/AXBgRlTtLDXSeU1XrN4B5IvvsAa3FVRnaU3IWyvlDwbt/WpnZZbNkCopT1i7DrevHcpgmbtP0FQnsAI2Jgo1B+JYgk0PGOHm3Tbao+RpOPo0zdIyGa/XrlW4fQtWagaAkppMA/Rh6jLQ945fATLJEpOvW4VOVBsTbRUKMJMNjJsaqC3JWLUw1i04m9axMNHkgntpqIsTbEWVnmCujDrYgkOIg3IgRQ6BklrL+JQr8F0nq084AlYVOwUFyAFaRvcu09oNmavRxKKJOmFPBG/HottdEGfO/hAcHqfwWLKIpX7feyLW6z/QRpj6vJYCtJWSeGk1FBJiG0NAtQIECy1SmUXbu3bUDsQP+lUsCilJ+FDnVL8o7vJFxa/7B/wa+0ZAPOHkpB/1MBbnLI3HIjt0FYEHlrDihX2bkbtELPqTQS/K37oNgDt+rU3O+XxQfznwYsEnBI0nYGUbxNoOvqEF6xDY+CrRbD6whbjyHZgt1oqz2LGFfsGNVdIBGDhNBtRCDLcPYMoeZnSKepSspNs9sKq3n/hki3qAntVYEyMSYFmuyLKRfyB/ZaQ2Ba2B/MRVaXagf3oHvjuYAxiLbIEfOLy7FNXHKmrkTs5jUcmf3a73QfacMz0G9oVb8qIXXEoE1RVzyK/rKi4tz2KfuIMM7CU9K4m84J+63UP2OzdQYu+5cjJ84cg0nF5YlMu0mcEnFuVSjxFg43qBohwvy+AlSlJQGPSSUpUGQrAotx5QGnRQk/1c1261rTvH0VRdb0pdVQEp63FL0MtNWdL1hviXaRMokG4Zcyjz/gmk53klq9dEW48X9Tfr9WYfjxdbuwVQHTI4Zyb6sdttfzKQlODudtu/wwKhKDO43mIQwTZOg+DUtK6bgpaQSAheklMyk9deYEt7mPIlUfabUf6M7t7A4YISMl2r6lKC26l164h/71pAwPH+3qLhCbG73XYs4KS/J/jjmydicE9b9L8n8MIB1u3eM/aB1K5fsT+Frfa/Jy+VnZK/V9oyFLv1/xDePeFX7EqX6KukPsq+RKqV1OYLLMtdznlpnrrdP4R3JZyUK8YnOMD7SU/8SvTQSdjvdv+EeDVWHW01EnhtqiNkuMIroVwY7Fo6bt37yqX93IBbOwmLGljKRoGcXtg6z3gre9lsZLujaIbCZ19yZGEVBay4VuPmrfzN2kktOsQNyT/eJAcYeg1YrTxIqw2KhW6z1Lm9Us8PUvRf4DJo0Hrdhmg1ilED4WU9GsVYCdrAcV4WdFXCCultlgIcJIDtU6c7s+NnKB6GrBHnltP13FWFJedTiOIBf6DBWZiTr7YE2DPvRosDgxLdwYATqi3319i8jQVP9NVZuCyKxoG+9pg1Ps42XrhEW7cLAX+aRJEs2RQOg2zX0TbdbGUD691o+XC1AxEeb1wNV2Jkp7Lotkhzan20nmu7FM6Vs9Vt5JCcVDK5ugLsnRt0rOlgBXfRBr09xOtJNmzf1GGJDaFZxZsELy+WEKdkOQov3CBkcijg+9lol/aGQpJERkSNDVsNvtFBS6KJ1zyVmqQO32uzXu2tooKduKFNvMQPSm2A5DjhprJ5eZ81pAyaykBJLDW+0RAbBxnM1BooQanb9dqlzL2dCwMT44Ms97Ts0w4k9TK905q6xsIbgLHh6iAJCIqbLJdh3qCcOvmK58yt3jKi5tyion8djUEhrx+sHa9e4qbQ/hHKW8bxV1mv388bVFl1nxvHzsPVUzhaiOSO/iJ38oVpsizeKZpMineKO9kSx7fbEiuwfYM58U5hBFuNNsI24L4yCm26u30cm/4tqnfXAnmb+e1gY0xKLOD4uaAdqLTvrombbdNvee2KlRiVygODel6GWQSbK1fGuqpM4kbQkMOPGo3Vypqhp7AsOh3HPDskn/vVfKg0LQOWghOyVWss/DwZbHmPZ0qwNQxR53XaMoVbExBQdXxfmwGO2bxhyo1mgGPJhM23mAFGt5oBRtLOryZFQ//7Pdvab7PApsGfU2aLzZ/CIRlBgbSZNW3WNpRkE97WdmjSm90jC0IMTBnJLN4bM1f1Na4FieHGJpXZbdq67VVJ/mm2ReLGZ4+OMFaq8c9tC0YFCFV60EOk6M+VsommKcOIK8pnc9CyQDDfOvxKUbn2aBNtPyl35ZapGIPHsfJFcwampsgap/bV2m5Fp2NtQDhmxoaidih8+4DqrW4bVQ3cjmddMK58NpaCobF/Axi2ZRbKTvJ24Mmm5v5N7UiYg7rJadxM5StNf1OrzYBXIUO+FfR/aToN3f6VeVnmrFGjOSutnWN6CndWmYCLtDxwqp3oL5uf3mZ5+m1Gp3oPa2PAZltTU25fNaYEGHYxdVAkGpSIyxLak5rB1aatlWP3JnsEOpd+bhjZ2ceRhTWU9rtui1m3ZrXHTSZ5JKf9plHWjfEaACWtTTe6k0CRjUnb7UnNVLRmJdq0uHaXNfvQbzXcBLqwZrH5SpIIko22uFlDGLoU4B0NN4GQVxaRCm3UrtwEDxrifXVhmiBBdrMGUWS7No7skU3e5dds8lCV7nA7dKBp+YWoWVMVOje24nb2Ks1IkfhEsruK+IDyJytPkZIJc5gjArBhooLdXaUnU846Oj6BWjhj7ie9csrKlTuBfAnMKSqyL9Dx1OlRy2wOgIOxcgxFyHZnpE2qqf7QliHy++WgVJxOYBkzeRE7Ty58UnC43EVY420AbSStb8UGQKMEmYHCHKF/ZYQY5Rr+U6Em9s1alDpZc+L3QT4A6ZpLlR0rZfsUYKQFsSc4hnBTjcvBgYt/wxyzDc5J+rsDNZp803zS2nwG5Es3URGbmkhYtfrtdL0GGUhiZ6yFZMnymtoYiRt31BrNoHEQKX+cUmS7VHtN0vb0XbZswyu/wU8fWWSvLotTyKAOrUS9uwvAjVRONq7t7eQz+fo/iWOvFtt4kqVzHXCpwslawJIB2dt7FK6iX3Mux/YIQiV/nM4x/hK1Ap+SDXsdq45i2WUAeL+S80waAXNrBgzYVnk5F7Qi6rDYSKvR0HlUX001Q2cIagkxynFvHi6oqll3y07Gdy4GRDZtFmZMZEbpQgx60RjP6ifbUsIYa+a6pArP2bqsKtqQJZXbjad3Is+vGiydEa2kxod+W3bOzIR64Oank53Htsl2Dyo5+f5ttrMNB33dELTUBi0ogn5l3yl3tgV1bnm8clxzUFYObBGGPA0bhBvdbpp4ZUMqgW0VpEG3DKyh+h9sPeHUQRxvtZz9BsNv6KjR/nmDNop8zfVrT0+4A75mFFkLGbKtQ01DSApmsy+/IuzznLDfNVPOr7V+C57covDRNOGmlb76ooRq4HcQeXUzQ7WcgwEQhlozBPRyZNkVRE0yGWNqYH/esDiINrQcYIIwjoXzhehFAmRgbWmGixQYTKwqXN7ayatJFEUOuEi1SYgYtsVc5op5mvy9MkUwgRkgc2nDh2QLSmJjy1NkDKOSQfILI4qoccRjYritBjSXLRtA2vzrrej0IdYgpY6kNkQllogkB/TXRyj30t0bkhTdtuC8SuFJ3A579TexFK/CBTAOjSyDFQduwq2kOIPBYeqlYH4Ltl5jfZ97E31jj9drjxTTr22eQ/v4p0y3Fkw2WQzHItjwF8yNkBYkWu15SB9yL9XkF3AZYx/EJTX2A7TUlm2EUZ5gTij7E1Pai0ZWglSrllZki/W801k/woi9JnpTZBP4hvuwqrDbCE9rELdQsobYqtOzeuSVpEo3TKPlHDZI1nMdAUBaiOSef/Hv0axZM83qwFRVqqUZcPN+PN6DIIhEWFK7IJ2GRBL/Dl1rAhjdFcSJk5bOCUn0LU2oSnLvX5ksa3cIOoggNBrUz9pKYL3GA2BlRAquwpS+X64shaq7/9brm0rHGXTlEVHtOeR71VwG2ZFvdnf1N9sdr+RfUpNr0iO1GQIC8rYa2esWJkOzXlZiPxNu8TD/e5rWMbVh38m84WZm9+9bMzPmRaUVy710aa3c+pRrQ2hjiNewKdUWIb+FLR77Og9OlNNEGpoqak1t+BvrRvRhi6H/avuy6eS+0ed9UhMNyVPFulR820K3EkleZuLbenM8RwxFCTaEIHIxrdt2GjLUmfFfIdeXxA88iHmo/HGPJmgQAVKaW20jSm16r4wKqb3IZ3XRQ0oxqOB7JHI1yy2rnPjI6VoQjYzQzYqxXNoTPI9YekHpMC1Iunf83dZOG9bTsqVblo3N+XjlRWyMwqV5u5ZAqdWwOKmvI5bMGcmwZYxHDImY1wDk5B1yjtCvQywFSJ2nmi83MEmx2QuzINc6nuT/CI5LekHOrIFs6NdIf3tWNlkYqfu71JIXDbAm1wnRKFHxyqb2Uwi8r1jjSksbNFJDF4ogojyFMooq5GeTQTatoVg3eclNyDeoE0FKmLssJkofZbSx0vc1uG2RUOo3xOe0wq3Vh9QIkIb+XfWFHb4NkufocIHvJHuRpJDKYlMA2QhFPHcRlG5Dg0FCDMtgoGAswVtrYQO+yNy6zmxRgzPbHeAuqagJf9ye9OwIoJBEsWcia+oVuiGmYCI93OQ9V1q+ejXXtjEf265tk1vj6jWAdFx9FRMq52i/y1XSlyaKlk2jsloEG7x+bV+5942Z6tcui4HyXIw2ZdP2leVX7n2SNHheqi9N+C/t2ykCWr0JGePYCpHGNlutjJV8cyumSr1E04DogndK1q7AbaPaJA22CpxlS04alIEJPdBYqUFFULOtbYhFXZNn1aVwU1EcOpRzvUiMEZXdInY2IHOFKz4XD3dtDAN3Mmh4mHUb1ZnlqDIzMc2YzWD4ucTwc2B8Vpmw7PbdgDlJUy0i8Lvdeq6ytMYv+CpaswJX02wieza29GnbhDYba5pT9C1zsiXEd51W8zGCiU+tWAom/6lmkE0gPYRlJHKmXm0ulwE3XrbMtIx2FnpSmuQA12HLD+QwtVMVR+BJ3BAxpH7obXx3Ihq1jbUzNGef7m5Dzpd2O9I2zD5rOy2in6CKLolV7QxHKn/X8yjLC+zSjiWNxaVJM5zULKq2qDOso2ZgLdEgCew1UbiAq5I0KSIacdDBoaa10uKar+kKK3Y858+kL8wByW/lreqzzzkmb1PI9KxX5rrM8ZzS50abEUVepy060VAw18pFwVplLuyXKlc7BBvJIbkjpOnSCFaxLytuCfZGsygeZyKBTckfeziOZ5MJiDaRoEp6kkdm0tVBM82Vjyp7Nup9zFfe8bynOmY3lJzQbdxnf25CA5b5kLJjORD4c+73/zQtGk3gmUlPqNPO3iDAT1AWT4PEpFuZ82KvQp819Soh+44Xq40hQQ8+e9kwqBcrnz1dccnL2LzbwA62aBI413aU5XYAeTudr+y8dAI2lhcQGcsu0QfHDMt14FZHuPU6ReeYnpwzJvpKHN8N2GJ+xT6vEA/r634jnb5l7YqsM9hvgDsZqqwC6QqJJZ6CqXCYAXNrjzkopZgzZWroYBkKQdKlBqbbbVuD7Hbb2mEySqbdLhwKE/BrqAVLf5F6JTtX1k7phe+zDyuVDdHAydDVwIDYEdEngc7ENBkM9sUjBnmWWEpeBUTm9a3fzW5fkC8ScoCXXko//aAEwxmNADLrk5XYy0s2UkGpN4xSPVXsF0IyCSfps0neHQY07Yh9MuUGes6US14qx9hybvDVSnCkI3vYKUxLd8mthKZ/rOrSh0PIXTnhL1eYSu8zCNnmfLyNJB/3GggpdWkOepcOLg/FJJVk4qDnzRXFPuXjDV8p4NLntsB9Aeun5IRj9Irpz5sc8dOB44gfLAYdU6ijLMI+rLw57HlvziZs6rPPEH5H555q32FA59cXdLaTXEJl1YvQlh780K97TdF85uAoDunTF3ARUAPXyejZqhBZEsanRZoJen0gg0d5H/SB9IUfDK4tcaliFDHvqffB94O8UPfBdd397UvFzq/ZwQWd99dNLkHbPvhs4xS5ttXtczhG5uz6wmefAKSnvrzflnNvzq7ZBG+931b6hDltOmEm7gkzd49ROm6m5riZ641Rqe5OneCerW9CzhActzVuslOfzb8WQKjtxB7pdn9ZeacMj+fVQM88mOocJmAKpbitnjQYVtCsRV6B3hu8VgenwXXPjqp0avl9xZHjSIZb+2BlFXi/cgVQuLnP00Y8frLySnAr62+sfOqGf6/YecqSCxMx6044PeeP062IDPvnPJWImm5D1LQRUcfc7cibsymNWppLwLPF9GJH6N+r7vgXqZc2+ueyc1nmQuHbZCOY7EQZE4zljyeg0QgmsveqevCPf/xH6x+tTISj4n6WloXIWst/9vZ/6u39Rws/HaSL6yyazoqWN/JbJ2IerVqn6aS4CjPROkpGPVnu3SzKW3laZiPRGqVjAeGmwFgxycUYQ1FnrWImWq+O3qnX5PfTioDFFdDEy6ODZ69Pn/Xm49YkioX80sr+H3tvt9y4ka2J3vdTQGk1G3AlKUrlcrshQxxaqmpVW2VXl8quLbPYJYgAKFjEj/FTIopkxL7aN+fmnH0mZm4mYu7OK8y5OidiHqWfYB7hxFr5CxCkpHK59544E90uEYnMRP6uXLl+vpUkhcFCzSVZZXDEH/GtIvN90Yb/IGp+8fz174zP94Dz+iZx9v5mDuyR2/0whn/63T896nXHn9vL0du3e+PFwcraC+lfImfvbyJhT51I39ZdXMNHwNwgJIi59/bt3pSSPcI8F+eRQ9IkReaKaHGuI10CouKmJ3hnJJ1OvMPD95HULa5jNwIMXSPudEjuu9nkWjxdu7n8zT7CH278Cn+qnfXTnCv1ZEpognGBFLwxc5de5OY3vgeh0NiH7Yiyb9pTCp+z05XjLZdJb5ZM8KoiSNmrxCRkazkhpOt0hLSuzLPlkhlTN1/d+NVySfiZTqg3UDWrb/dEmviW9oqlsG9ryfC8soWyTA5GyQajPieBZlMa2AVGLuc5noHGouT+NFo9J74MqhFzICf4C/lkTNX4iJTrgI2hHvG4kLXsgESHlQPylcx8FmObP/Ru3SwGy2FmUbJWJZKRhY7++5e5WY+8nLmxl0SAgpacY2fNx19aSPnY0wHd10fqqo4eXObgLcC85G9A4AIzR0NvboeUrSXA8HbzG20C1+etPl31WdLa/or7YzOEWW3GVN3adhJTF7d8kBD2KYL6hxYb4pcwqkpfhn0Lgb/FhZkslzCO2EM70FpYhKbWT4fsEfHF0BHfLB1CtKiCnQ64vJIBMuiPALHt2s2GhdnHCFEDMghtMiCPQouWnU4JWT/jWct61s/IoLTJZ+RRaVFt57+MVTzqBVpd8UeIHihMyslnxDosjxwmc7lmMfzUGoAaawkgm2eWPIley4BYh4mohXW8VixZryexLBpjfjFuTiy3Wai68Wwu/N2RjAFKFQvXbgeOl0xK4IHEHeDH0L+l758cJ1HqFjZIdFZOQiMn6F2HwHZXdOqQl9+/JMC0w1KaO5VpHc5F1FRz7vRpJKi6il0R8YUOy3u+ooRY2kVFQR+akXAPX0BOhFSyeqE3V70BvDPeBg6fBWY9J86QNQHx7u1hd344d4Y07XRSc8E9V6ZU0DL7VJI16vmzwrVPVtpGPYawo+wrP5yfss/cOFVkDq3B0H6VmFpxyHmIg/BoHzOeOVeReUPnFuAvnXIxxWnmB+YN7ubl8obRnAg9KtgQnVFCMJwqY17eIAF7o4d6OPn+xdP5xEfWpdN508PpdhwCMvnjWRL7PPY6o2RvDgNFuLkkFar3Op17D8m+PiQXckhePX15Njx+et9Ruf+YNFaNGBL6kDb39TZ/MIeSVuzOzYAOEaLNOHUAl9Rg1SmSPl1B0CBD1KhexGZA4RbEzJWgUgiosS7lGxp8jxhJPKsMdwLTlYMkz2DaREMYPCnJXgB2c0/f+3EhbEvNOV5QUmfIZHcB1zm3Z2IBE6g2oqrPIesyf/nDqzP7A/VjYCfPRB+HjJ6dODBWh80T4WSN+J/UTpsTdtqsKCxk+5jy+bMv6DTR2hH18FHFdThVc7TLaRPItRaMKJLrokjtvT2YiNl1khfkEKgcUEu5pJMsnIYA2kFgAACnvvHK1lKuMz+w6Al4+oAkltE+9dpkJZaYzXDfu+EMpNkgiGUjZ/zw6oywhSyBhPQDLwQGR2khAydQXK2xu0fJ7w/6gF9bdjp/iXqFn4PlJojInORRYFFYQT+8OjMDmmiL9xkHKYGDUI7kG0HJa+AtMlXjSrecy5HjzUxPTu1yCUdtyUxIeExksfDL2YyH9X4OKJ2K5icViHiCJDMZlGP/MBUnQPX1VARnfvSoslInq8zpqBrTOYAx8ppT1XqsmR+yu5HmnvRyDmY82jm2i0MCeCIl/JMAWxDIheM5cP2jqROwsEccYzLzZy7svZducW3PlQVShAOwXBJizym4d577cR5CTjvq1Z6BGe1LofhzOLDtKcXrnR0hV1D19K8wX/n8TVhcmzB5jBndnAeECqkAVjrxzW056eXwKk9mZQEXOfgXOmGQ3UW90IoYsZ8X8q4ociUrIoCL0Z6iZwxjwxU1Yhf1eqMyLwz8vnEbFtd4f5wk0RWEQWI5ksBwZzMjLHIjdeGyzornvUuL1pvkNLvFfO9E1HAkzOfOXwtzlDRKjkGqVoI59cQtzAoOCjEZnY76LU1ROx3zxDcjxlrt4ORd4qzxtrFuwRhcu+9rvc57xsuZ76IPHZBc7Jv+3gCHscbIn68I9HY3MlVbaEiP6TlNLYvumGylie2xwxtmodINXCqR3trnNJ8kmW/nlXlORR62ynKMWXHM7Ksu6Ae+ukendDh2XkbmRW246EV9+dIPjuMcy5jflibQuKARsBt+Zp/SSRKl4KDOg3cPV2jgJAGSpdpWyFyAWvCOOQRw09jTQMHkAEtreZif4WwKcpHCsnkT8fI8D93p0xQRWDXCFinSEKO7ZoE7iqGlsh4hri+3vxsDsRphwJtk7IQ0cMoeGOuzrTggIEEqNYnDAIgzqy5Zr84IBiOPEjK2Rx7WbETOm8hMej8nYcx2Np06o7EYoymbTmR2YapS5yjFwRl49sij6VgraNGg09EKWHQqysRN+tHp8GrIHrFTje6/RMoZ93KI+2YKbQEuI0DgZb8G/G+Xv7HLCnTMcl3hZ8HDs1ejcBBQ7u5MFhPW+JWz9zd79Pa2O360u0eLynlMZ5VzQOPK2adu5ez3aVg53QN6HoG6B6Pcfq6JdfKK3YzF3UpNNZqS8uNEWvkgTN55BGdo8sgJK4uG7OesgnZrJlTnkRlYVi/zvXLim2ZAPcs5Ch6ZfsXOYc8aFJXtsfGNK9utrNopXFZ1bHu1SkIZ0jvmtKxPu/tWDwztKhNlIs4RWLyGo2BsDeJRLLfguBuOQvVk99X3sqrODi00ApCsnBgciVbUw9txBGtP7Kmp0z+cfp2ok3fKRjN1ktF0TOfO1HGcRH6UVg52eg+4l5B3wBPEmLEE5w6jTGmdvNTPyrRBbPzYg/vdsZP2OG2BmIZ14jJ4HpnntKIyC23msOwfcshi0QsnZesQVevHnc680yk7nZ1kpDozZjkYzex0zGPnh9z8yLaDCroCsn1cY4LqNikBPe6lrKE0kkQc6WZAJfcMZ5mHOVnC2JLvvnFz3/Yqs5kF0kErypiLi5XVeLeDU9bpmJ6zoai4/kdqTf0g7H3W5DsgC+GTHDfGZmefDQdEVWRENcGzhsmDaNwYuhjorOTcniuAPY3P01D3GCsdsvk3Ge+5E6yxnZ4TjPpjVFFKom32rLd7QGXI7j4BAhzw1buv3V9wKhK57eeUpX0H01LRMP+ew5nZ5yt6zE+zihEl1rgPznR0PAbu8BC+Lja42CHdD2KrbGoYh1G6wHrEuJx3OjsXgzloFxkXauNv8wI+pFX1+4NnXBZO5yu6WKmFY3v1NRTBI2hi7FgTpb1kjPLOPhCSvrUAoSjr3XK5E2tH4ucEMR5Vwt7nxKKXr2oMTrwixm04m0EMnAIvRJ7h5kYYGGFh3PqZj5m0Y/VzGASoaUWMK38CFn/IOr69/PztpQGCN3cCqhHkw5glkxEks1lya7jG28u9t5dCWcE71zNeJwbczbPQkxoLEN+CrtxIGcs2QU0a13HI9hfJltb1LoUgbjSmgUP+RmoKib3P337OWQSV+re3e5/v4dyotNHbt73PH/1td7Famtbo7XgMs/f27W5Hz/R2zzbZEWmZbwfWYG9K2YWFzmnF12CiExNcrVN9taZMuQHrgS1TUERzQeb8USTWpLbczhnFGJA9c/Q30Md8bhGbmANbPVsDwomGzPTIAi8UreWi4W8H5tu95a4F/TP3dvetwe6BkmU0VtagpTvkc4hVGzxy5HrEH3ufkwExe59bu6x1b9/umb1H1vLt2z1Is+xyEDxyCDzuEhvv/UzXw8khvDQHtjlw3mL7LAJWToDA4k+fzlMzoOGA7zgSAlMxVpslQaNaEMjJbij2Q+AdeD6ITX549RwEo0nsxwCPoY8QjMjvD54Ry1JcHhfmKfbhAkyn6OXraxQp1DbYBCKG4aXkyjfY1zy5ecICbm2uEbmzIMki38PiuT9FyS1TFoa5kWbJlXs1qwyvROmFa1y5npH62QQuZij5CeOpYe4uwpUFdxadZngzqTgJGWegbCdRmdIrkrPk1s+O3RxsJjQmNay/stboeOmEOo0h1kCxP7b4iVY8fDEr6/Wk00n4GkbBrmC2SsakqPZPkHNi0hJgnCTFLKWGS6oRAlAjOC36DhT4g+Wr5DUHZukUKUjxyyZfPvCcnyEKqxLJ71tIGGz2Asw9bc8JNc2eJ9pyXcFlHhsTVaAcUx35OdJ54VPwx9WvPm2LVNzNwBsA7aV6PTJQQDjgRZUmqWnZEGKE9AgmwM4M0JZWZhyUavHatdF9nzQhcy+P3RiWK7/uGa7xB1jJf9DoexjjmgXvolll5Kk/CYPQ94y3l0XSg0X49tIIQn/mGaPdxV/Ov/+ux8YRHEYTazXuGeI6nvtAQgrcCUlZwOJmBwpWVMqKesZwhnYJwJTMKqNKSiNyK9gZYJGIhYJyNmNbz4U9xT4Jbf36LIxvjCKB4euRI8ONPXWaZOwITN2M7ccgyaD23qUao6Bu489vIfxeVsLFcrnkNy9kokAAoJ6k+EJjmfxU3X+xdmVwjLIAFlgr0W4jXZhDtfRr3KFW8S9yOqX4LDhs2Q+Bw3aEGTgszBRIUXcCTXaoPdRv/vR9An+p0sNToYKngWXds6LPeEWf1StC7b1WDasYK2E/N1QhGqAqED71MRdjqFZgAvCeeO9W6XSqS0qnTslgQtlxrOYBiWYCAiqdbPR6IA+EvMdOpG9ruMcdHjP8Kch1aB338uswAIOY866zf6g17Fg7YqbO+ZHTH4Sj8zHuWHbnm1QmWr7NnajTicQxGdWJMK0c01suI/wiiBnK+nuBLpWqiam9B2XfcgkW+6bK8ggpME1RLlCkeNuvMUpgEkJXgr39awE5itSMdanKad4oxxlrEOBU8OoUfc9beDJ6je93Ys5YDMiAELsuUhkQaxCjYjimUT37Zy3ZP2PZP4PsacU9wRsRXuVOkrB8pcSkZk+v/XkBCOOEe9WEMbOfchJa6mo+BojBHOLQerZUxgXcVh3NkpyS+7axTDrs0rRGh4Q9jDRuFQApyri18Qqbqt0RtWjBvM16ZE4Wna9uMfO+0uV2QKpK56jUiJ10c/uG1SMJ21+Br2InOyygWSpOaaa0YQYctaSe0GBve8c1jk9nvsiphitOa7InDaat3JGjsFzufJMwkVEp+JuFEJyDZk3Aj+TCMA5u0EVilyt+wyhRVQG1z1ILuE1Od7jyp6mUYjqryBEKo9IaiKzfgvWeB3aHRTJJZpZlizcl3Ie9GResIrEKQVzOFFwQFIn/7HSmbF0MSmf6KOJE81HU4+zRTp+bv3COVR2NcNqSIzCoKdwQ4p3GBvfNRJ709jqcXPOjUnCjGIvEuL32Y2MyCyc3vmd0xaWNWUYyZlXVgufhpZBl1MY50Yc4YCO84tIZMMm9jcGW0s+KCu4buclf4WAhGCuTlgou/W2fWIew0tzUGZGX35+/JpS8/AH/Hb4+PiWUnDw9e/r6KRkfirAfbsqKzCtnRP789DXawbipyjCvWIYKMrhXSVnYhJKrWXIFfyfXWQI3IfGrW8ZFVoKyBtPAayHGAgieCX/990WSzHL4DZZ84m9e5YUfwdPP7nsXjC3TwiZjtbKvmjebqtJA7PmaiS21kviky4gJTLjrrjlb4Ml36Ls9L8zTmYtDzSwRUHKQEez+X/MNBf+abyrIgtdj6TBtcztRnTvX4mXW/D3ClAml8/UKFmH+OnNRZpWA7ACkfdZhntabAyYwKhtrzc/Vel8Y4IZ1+HNVL8+CRWU5K/m02jAKTxvFhgCIwcqcFhvKnBb1Mt+578Opq5oZhxsKxmG9oDAAYMW+Xf/eIimLmV8wNFYmps3t0ZiGuZwtNnrfNpqEr/gCSDYtgKSlDLNfwYJlCmYmw+PX7159/8Prp6/ePX316vtXhH5XQfrJ81dPj18TegxP8J69fvfq6fnL7787f6rR9teV9AnUjvTL3UWZruzdxXfVyl5cWpIchw7eQpDJN8VN8+Arq4ndKa1NQ3nghJvP1XD7uRpKot/6MvNnieud8IPMaQmKHQpGSHtpSSswYcaoIohsH5Pj+4zJF/3fZEwsLYRwWpkiE9Vz05A7aK917BVGHBbyfjtkAXNPfNMFaD1S5sw+xsIb4ZUPDnges9Th0kYO1oZKbeNrRpHgqOMSnx6zAVlcubnPJQsx239JBsqZutdZYdEFO03p+vU/Wjl52Ggtnt1S7iBFW1OpqClRo1BSDzQEiW5EpQQMU/kFLunQ7aFgHFpJZhxaXLKojJ41cEN9CKXl0K8ZRtr8utwCSLmz1CEXSWnk1ygUg0DrBh9p37SYWOEV2Nhr3gsWReEZchlVUmbqeyAZC8B11cjQtcP3ehqFYIGZm1PXYw5nyyW+OHOrpCz4h3ST5YuEuTUudKoYN1bCt4UWZviiMi37HUSkkpXAU22Vfqe6+utWqhHXW+K7FlWrN9RWb9m2egXVT9Y7pIm0gpUDa4V6TkOC40OIb2BhsfArP8DzW4j1UuZvEvUmzMkDWE6xLqR7R0rnsIeZbPwCfLR4bpqlFt2Rj8J4RlGkVNEca1GC/ZmM0bZihkG/JGZKNdLmWTSgc6kuZBAYxTWxDmNhuhHKTVmpy3hVExigPhW2aUgrTfFnUXMuqPRAWiDYTApnmRWdc/PYOTitwHXSowE43qzaD1DliRDyFSgma9vq01XQA67AHAx0ErpGlKTBsKi/3LoYEr4YgpbFUCrUEKzhhR8luAhQGKXNRGDRBGH6+ATQUUwDSBpru2ZYV8xP2CVOEwenwvdR31q4U/JffwTcQffFYAUtgwU6zUDNAwqYPD4Z9mIFh8BAGeeBTIemtSTU+0Hy3PEgCCDTji+YKfS805lLw7ZDLzWndGe+XA6bOr9aAkjrLoHgCgJpeH4+8WPPjQs+CPmRYSYZkmIU4Gpj+fbSMtwC9BjTFTFMZnD2tVIhwp1xuCJHlnEFklvU6ilLMSaJvXbBKM0oMjecgTSWfE64WiPy3Rh1jSDKFYeA4fl+6mdUr+02if9QGDjyhhtXUZL5QoKboWsqm1/dmAuvqTEYbPCe9373u5drykRef0uP4NraTOZEgHxO7EvItff55YocwWWWkR3cHudowGTxOdtsqgnWgCmrEaZMrICBzrilFr18A6de8t7PshA1PNBscaIaZQ5Jby/5RMoXR28vjSSrzSUbFyqzWG8vab0y0QSmu73yp2GsTAPTJGNnaoCP4iaP+YtrtzBu3ZzNEOi2KrSuq1sNGqAa40RdlYUjC05xsrtIQaVcau9gzOXAgKLazY1p+N6PxY5+eyka//YSZBIpKL7OnSHDqD13qkMmjT1vGMReOBiSPGU0X8xVWhM6MtWwJsW9gLyPjrdm4iz0UFoQKGEuszwoMTYBunJxQsKzDprJIFkbOkd145QhZUZDdtmL3DgM/LwYDYWFjDeGhYQPK8uy0chYke+LlXV4UYBU9wPjB+nld4m0muQTJ9cC2D6qoYcHxnviTzQMJ8alRS8K84Nw4/ow+rBmteMzodyOggloyyX1rnfkm7kfKhU++PKFaLTvBpzguMXDuuAlfq6sRd3Y4A2GzSNbVadWBSMtjKhAka+/x9u0sXfEdouLulEDATdgJ3DnG4N5UHN9lBsbxI/SoiJG6k59Yaxw6vxQmR86nQ/t079Y0SEVJjBrryI6lJZLNUOllELgS903YNBMMNVG00xVmOL7Sc3SYYCJj5/piZ+xjI9BQy6/PNxoHDWsnXecqqYf0VQo/umby6ytAB4l0KDWwk7ndPC0Lmw243ANYmUhHVgWtYN+3cOO+dDhVgQ3OuXVie5zfJX3er3zleDmwyR+jSCB4CC1WtFTy9bcLV5UEojlrwwNGbUFg0uwkWGX7JWhfsOFe3Vpx+sairgX+XnuTn27wekhKmhrgbxwJzesL4lDsumVax70+1T8Z/R7Tyww6F+krocQ8aTfe5L5EaFwG5hm4N98nMyQ6aKelu0gnRtfpPPWfBFSHjFBwvXTZw472DLj2o29GTuR8FZpMLZPbkrMJYEMCIBUK0gcMctPe88yF+0xmGdr8z1JCXtB/sd//df/1zj1YdO/92cgqzb+x3/9P/43vBVvKgPM2cSNpR7bNW6RgQXrKOOHf4LTNeZWUfIC7KapgS5KuYHdzaGDrAIgMJgnuY0Nsv5d2EqELvKimvm2t6KkNgTEosQAzH1yr7L4cf4ai8IhbCS8lYxqE2u993eO6PWBGJ4fYgBzgNDoxjCFoD6MumOrd9oG9vqxbOIiSOLiHH+SsHBn4QR2DbiMDtbnI1NdC1YQlJOJR5nA+bZaWxUvKmyhRYO0AR//VJ1oi9YovDKQK0YkURRD+TVRCZfL0vXHejCy1YrJMwyEzMjC976HkvZnWRIx3zWpMFzwgtuKIFpF3ZFfNgriBMkHtHfQWrUjojKA0bT+QoZrGCzqUdQe2O2VXS8vOYEBT7DDZsXhvSpu9mS1knfCk9A7RjsvBaoHK1wFXVxPwqx2gxjVaM/ELQEkAu1V0DIStixmNLwSDVYYcwHUaMV+SsKuhUFkCBYikoWKDtoGnACq3HDq5wVHQpBqX5baFoILRMn8tXVYojVxybjY0InV2Dd3xbfF2omojRFSBH5jXq1t3b8m2wDLtGrk/ACfq6fzvDXoYD4yay39UFG5IWDKQtCzpyI7Ys+GKVMGsohvb3z35oWbKgHNh8pcyOaJbRlyoYqSJK9LEbaI17V50oXt6/PENM+vQZIuZglj23L763miBU8LBOyJMIhJTYX9ADFOwQ5a08Qul16vSKDkFfgCrnvDPudq4cxnaCOS62YON7O009mRLYDIbV5PqXYBJqOu+bDaNOROxIxvBNbUGpYog0IFKRMv/u4dorOwbaYUjz0pX4ZegQiMSegSwREC5i3r4jzhke+mFp2upESrcVRAMBdCF+DK+vSXMnwPISSCzAerI6HivewfltnM2V1Eq0uIMyDEdBokRmUu9O0AUQRdxBZWS75ciXluynp1E0ou0Fa/jgUtEM5APf2cVoZqtZMfHW7qFQCQo5/6efGKy45E3ueeE8pb5/oR3EIDYjhRNbeFSnp7ih4KxL5DhhGCRys6VyY1k1M0pGY8jxU7ibguH8qoNJqvF7iS9ADuNnRn4QffgxGq37qP+uuVaF9bsb0UU89JBvyr0AJvh7ePyX0CxHhGrynz3Dk6l0PT6XiD3kg9jxWKOzqDHjl9enksrYOhFrSqZIBy/E4L1oicyQMnIkx7fpLbxu5Cj9fhCWEDJdbqEsS0gXRtYLh2YWwK4SStHgHUIPN729mnU4cZtZWdTmItIge2Bkz5My6ur3kDV1+Lag6rR4+EsXowqsZQhSm6e1p5YNYpqlguxYvr+gu+aizUh1UWVcPFCCkQCrad7WNGY3P7AoArPjgiK8vS6ewcw8VesxMxtco6HXPnYrm80OfDUfMRBjIzyBmWyw+gXBCY3J2OGYG7BZ2CRZ4+ttNH+5YdOCPwZhkfoj3MarXitnrlQLAF4Ffd6aQDs6LnoPRIzYoqxk+jxtLBhS/KQW/Ur0n0pVfI+8qUuSw2Ms/jILHPV5YAiVFIBMxd5hXAU5nQBuYaA628oB9gCZwyodCQ3akA5fECZNBqsAaePnLifnoqJ6FOY24rGsGMft3vdI5hMw5MLzUJZu0GfOoJ3dlHt/3LxmK5lIIYfivyQB7LZS6cSeIb22DLCc8e6Emfd8GywSvuuNMxeeL25ccAl4TN6IkTCp9kNdPHsGfoDaKQ4sidieG9GJw5p/aHwZkztM+bQq3B2drVYS0Pu0nIsrzzgzOnkWKfOdUauT2puFbCPqe1E0XYeVTSyOOkZuORcMWwOnDONLdgWALnbSeF2sn1Kcdpttb4rCBtX+g1jjyp33Akg2efcrbqQjXyxrTa+1mzZ6l3FfH5OT7hfGVBHSs26OpQ+nOirkogM1kJQThTJYFwD9XTYKLADcd7hnHu+wZwArm9t4fsB38zSaI9P96L3DDeY0n5XhpObsJ42nU5gJxuYX6q2VVuOu1PIGggNrPmSn22qehf87uKfrOpqKZb3FD0JlFFv+GyoVDQo1HYOGm7+2OtvlISEspGGgQgqKtTijshm0btArdJNFyjjMNfSt8goUcupSMzBBxVBvCa9dhNYkrd4HOPaLP9VyUtq49ZApKzs0qV4+A7tHT02njqobLFlddRcREF2l1q3kgX/JOM74N7h3PKviPMAgCBW3xFSyu36tnLbXp2DMwEvtFc2X5RmGVN0y6fILaSAnjjCvYBC+UUKx46sOz1NLoA8AQ+ynYIUkuIJAEYxajaRWO5CHC2lE+SVOLuhJ3OztNoFI87HRP/AsFm1qkQwOxpLwJ18p8rTTf+Z8E958g3M1WIHdKgLMoM/YJQuAoWpewAh8h4fP97q7pqmZ9kC62a7aU1TTUin4Ofwj4lQ64wPAJ9Fi5mVD8WiVzUbq4pKpNA6V85eac1jSXoavF2NaskdsVt5qZMuCa/FUttdn7U01f4TWWqWygDXVPo3+y0l3S5bIqYE44EprThAWWXAwQOo2XuKzvK3I6YNdYO08ZzkSbwtEJPohTuYZyjmDNOQH0r03uGZhbEhgGVMhFT7yJ9QKBLIQAVhjBTJ271FkiduimCNhjT1m6t9UmspsUKgNWnNKAejcaWcKYpa87WJbjTcPQ3Jfmf61h3lXRSO3ek/P/YkRqAC6ehA/iwckp62ugH11Z6M3MO6Bsi5rWOkNamgxjKVvAmiO+zT4sPrq2D1Qq6PqfIM9ILmtAPY/nVi8I8FdpEPpGGGGWHWQ0cCVAagbvE1PhCiUx2F/PV7qJaIdiK7hUpdXMNkBpRPzVy5ikmVxYzE5CauQrO6ymohE/1wWnyJ6frt9V0XTZV0/SobURZgVMgdgqbuy4XUuJIRXiGlfkOzs+aXcu7hN+KhcOCwh952jsWUDgyoCHlQBR4S37aC/MfgYESDU6EKwOX+YwwjFEwZjdoHrlVyeHhsgMGSz03TWeVWdJ3gKZbF6tRT5r5rABxS9QyzOnlaFdgEIhkafDAEmz2B7HtVmO5KCQdU3Y4xhCQeaSJn/h6jWAKzkyVT4CUoOGg6NLRJXMhE91ACInlcmetW2QIOi0JIiRI1zqCEKc4kbMIPVtW64G4jN29u8RqYC3Ij9XRKDg7L94K+i8vBPKNuiJgC+1aZ1CtKpMQvCEKPW/mA+SwTFdJlF2U5Rv2SDn2nkhlj7T9niRztb+mjYuczN5Ip/rVQQ2DlkjrqjmRpZYKlKw9W/MF4nyB4LGllh1uwNDWCk7c2LH0SoYwlN9pvqBM4ai1Ax4pSBTUqLsfqtVhcxWChEE+OBt2H+W+vRGLkwNs1YvcIVO/IPQ2d4irNGR78+7t7W0XfMq7ZTZjOnRPs469yNfhlk9fvzjTHKk6nZqyV3urYZPrHmlYJyiACncK5v9153HQBF2VRZHEmvfxtw8rD/3RSv/0sNJhnJaFVvwvWvGdHYis4hfutwBlG/fcWcF/TYpsxn+i2yaEodHA/JowQqyLPFzpTrhcor3ju9yfBeBYuYMf1XjIxIwdonAAufPOOZ7WDDLHXHffXS6HWeZWvTDHv2YMYB/6ZDWqGMR2I7qvAFDhkVM4aO2oHCtHZC4CqX8psQaJiE83KmkwtuwRQshANJLRWD8Id2vQT9BR3ZxChYsSB1mJ8aQDC0TUPIBvoDzgPczhphBMFdGeVmoXvM6Z3ErBi8FdKwzM1znHmUanBxjbZ0kWYbCzdgdCky0xi/Yt+joHjFzmjAC/+0KO/zrHrz6vHOEXNrrH3qMkAgug1M2KPXyH7pWUwL1zL525YUzGOnJu0ubrufO8YlG3rYHJPfg4zIQ8Uplu5u2lH0+AhwMHejQG/Bq6foQgLF8HzJGpx5PQnBINm4RlBANXuc1XcM1m4ikdPVmfWcCmBNYYWAvcziwdGHOYxaLIwquy8E3CThYAQ3amA29mTkHvxuLOr+WN/OI68cCe9UVOAwcGo5HDjyewL4hlLZe3OWX+lnJ2Y0tGqv+RbQ+kFRjuRcStz8urKCwAqEQmhZE79YnWA5go6NhULKOGLuwPHCuB1QUMCdv/jCNBgmOw2Hv8a8xMDJAOXONrqP3oD4ytSNfGAN6KMVsup5tGEzg6J4UBTbcMKFSmBnX64OFGdBJtyKvEnD5oRqZgXbMDG5NJQdgFicXqrDgjfb5iXrqVNhfcpX4OhkzzVe/SButnQQcudxfHq/klJYDCWk+tWCpbBnMwp+Zv5/TcWmEy+h7AXrprWoU4GoVRfKOxyaNywumGGV+yfsA0l86LHLBQENvfuc3BhEnQFK/TQXwPjRqg1sGjnlBSUAGTnFA2X3ZZP+Mo3/J2QAM+7LZHrxKvsqNf7Uqr7jfJp0Dzv06bmGFtIPXCk5XWnL+h8gHJMYCLvbcHVtAzFuRlj9hNZTJzi7bsWAG/BBtgD7QXzuXuQj2t3vUAYfXS3phh/T2za9QLvIMAHTxj2Ol4MzPQvblbSuwuECRmtacXXcugEizeDhqsWEh2hTfSV5PWCzGYSCg9CUeQND4UfoGlw0R8YQRm3xhwzitnvjq8WX6npCXHQVKBpOuWL5fMDG/GAmbx+xWrzXiLhonsYQXW6MwoAPKhTWwPru716kqLtun6pcJ7y0tQ5aUuBD2kzQXCvmsytGZuZsBELSsdvKfo6+cxFyns7NvcXIGLGKA29G9htdYwFoAOnGeTc38DBgO+Dz/4uXotIzeweltKsY+LF81pn/WFmJUxeGxi9dC+DMSBF5MhhwWM6ChQOj9UfbOKIl6R3zc9zUg26s3C+CYf8L+mZY/GKwHgqoUWzvtm0gtmbmHuS7iIoq8HSA7EOKJFXn7t+wUiuDRGV4tlvZ5/wAL7ZP7MJiIIFKFubrNMZGW3ZaiJcr6LFPw2lX0H5zXA/RnNx4NUjg/gso7mErASABbpVM8rd+mO46SaS4JWisHs1hx6Oh1eFtj4EfmcjHlx+SzDZkH/3Tz3i5wMQglSxBoQ4d/lcop/LYaLigzoWlZu1pCIJZDWl8BOtVzuIBN6hvIDS2AKIHZO/TP8VR/dLXg1zTuzsCDY9N5ccA3FD9lMwmEokvdI4AI9CtCsX2xvTvYpL8yxj0tNm75cLlY09ue1iuNmecjAC4tRF9Fzz5tiAYTUVMZd5y1e2+crMSYr3B0Kq4Tt1AVHcWgKUsqVHmLd7fN9m/DpCtSOTRrTFdQglEEUGHCqO1b2ASz66BCb8gJfIhKpp9Tg6zkwwDhLZithW0E9h4VBW4KmDKm9eGsm/DQ7nvJmAZ4MMcVWlqAyuqu0RsdBKiqucLGlzUbY18PD0NJp3J4ZeyTtYhKQTpZWCEHZ4lGi7tCa/2W/hkXDPnqoXfrP0VqOxvJmzm/FbNIapvhhH8CPFWwyXAk9CClaQtAJ8HgVAI0g3vco0GPwGLdogrd0TYCTtCtCNb0zMnyo1UG5K5ewo026YIyFVsf4WhneiZNXCM6lnovUIvGU/Q2a2PzXNgCtmre3AiEykk0xgQ+/baBZPMvcyIfoyTyjJk/7c97ejW+Tj+sGJwCe0G60jl1WW1TN70KEv6AWJRCi+45Ai9VMXCTxs2RS5vaUJvE3szKzUwr7HLzwARrKnovHM999Dxe2JH6dlBOIcZMVcG8L6bFSU3MYlGYIQn5jINzAudOJTDCqwrT3oX8LG5df+E6dE+docSKlPzfgUW7e9ML8ObQn9yfgSwUyIDD48W8NlZzE31+xu4F5SiH8l59fJzPP7j1Zyak4VmrvYS9h2U2ZyEMrD2HyeZxuiB7NYu7Stm4lotW5X0Dk2KTk77CHK7rf74tvYzLGdRUZT1ndENAQLbKYiVHASn6QT/sWjeDflbrIMLN7tDklgGh/TBcrwLSHH2JGd+HqfWGJed11zZR+sOqzu+uac5ZHm+Nd16xYTm2md13znF5Y8JWdffY9TeDn1mShJUQt7nRiBrHJD82XmQ/RbnxvuQzNUmewkz6gXUzB6huUVSuxrAEOiC7iJJ6gFzpsNCpsKIKVAwRMhI+UWtJnkck515zGNOgJraFFR63pSqfpDSBGOnxMYANgHHQESQypaEWbD0nQp6L9wgrJ4z1Z0zp6m/MyCY7m7i6OIt1CAbvAzkJ03P8zCwna2O6jcd0vX1+07FiRIbvVTYHZVUfO0cJbLgOma8ASHkhAmU0HRMOmiYaPqaZOdAetQJQFMzpHL7hGPRCzKFXyHp/HqKnt5iQjUfxeIvi9BPm9OnczdVCu4NGg9/6rd8Lp/Hzm5tdDUIHh4eD/Uvp5kVOS5RO0FNjZp3MdcR9jZ4YWZ+OqDYxpLfB06uz07Tk7cytldSqD4oDzvopiMuV9YIwlGp2Td2xZEjpXlrxgd6B6LkqNV0DE79PDmGErHG4wZW/6YbHgFlOIst4QigP/QCgyE9O2GxX/CZdRe8pnXme6vAcuEGXU49XXerS2dKZ86eiWwqn8ynzlAINBq8bC4je7OfVowvDYGBoFTxqDI/m2Evye1Shz/CuX76lgDOmQIwqGtROQBVzzBKt/o7H6O2fL5c6ZupYtlxCYCMNp+L5z5Psyr+M4N7pdtHoAqVDa6UR6xYO1db9c4meONb5+MITlf4q8p1Y3kI7TXh5+8NdCuRgnbKdO77dT2QRJa5dOh1ULe+lk216Sd9sb5+gUuWS9ebjcb5wjLa2291SQtEcn2t67X5tTmlCPVjSkMZgN0YvG2oj75jn1LDo6B9gn+sGZ9M3ze29VFjLodPtWPb1rq56KrUovHlAfuz0KGUyzJua8bvICePEYgu3h9orZwVqykxfqGtJJluT59yzk27CnPQ0Gpf5YpzTXfYi6oyR1ITAhchMBS6LZbyn6XQIqRym0bMpI0gmBx8R7QtT/zRBLMWYk6IvbJJg/+lkOjpSEhZsmEh9N9jnt66Z+mplf06at5NIN4GC43EDy7NZhIDsthkGl/DQ3RWTRRAURBSmGgFmWeSGk/LTGhwi9hSeMS6RNlKfgwaTJnjRenYPGFzUMg6k5ByYKbcFUh3AfwStQPZcNZqcG7QUZvR6P8pjiJSgdr7su3VR0w0DKFkfKhL1hKxeJ3inrwjWjwnLFrGEvZs5TWJS3bubB4IupNBdJfAxIqnZI8fIBGo5S3pio2MpgoBknsU+oRHRCQabmVQcnJ3d1i5h535Tb/KW0cLOpX4D2LbErqBRY8vNJlsxmr/zcL+xz+r4GlWkfbxR6XcBu/QBoBHXHx6E2DifNcbhpwVY6E9ur0gXdApu3wgCjMTjODK3DyvHBvgOX3hvnVQXuNGokVhbNfMZR/OgIvzEeNekngAeb0tGYZj4Lwcz/1oGZdvrW4VCCgv2kVC4/NTU8Q0SuGNKfaqBgPzonOozfT+htNZr5dOjTf/LHTtY3E7hc/eQ7VZ+1/q65ap0mbfofMGNrs6Gp92598ydrAQYa5k8W/anl3vaTz/tj7PrOjun7Ne9Oz6JxsebzQsBlEhYKrJZ/8tmhYe76gx9F2PHl0vfrXqhvqNgMu/7gFmDtoNB13zylQ99SA4PcQVdsF2LvnHU6pSZnGJAiK30iImdLMgHCxp2zwV1HLRijNbIk4vr2Bu9rBYDrzBowrHC6of/wtL9prxM3C90uJ5sEgmFDpaRhQwgRwyhCDGCcFYx1AQGcAHSacjAG2MhRcwVMFQFLYdjnEP+KS9vz0Iy0LaPQ8SAwFds6F2vyOKqho32gcqeftmzmoXMhDtPvgK/pdKYs2OaJ86EJ8dJMMM81YBYF4kNvHBUTi545F53ORU/R4PqTpNKD1lRVP5KHcrk0b5ybhjb9zDkbnNXTmHXFiXPSiItCzzqd007HPAPD7DN6ai2XZ9Yh8132fedEUJKThsL5RAVLET85TbtxHOcEwh4Ag66Bgp0gx86Dqfg+1xoDvTuT3MuZKnvWLHsmyorv8RqAZC3CfIjRjO03NMxf+jGCsGQ+bYIrD1d05jtvBqHwAxz6wiw+qbFVQ99JzB8te+g7o4S+GRAWLZmwccz8AUnZV3jKEPaq9iWWPG5iynMu3eAWuf/ki6PDq33dg097m1jqixlSpDltbMOZr+22oY9Ep+Ib7Z/89p22oho6pSYVgAZA6MvDaX8N3llRiPcNCmEu5Nkfq7Of22x961cK4BN8XRrHfiKP/YCfIZ4wF4nA/kSG2k7iczRQsVN1irSeMlWzu+cbj5ZjoDMXKxnIdNE4adpJxc99DPP+tG9OdZq0AnfLaN2ME+xdB/ivTVIIIq1Yh2kb6zBF1uENu45jvPE3Fn2zdrAJg/k3PT4GJywDD3Se+Q6EZoemYbzuHjPvKfyM/uhk/mCrvVUE+wWY0KGZ+cvlG8Erv8YzjC5qc8tn68f6JDfn9NPN2co6PO10AJd4Z3/QzmTPgMjZ8O9q03ZihpMwf4H9QfThRiy3E7XckkFq+3jHu2g9uuMtR/fKOnzf2ElgYKbpYeb9f0s3zii9pxsnNrPmUFmBAHbBmZpQTnjJ+MGEz3vQNtnaYmhhAjbNe9pkA+cSE9aoHABApueMEzhuQlpHq/pdS17ZLtgme1aZFxAja3HRvpc+OKVymCwhxvo57OwiNI/BXQdWHEf8ZoPw4bcbhJV1ON+w6E9hzZ+C5geENBU95oYgINHxKA/wp+tSU5z9i0IYADSMsOGqX/OOE9ETAadTz3hpXCfJDfPtuwKYL1QTurpTVJmCskz4UemFjeHL5z3jOYdV9ZnvvMiPWrLYL4yn83SWZH5m7O9T4xZgLyZJFPmxh8XgQDFcI01mVQBWwXk5wZhaYotMw+K6vMLdUcZTN90rs1mXCci6zEZCOMuESsAwZD7EdW9WmghY4bp0bBfQDViNykeVeXCFSk03EnnGoAK60DQICml6SlPUuCEvNgczdO2sUEf11GyzvA8sy55ah7qPLfUgztCjOYS0Rjm8lDuMAhqN8Xp/1Xf69LyPC/ny3bvdBQ/z8+jRVd9avXt3qejVz/01r+AoRQ9gRi5JDdi75fgsHfB4xhB7eIrQwFG+ue27lHkFezTiXsHweckWcMqdSmPSuTImrZgx6fnKeVmZHmXwQpH8GspqVAwsdagtl+d90zpkRmOJCRtJSEuWyyldbNqdUW/DmzYCEPXWE9ebjikvOD/UY51dLlNMfsp7DNG08NdyOafBrMyvz6t4Ykc9+XvF7YpZj2AM/ifpjRQzCKdZTlIjjlKuO3KX7X1vEteoV08AnfyI0clSp45P+5sQyLeBZdFkHZkAvA8R9BvO/iE/efVTXppRvNKAIhhFGgVjBfnDwT26+0A+4DIA59xySXqkHr8BwCaBTNXQiTxOexwRZEn4I7fQEZEXhJxT4dxC0IuPWfCnTFVTOUdo+w4uCNOe58984CV5PpqKexDADCnPmHPnaCqM23lees7lspUz1aKMHco2V4PLAfjdgjm9wKcyZbA0jBkXNGJvy7Lix0D80B2wB3uUDFgRiDQ5EG1XYS8U3rnjNYVpMhBGTZhWhKanW2VuWkhNG5g8xbXCxfqUwDlbDwGE8unLVhbxEvlAzuN1vSS6/ENusPjJmTAmuuwZxknIzkx3Mgk9Py4wIiYzQlvLbsD2qtd8OWjGHknkCdDSVgJH5XqAEbT0A7yi2j1dmWYyEb03U4qNs6YkhCbWcrnldcSKgz3kprKt78QR9INuqE4jS/hd1tM9Ea4EjtHjvnMWmUKOchw5JEWv7huYEDe/vkrczOu6ZXHdLZIbPyaHDInzdV+BcYLh+IJB3h6CkOlQh+Tk/gcMljPksJwxj1zF6x+mIQP70GP+OSXl6IKe7ySaEcvrCFhA1uCwBk0w+mwwRsDwwwZOWQ3gTOAvthGRUDP4A2w9wjptgT4Mn9+H+TuepgUFarQIlKCiJLg+4bNWUoWhFeSHv6AqRcuuX2G+B16GfSm+I/xd6EC7aqpbRBdGpbg2ap+xIWM2AkwlXir0Nv6xoHW4Su4a9X2EcSdEZtBUaRSRfdHxZKUDD0miGS6XiSV9Cq7DvEiySrSM6bUYVSE01irURv5VnzkhrvmutMx47LyO1ocKDARo2PZKHCelEw8GoR5biMkA1SAN9LKz8yLJwLsi94vnhR+ZxxEAQMLEAeZtW86pzGk1vQt+4p5AYtEu3MnEh0h1ujPkzzk4/NLEgeE4TGQLE80eBsxWy+I6ycIPDDT28hvfhdsJerMIAhBwrwPkK4ECclY1pNc+WCGgeo3ZPPeSG0uEYYRBBgWrJ4r3oEmmVKMqL6XXfdPjQH+DweXp69cvDXDyUeDZQQ08m4pH6g2QEoiwhkbjQ8ot1dV8SHD0MMqgDgLRX8uBoQt1j+H1LDyQobViA/XNzGESdT2qOZ3EzgI0oOd+nuN9n10DABuFtfeNa5I9Nw33cp4DENLFbzr1RUk7do7euOZlLe/e7uKbGbhdXlqQ9U2Y+Taujm15927DzB+4U7hZYVooygtwL26UtrUSAfHaqOeRiUogCApNBqTDd7CDzzZwWPCdIZR5nfn+9k7tYd35pUUZCdRGYtjfMhLAwrszLfe7jbn3WN5LS4/3miosq1loLn4p/awCIeGIyDkaU0x9FtswlZO4p8+xael4RTe6z397fYTGaxWqmQfIEj8GKBfP3tmJ9bpf6LT/l9yU58iPc3MRlQXu62cAPQRV1kYRTl6QDk4mfp7bwiMdaFzxV2gJuo/Wepw4R8mg5i7EXz5HNxyLmdFHyXsfKgj9vL2r5XgFOXmUU7fYljsnYzAE0Xr8l4RH2wO2eec7RMzqhfkzQCb0QdTBBoD8/Z//IzkU5+8JYDfHya1pddHTNfxanP/kZ2BC4+SWSCYAgTuDWQKcyt6+/xiJW/n1l6IIRPZb5YY7TS55mUQvU+59yZAtk1qRZBXpRQK9SCKKBF8ffKGKBKtrvYinFwn2Dr7AIt7Xj7WveCtPLxLpRby9x33lNPb1/gH49karKMESEF1Hyxzt7R9YqwpfaXGI048YfKDxJ3hww8GZOEf80E5A3+ihgbN5gF7Dh7IfCEXwrJzNLnw3M61Vd3dRmpj4IomLa9N6tK8nYvWWBYcFTzlNyiyHJFuVDGMWYElPPPcnSexhotbP2/79+tnt2vj/X9fZT9LkV5G5yAXFo0l8gnsdDFrgB6gPweoTm7mQ2xZuPRDxjAYOBL2P1YYGP/Ie+FychBBqgv9aj7rTPdDi7djEjBMD8uapO/EtiPau1akxmDztHY/lw8E+9wHr1Yl7RVjMfJqyxRu5c7NP4x6eBcdJGRddPWzt3KysRbUm8KZVLy+SFByu3SkPsEhDM5bMwqT3cz7PTeKF7wldKNXjiEDAi9QQ9zzjKsk8P+teyR/4x1AK0+4EYmPkhAYDcjXt5mUWuBO/e0Bsco3aRC1xn4yVFlUZCoyCAbbHJHnqxrX2EGGcAeIVv+hW3b4x84Oi2zduu6ODdD42rqbd0Xs3M7usJd2JW3QnSfzez3IWdWpMVlyHznsNStgisS/181CbqNWlpouFOMuTGyOddx8badU9MNKsu9/XYO1Gm4aSBDN/boSFH+XdCSj7MgOobRhU3Su/uPX92Ji6affg/nVFYdy97fbrdbbX0TaUYTwLY7/LOnTdHf0RRu+W/82vszC+6fYNjHfie11kWbgGetGMhEL0AQ+m3ccWAcC6jZ+WtUN0jG6UQHA2f150R/s4hfg7mHb1cY1W1vjjquz3nuiVPjYK96qcuZlWORyjPRYs3BsWlvaptsGPiu6+sT6bMPKP7/z8/eamyEpwJfQJxd1vK+ozIFoFntbMlkpYP1XP9U+D72YYA4v8yp8kmYe0hBL/PfAYND3qD+5Z8dpmy8srpE7WWP9eSgGLAipney/uXfvurLgGkVxyQzbv+M0fy/33Xbwo1b4kKuYfgt5MRe3tUykGe33u9vWZ2xeTodk0TVV3Zm5egFN7Wgy2r5zNn2uuVDX7qnJtSEmKKcZ//28GqTVgrPWd95yDXlG0PLblozBum4OlJ+OpS9pGbjOA7e4eGEWSdg/Y4r/uPjFuu0/aSRp/5McDPyzwlOAh+bTt2Dw7DH5QsEKbppzn2rosDNmrSZnlSdaNk6LrQsATRDbl75LUnYRF1f2iLwb8kvELBj8DjOZ5wKx1Zu6VPyN3ZpYTxqbiQ58CqAOQGMWvfOirS5bcd++nhC5uQ6+4tsn+PqHXPkwC+w3qjm+SuU36Rt/YPzD2Dwhv1XXoeX5MbGa1sE5sMOopXXg2eXFgPDZO4fTKiyy58W3CBa9I1EXqG96A3oFIOQtjf+KmNsl/Kd3MJ2qVaXV/YTw2fjwwTr8yfnx8/w+AptdmVsWttT42vjBO/2ScfdV7Yuz3jdPHvSfGTx9ZPUwAY1RP+s6I426SzIelSyhOrnxcCVxOksw8CI4i3vNH9T5K8uJdhiQ1l7kg0RCJjbyCXDZyq+TVmP6gNdCFc5jndONKb9qNataNlh7GEzSVD69m/jtUscl8+iuDvdpQDqQk7cXwjdapMAcwnHdwvNRLxYC8ihE9ocBYca2nfeDWERoWmHUmQT3GGKmAhJxkgHdnl/AqyQr+IqGMzj/DGzjCI59iAn8P9qiFO8NzDYB0MZvPzjmg24s7WF+ygdXV+VfFB7ZstLUKBRNNmiSBgwIK4oyKQiqiDyUx71AKdi1m2mPmOT18b60oXiLAudjPbMJG0Qg9Y89AUmbsqRsI0Sn7LXJ0DQK93sU+dPEAurh/F7Nm6C1R5D1AD+DaB7qwM+MpfwXQ+cCI8m15B+d1YEyz0MN/gN7nkLCJbWYE+g4mHErv9558KsathdcisII1epb7Mwi2JHZMWZ/iZH2KG+3v7t9v2vZh2vq9J4qDeeBkqC6c9NGjKnWOeB+SFE1WJFwxa6tm7N3DwV9R/sZqMNX/XqaG0ZDNkxPUJ8f79zg5P3yiydHYxfadt/kG68dek6q13hzWZ/Kuq9loSomxZxAwa2pyTWc1OKxfL6DRglJwUIv8NgQ9D+iX3NwX3IAtP1kkcCL5nmmCEtc5StQtsltqN8pDLM6Zhc3FS27PCsUT9ZsXr7EW29rQcrPrlm2pesWK4dhWtS536oLKTJdD6UgKv0hZ/ALMXO2YhugVDLK3kEdUKVcOaBdo4oDkngbMek4TyHlcILfmzUfA6xksH2uJfHosOqroef0VcE3tvtE7sXIKZuzgEAy+szCC+G663bfSSnOp/4lzBNGgsXLwq5A32gpA2YaD0YkmQjxhgjy8wJ/ot0WWoN3vNaFYvQE9DsyUm0PLRmSOUUwjWrV49i5EcHExJuvqXoZZguEEk8zknDCgDhwLJcqNc9Y3T8AJZYjK/Rvr8EzGqxoM0c/5ho5OxpZ9xmAHToREEVCNhj0/LlCXYY2ZQ+7ohN6MlSXkGbq7iCXm+/QNKNm0LeT7+h7iNZ/Qs/HK0gqO6MmYjho1nyDKliw9GIhIVObN2hure4YGY8fokPmhMZQXHFhy8E3fPKYpv/BDZnoxtg4b+uhTcyiG78QZijkf1iZ8qJYFKC24xnuSxEGYRWbzPkl2FycrMvjd7zAGOFMpQRTw3AjCmZ8ze56XF69Pn3/37dNX746/P3n67vT7F097lxYiDnOLxx4qwPwhKly1FoDZmeNoCZ1OwOLFM1zFG4s73fbcmZ8V5s168OUbGa2Zy/dvLBVwUBwGLpjDtZz414wRBWHmNRPjPj7o10SQeKxOEsGsdrccrfvrnMBpn4rLRdS8XEzl5SKtXy7m9ctF1bxcnOuXi3jAzQgGg37jnnHM32w7WXnHOfMAQo1gltx2KzAqSvSAKRulWK1yx/2GKEnxPBzi8u///H+BJLz82GrvIYsrxcJYWXIoMBLbJ+pJnIhdkrOgHtChC27eMbjgRGdIT4A0NG4VdwvZ8yKc3FQo8uobH7r7feMhV8KW+xIu6zJN/QyOXRB8MS+OUb+3f+BH7Z0cDTES9hZp7DbuiaBwUPjZcSbvRIBOsGl4Fckz175RyrIz52hxap5ZK6U0S3rSJQ+MZ96DwOlqBmChzo0m86Lag2WNV3RoWfaHAX512PbVofrqCX715J5fHepf1Ymapct3XiSAM8lt6QBbKPI1b52/arK3Fohfz81uJKIvLrIXvhe6JjHRET7LxQ6YQL22MQMhnYWwoCx6IsEUYrOatFhc8F0g0MJiodXk6UUiLH4dXhG3j8Xa+G+kFPKIF0ZFXC2LL7UYc33dFk8CQogf3JXqMOwB25b7BRsvJ5YWAbIIGiic430J4KohcMGIGS5iETYuZPwHFry57PHItI7WlwH5LHCDq2BCbPJZ/6rv7R/U4kdJJhKjeOkcHZ7MfXB7ReT/5iuY1Bb0NxHJ9+HTSVMGggaQ4+0zq9zSpgBCgw56Z4j24GcmmeDBQWjKkaym3DhkY7YVgiRKAwk1z4PSjtd79mdAY1zRUSDLeA0vlCnKj6YWGqJNVXW1ZccaJVae3Wqv9yKhU2m5hq1c93hBsC5Ta7QYq9rks73Fly9ys570ZFlMrpNwAhIwHm7ZswM6qSYz8K1CZyvttvFPfT3e0Uqt718QqIlXVaqqEl5VsHJgkR3eJQzUWRXBj7SJnJgVYGvZ/b5iabaqge95ynzR8n2uysZAa3dJVlqkJt+iboI+4P5+cN8jraYjVzbU4mTbdrDpDVV0nNyhEI5mILH4JEcw8fyrcgp6NtwPjO8md2iJNwx6K1PTMhU/9tuXbRIf84Wrvi2Ve0Kdx26ALUZl6/KqdolbqwhLSIL7bSK5uqr0LqVeXbqlafLYSmFaOPKKxTQ3/v7P/9HI/G7mg0cguALymxH6z+L1xwvzm/Ux/IktZxkbfVwXc201rFAs+Xq9v2jbxCQgV7mzkjaiEYvmaEi4SMeadC+Usy6tliBQjiKul/AH1ufuIlxd2jENnMvX7PwCW7uegcsCnDCR6hkm5v/7v/wrO97wF9Bi/AHvrN5lkySuLbLyf6KlFDRUtjg4zLJbt4lQI8rmdhcn2g61M4u9+At7wR6e1ZbDRgsGbeaTxrR/e6f690tN/ftlU/37pQFp91P/Zky6PbfJPqEV/su/oX3iS6Eq3WpIpUnNVa1/+ohapcVIa42snX96UI1+ChOcubNtjXxYlW4KgcBZjbXJ++nOyetrk9f/RLr7/b7x2Bg+MZ5AHfBfv/eV8cePUoU/RK3/R2PfOMNvn/3RePIpP6clg/gTBCOFnzUH+y//OEOJSZhNZiixmuOymFT4J7OJ7E+t19qxAu25z8isjQXa1LW0BdQ+sHT3sQnVvk36vSeEzg/Y8wG0Sc3Xem5Ydlrm/f3ek/b8WG3FCs6xWizwZXtuqFbLjNVuy3/Ash+w7I9Z3sftef/E8v6JV93nLe9vbwmv+k9bq35cq/pAq7l5Hj9rLrdPvtrWt9ifjD/2nhjDL3pPDPgPtvc+/npsDB/3vjTgvz7+j+X8acNq1Pux+29iX/Slsa9I1L7xpbG/v7G19M5d9wUsrnU6cscOa0zom6ZaiqugQkeX4h7Wp3zrbXCL+VuLSnRd/OvOu7fdyAOW6CvG9awV3mhR0s46tV9vNt9u1GVMv1xtUwW3CGkfN0wlNesHlBAZrmTbkWMH1BCw3DYAwZZFKJ+GGD4yZbj6dzThyy3aaI0D/fIO9vJg8xzdOfp1y9SDjabL4ZaetNuLCHcftnjvMEXuG6i+7oLIGavDYb2HcelGEXiz5zdXXuOzWfdgI2O/3zq4ZA8mlKAhg8H0MaTlJvabfNrPJ/jxySzJfcPL3Fs/y9VtsEYfYnMhLK+Y2VrokEmSVrpApXQIERexNcB7Enozn1gNErJ2e4JQol6LV4ZEN4SgNSmTWN9mYeGDvyeEfUFofB6PAez8rJXVY5o6kehnGaYGYQxAAJjejAkRiHau6P5Bv2/VBAOXd5ppbLiSqUsbeIVdCmva4yStwCxWv2wlGF4RmjAI2QNYgZO//5f/05gkaeh7xCZ//y//2YDO6BM0T81F4V7ldizwlF67VxjXnE0HCoPTFM0JhIu6ewUO6uE9qfqfmmK6vMjA2/c+crmaHTqoOzwpdI4cjwGfB3ferGui6g1u5VOGMg59o1CxRROGXMewVQAVWZ/SkbQ+NDYJpkCkaDzkROl/hUfKuutPNCCKDmpyvOYq2eD+4zHzJAoBlUoIgM29tdUzt0DYeON+uJkRr1uY80f39kCad0EtWBRJ1B11cbCuH+yINF6xKayToiqVippRyNa0iousL2wtGM9zHkHDAYB0f0Y2edpyd2VCWSCNFm9bgV/CfJ+lx22secyH8rfUtL3sOwu9jzbZ1HtC0d5hupYjCKeEzsLAZwL62muZTKiQGdQziFRCxXW9/l6kEiolBPUMMpkACJVbfwkphBZJMsvrLzCJAJZdpl7AE8Dd5yGYS2gNlUmsLvUCnmBcopQjYamPyDRCYT61MqDio2EcJCoNlPGQAqPEHJprr3giobduhlCqtbc8kXAbLbKm4Sc09suiNnJ44Go+47lvLookBod2wjODQXMWwjiEDsmToNDNBPhBkWgEK9DOWMNzLmEn7S5e9kfxeGVd0shBoVyezEKPDNYc0jw6WXdMS+IugEOgnInJ9LhBJVSAtNy+3E/nBlZqgPvwJa/GW637vF2ySqNwboaxkdzM3CuKhYz9r35PDc3XxVLVNI6gNepymQLUHTg7X3Jfu0iNjRwunUr4V+ZCWN/E97Bqjyrg3tud1+5lRnvdTedCqN0kcdr6scaG8Kt50icPUyrt30tJs/nLNVUTJ3UGhrvyPdTihLHPXKbC2P8u2apG+sju1i6dRWOOhHJydsUco+Oe2t+H959AaRf1QL/R32DiVfv/wfOuf7iuYczCJDPk5LNsn3b6N3d6jMG5Ns6Cdst/2D2t7MmOcH9NI8qnOfMAhEDcN36cf+MHSeaj5ejEnfkCFIiSv//Lv6pcwwBgY9czGUVyo7w01REvGKI0axjx3V6Hhc9uoGnmd28zNzWuMt+96d6CofIdXiMH9XuycFtUewf3yEJZqIYGhhzm20nYlFiIZ4Uhz3n8cmkOE/bgkW8rohnCXKXmooSwq+qih+GP35XZjNTvE83whQH11hLRuV/hnCOqsI0V7gHI1d7fELh0YL/de7u3x0ChYwS/0gsBBvp0W0UDjFjNJmVt9lYG4Krnl+uRZOvIV73rJC90XLRocIcX1FZnhnvSnuiq1W363k7wD2bz9UrCLXQILQVaSzHruWlDadzSkPsaWLyJaUO8gMIFgy05QYYgTgUGp4hFgAny7mrmQqiijDnSoalShkLC+996mrcvTUKSpD7I3uBiZPz9X/4zubfjMLeJSp2jnbRhXPDRrSkH3Juf2MSfp27sSck8DQYPk4Xe11KVBG44YzjFCDyM+41IFS8JI9TpZBM7pu6ssAknPDwj5Ta1Npm5HyoYHrSMtrkdFNrqr4FIMNEvCpETDO0L158CPCTJI7McoGj4ujv6qv/+ekxs8XiA1tGEGTja/7B9+5CtRgyzjDncs+8ZzJTOIlsPWnZkgHXH9gNjf82AQotfvYHPUsdFlswk0cMbAF7S+Jpyr+gigoLscqCuafx9WH+PtzX+Kpev2HMpnjUTtVyqYBqyGG0dCEiPpsn7rDt6zGUKbc6nWmRpGAKt86XWeRZOfNC7CWOPJhr8OQPeZ2PR5D/drAi5QkZOFnRFQoFgA8/8oGiBAoEaLbK6p53chgNiAynNfcrumKzd8nrJ74Ta1sb3D+NE73ZLIy1cZDJoNo1JDGTTxH2zdpcTABJimsKceVU06+KX7y2V8Sxr6A8/5hAqKCtye41xauygsLF89L3DS4BY2QO5pqf4LVAfAb6uAyGbGsmDsIc/RJiWpLVK9KeSle5olQZafthyYGOaf9JVKjf6b79UNdHP5vWqMv0jFm25ttCEEG7bSmOzIxZv0Lgm5FptXDa2pbKRKI63j2RmTBDjQFa6PyA5oCWO1/cKrOrQna11QQqxtvWBF9b3ixiM85QuEPuxrGFFaTsJ4i8Ha7ejdr9ZqZ/Tvh4I9QArmV3RBXTb9rgs2Gq7DOX17TkC2a1+D1kjIhjvymrdc5nvlRPfBAh+yzmK6pengfcownsT76Ht0T7iol2Z63evT7oZYQG078P7cKIljOiO12LmetuirdcMckFn+tFe5OGA/P0//T+gsPpP/7dinetbYNuGx/cIA9kkdM+9wUPlJ22M0ojA2sKNv/4FBTh3YI0b0Gi/Hdn59OcdDVlsjq0XPU61a3zWftuquL8X2n1QvNS230rSpeJxoydX0iaywVu/Zt0cbidH/CLxJdwrlL9hw9vwHpxDG4UqJYX6lERBqYZ++yN67fRb2655lRd+9I84nD+GhTsHBTkcXZuPiZ39ux1eoqtNdhb6JZJso9MNypw4RzvJgyizgp7ZYtdSN+fZIlb4FDS9pfAW3fgaw6SbNT3clzOWbNJE+DKorb4mlxWYaxss7/eNXye4bdzD1ylBdmUyjmbrOkTWxM2m6FGYA/rB4f1wmT7Rogyco53gN1qUa3inB78ph5HXaFaNncHjf+sqbrNbizEMxdZSAsqwrc0/p2ZCv+pbdzhpoSvK2rrXyWatSaGnA+T+O9oEkyszaT0Sf8xNQbpXTWPidZ9zflU4WLMk4pjkDS3HJ1TLML3TzEdBZne033vyZIM9I94MVhCsYe2y37g+8bt/I69Ss/D8VylFVUzYw1c/ZLNemc0apdzSCxOtVP4wIXDTzY6MsELbIDTs4U/+WUrGZNz4NoTO+aTfxgrZt/En//ZggGjhqgkf+bW6FmMEn4GuyHprhkY/pwzsSMLbsGPmayccwOIT2y20HhHEjJAFk1bdHEKFN5YpioXUdVKFTSkd/oJBNV3+7pLuWwDQop0EP6dmSff7fWtVq1itIgFAPsKkMTms5VPrRubDpGY+NccyHyaNifCkN/0oLSodJ2qCIwAO1bytfzn//rsei/4aBpWJj6mb5QiBjreUA+E6Ld31VXXBRgF2GXvJgNmoAcpIKZOYw7b3gpU65hkO/5cZSMMeoIy90IP9RonBcHMJBTH+/oAQm+SEEq5FB506G0xCy3oGPtb/FiYkT1NTw8WKKWDvh+4MQ4c890B3jW6oG9XWYYvWWtgIroMJJBC8hCLmAkcTYN4S0crB+B2UPU41TK+U24vNVw4aIZY0sGjlRAMGEZYPBqMxPXemA3F9YSnHkFLideaqOp8kqS9APAYD74pewGtuxPEa7RZg9X/gqUE4HQwWK3oKz6mfIfRoEvegX4MBC+aNr2Zu/CLx/B6L/D0Y7OzTE3gxTdwZz3gDz14Vu1E4eZNkN3An1vLfDyDgbp/fO0xz2gECGLL4r8MjfQjm4sEDnCYeiLfIrDN1OrARDPMMF+LZFrzFTwle+rHgi6OqBdeoAcBYtuh7KmTqzhQsUaPMGds3zz3tUyqNGCahZ+xMP+sxM0NOCwaXxt//5X83dheNdAyzSAmoYamsyLLutm34FEuhgUSvc5PnaxBZGwR3retJEBNudPUB9/5sOAvdfPAb96m1OUhRwdfkPsAW+jDoLV+ty2HXD3B+c7qvhEvWASpwm4X1IHWveMLDUxng/HcHgIgUGzJ3wFZJUcswbpDhs2akmZ/n4PgRADvGhrIu3vfkmVX3bdiyzdnMtoQnUd+oByoxdNeFGjHZbLvS6segFsPDO8+ibTT7jqmNrgsDgY033IeNCI9U9tsMCIpU+CLjG/Z0XWGobPS36QyJOu/hGnUqd8xwTZHATOE3y3OBPTBgsqQm4WatDuVYsLmeE8Y9GIJ90BSLaLqkxnfj7V8c/78WaG6zDSmbBECR5SFIfU9MCeKsgF8mLlzDnRQlBpLNfT/HdGVFixSirhq9vqILZOTsY1rj2OyLT4F+uJ1vqplCP5ZDU4NIOtl8dedBbhpUbM17R7qfWOO9L/qf1AryTrWE5vqyUcoH7CxBfD8WE/KunbC+rUTJ9QAidwsgNkrGTnrMvC1872PjYBkBtF8SH4NjYwbM/h3Bdh4g7PCS2Ddur30kC60f08KjsAX8ocdUOnr4FjO64jKs+ltRJt2MYSljQ20fojZMTmGwzrE557/uI/ewe5xrCJ3n90Lo/JjeQeQHzqghkRGe4NdubiTxrDLk6t5j1zoRLMPIEyNwsx7B5nGO+ayXJ2U2QaGNssF/l5dR5GaVoK4FmOBxWcoZsLzspg5Qxao4yFBEgeA+BdCLRJTwN5Vgr+MNry2JZM69m66cBUaA+L649jO7T5OySMvC7lNMPXYn1/4r3/VqCccADg6HX39Fv895BSoraU49d/qyCNU+tZYLCIRFRAM2WUm1NYNskmrofmHXVyY/I+LGGRHWLBZVEx/FPdaYRzxVdrGWIJpx5zVdnLA1g1Ut2NUDorb5scdArj4JLOwnuHwIp857YFFtkus3HQruoTF6XLdkuSHa7aXcSko0R1l+Ydhm0My9s5kd7C2PR8cdzCewBt4hPhy4R9QXSoubxH//bwbmUblxsbXnZOsPs7Kf7dlYGxhmf6MVYnWuF6yhjzXa3bQq44YSDLDlcnfxKjebRWhprX5/SZuuit/njXwrZUmnen//72H2O76FebTvsKG71zdY1s31s/fNPtRG+oFjJ4rdZ/xE3lWNu9dCj+Z1bcpe+Pl+v6/eR1cfY5/RZj7zaczIySexwLmfcd5DTEDa/IIea4E3lfb9Hnr2X+e18+/AOGijz1fD/IveHxNho7UgMzYwJsls5qa5z6wOjPw6uTWQ6t4BoSlXpViFbM6uWo+2pt56RESpNgX2SN4P4dF24+r22s/88UZttnbMhajH4bZvfwL1UUOG1S60iNG+QBkWig63g2FpfHISwpLqYiDeHKXGxmboiD7gA3/REl/VRqswN+tOM9cL/bgwASoJC9V8zI3+76lRY8HE/raMr578HuOxjnWm813kHPT7ijU7LhTwDQdW+T714xM/La7t0Dm423Sh/T6g2xOM1+wL0yuqfRU+1W/5Or3xq5cu4HkxWDPWg/TKedqLIEqHcgASfQh5beV6bYmsLYBfZ+iF54Emy80LCDPLifKUpjWiXH7NkNhDHg7Eqg3HLzO60GrL/DSzSYw8Ckci0Pgl9S1RIfONuEeVIKcIwtj37q6XY9mHjkOukmTmuzG5+wOcrIRWrfrRhqNgvOWbMcYJ/zSflFeTbR9kWnfN71eYMbwTaC5tX28o7O/quAIWrTXFviu4RLv9WWrOnaOd+UZU5is391HTzO4ZzSOvFVv7bobduO0+1jSNWhhorZ5pw+LMa8Du5Oak9yxzp2C314Lbtfnz+23xi+99wbBJUzlyt13a9kmUdV+S3UUorV3eRdYKpDCX97TXbLtYEpNID/qNB7clTu4tUYpn3REeZ1GlbptrEv9ZbaTuPp4fbkfZUps80iBKyj4PE7yP0VLWiL1yfg4fosXFuzMgSTO8aC1EZIvZ22bb9jtN/VIezf1L4+6T7MlGpL+xbhcPFGqYZW7VC3P8a4ZWkz5pV+RWij8abyf1//8jPQ+gLoxOPKSCNWpzlz/MFpM7Ya49vnuLb9jUcuc3dnfITELntBKBjUy5ueaC93m0v437uQSUodHuolqNLxUjVImVVTmOI3rQ3V/Rymqsa+3kZeJ9IiR4c+d7TJDh30J5nZ3fc9EvVr/Roq+co53qfy3632bRLwidy0W/+uSLfs7jeUFwR3q8vvDPH7Dwe7Dw29b9seM4803rfnUnMyt2xdbVu7LUveuXmakqiVklonR5Dzyvjav3XmtUX5nogfhbcHnxb8PllWvnrhzU6ZW5wGjmNSvaeljHGE0HR/EY/NZfRGaoxVuFiKSQooJP0sAJuSWjGiJO5m78KjfFW8sejannhL1JmRdJNOiFEYPBeAewx+8k1vK7ySxE79y7TXW3qsiNtDUQ0EMUGVzC9qkBp7fE08G56f2cJ7FSVijusHklE3bTNcQcXhgIjLdVe/x4m1a/bn4Lxrk6qt87huq3qcC7/f7vqY7zNza2aEe3wNG0WyzzVNAtFBD5UyhNQ4h1mN2gCIBjpifNyJrRnLPNmxh6smVZov2aG3vGbVLOPCNOsgjtQK58Q0S3BKUIGPsjineYFz1yD4s1mItG8PZ9bm8aebYW0b1lPT+L6YJPP+p8avo7FmVVgzNRKYQMHhrNqvWU/wP5A+W10j+QP4wVMtBDq61tBBPkghijJOyF+TGSjNfwESQNH4XMwOjOOgiINoJBAsvnWZZENZZSJdeGspZMyOAj+903JMTPhq9+6jFt67qk8Wu302FEF1EOwbbcW1s7DNrrkSfDpnoSXo/M2F6PCgtcGxSVXJuKWvKvmQpkQSczN0J3D6ZF1atvb8s/YIIu2SlqmLuLQBoGX+qwGes2Kp+mMVu6Ji3MUGawHuVqDdYjErxpG9DVw8Qdxv31NtGKRujZVbfo9jlucDOAQst5f/34NznuJUmiW3EJ7gNZVr8Es4ql/Hy5rHNk7LX1cUtmc9Rh6QYm1sxxIQVb/Jvr94+Df4fzkrm3Rhsr9onmZ21wNo9KPehObHJqENdY/MWv9gzf4h9zH2Z3W/DITYMcbx/VfeMu6nJANt9zhpEJZ03Mzho1QrEIWR/qx8evOjMetx/fn/pIsO9Q199zoEYffTKu697fAw7/r3CRedxUCfwlARc6IgT/ajpfRCbzno01koZBCVVw53I2OxQX2hPYusK7VNwjv0O9Wy/Mn4GPIHj+DUJkBdV33l+BiQogQ20ID8CvGhAegHls5aQtRgB6Fr3OfHRuFRECdnbiTieUWtr5lbMA10RkXjX0/7y80q3qw9jzUz/2ENGfoRlodoPfpeYiTjzQEisnSGkyuODOTHa5csLQhNs7GJ7F0lsqcHbi3m2Y+U/nYV7k93GObRGf8Ni6l3siPM8eRNvcY+OzBxZfrvC0ujPwBuL71vz67kW1NsfCrBn0JAOyyYRdhlocI6Jam8XDNlP927aoqPcwzb/3jpV3nfnVKEb/tvFGe3f2/kE2LxsveGryAJqq4QcKUa/v4xEib2OMoW6tR/9ky2tl1FZz3uMxUB5yl92GI07AQ5A0PyKv8MHH4enFiQGb7B7iY+m8dlej202h4Cuv0EBcoKBn/gS603ihe2+zly+zpEgmyexHP4MtLIb1EoQs72ELt2RCx0kZKQqWh2jLveD/tipsQdZd4xZU3TV4wO9SygigVyOA1FOOnG3QIxXYGmb+Ot1sQD1st/jvfvlAW39+hwvAgAlQpWs8pr35O4/XQE/KtQEo6wNQ6gOgYbtf1dzlV7WYf6Hmtl5yt/Vk5cCpKE/R8lMNh3DsYGPC/TqSj639Hh4diebRsQUI7kF+V2YFzjCwkMIe/KG1seUrT4Mgu+Jhg1Ssav/WYApwNcSQ9jJL5pWZ0AXEMwooYFEulG7R06x6ZDwsrwdmE8eJ5w8Ls4+qyOmR88VXnc70a+fJH0XG1HnkwTvJDj2PC3/qZ2ZqdTrpkdPvdNKvY2sx80FhGYzSMeTemYvylROO0s8PxofsJThYeP7cTkFlZJdmCsB1blbYFc3DD76NmR/tj6kfe3b1SD7O3Ni3+yuhOJqvAMPEw3jSuPMUYyeyvPID8IjvqRFZ1SJ5xmJsoeWJU/Y4FMSJnzJIhYB6oFZQBsemGrxYxpAyp3zv7zhOwn8ul9NenkS+UCwno2q84zhzy+p0zMSZghLE7PV6UwBK7QlH/U5nx/Q6nbKX34Tpc9aY7/k7C9JFRjOwoGn7Fg1EZyMuH4PGO1Nw+HemKxqp7t5GzJq6xhRbxXWW3OICQuhIk/wQ+3OIG+l7hrKQUyuN86I/XzlYm3P0wi2ue+5Vbsbd0Pp6v9ffp0/ZSwRZYmMriovGQNcDaxH30AlIhNNLGIZZI8Be2HPTdFaZILkHJIzSWq0OodbnLmffrxIHddJhYD53d5omgM9dTccuYwIekZKoHM7OPmTaC1+a10nsLxNv6XqWiC0h4wgC/DWe8JZetM+bETsqZ+TOXyfl5Pol2K5KNvm5q2VJZ24RJBnIXMgLdwKbavb/tfduy3Fb2YLg+/kKEJJpwNyZQiZJmUIKzCOTtqUqyXabLqukFENEJjaSKCYuhYuYySQizsNEPXf0TPSJmOiHeZo/mOjpmYh5OP0n/pKZtW/YG5dkUpaqazo6yiUm9v2y9tprr6u+uxtJ78zo2CrRTehEPIjdOvb9DOc0JGuA6NdzGmy2KB1utMO08wMeh7YoS/TDFJqJ0Mm0ak4LHLJ9obs0WKSMF3A2e1Ef0Fg2cyNEfN7SUkFkRH0ceaTMnlQmou50egOzF+wNwM+0QFKxOQKvRrBfnmONvKfxyNvbM/2Jd+4Ee54AjBL9IkBKMl/KZmm8WHy7wCA2JWilYEs/EgiRubV4TeJzkDKxWsZ3QrGAdGXmfGWS0glHgcGSyTSJsrAxN3kRKTGBmxF68I2b0ChME+3E/Z8xoK0fpxlOP+CU9UzgsRzxmFuwHvWSRiiFJpTiFCZOOLEICk3GfUrgfBMvz4IbXGmFJGoGK7801z6fyrJPtSMgm89k2SeqEpBU8hNdlnwu5SjqU88ZGQD6sygIyQPzu9QNMcRPUYc/TvFfC5zlajljbtrE2olvrNePaQWjQOsphEEWpNsS2GTUIL9fRKKYWZboZeasEzCT+0DiL6Ifpw47wDQKC5ze8Y5l63FEIQRHnh5ELBf9LCOgCk6awNQAlBZg8tUynqMs0xnv/lvQTt/d/XFKcFPocKwwdzyq8/7t1PDJdAMjRIC6q3aCjDYTRPOfcYbzU7xwVyZKnDPnmCxQ6BSAHefjPgRYNkJ0ZpZo6SQQDgOt4G9l5xL3XY+O5mWQ5TjCqaHTmetoiV5mJvJ2d7sLwUqiFSlHHRgxv1hdLbLmNhSiLcK2vhdH/GdG14hzeRmnwQ3E7FjYMQqyn/OF7ZfVUovJjQu2jWBd85Xh7+72Bre3A9Pm6b/ESWmiZzIEBL5RyHtGjI+8V9jNipTAQlaRV0AEeHgJQjQGKMCRgQCovIE5zuF5+UdMkBsncvvwJIeTRRqnJIc5HlfVcJbDQSEHEtwbgrp6x/mOnaD1fHNo1SSUFE+qLqo1HOvV6ddZkBTy+5x0vBN89ISR57TPFQboNS5fj5MnUcc4pQtNt3X5QgPvOq/INq5dDxQsyE7ZgWOhKb50PwRErQfFsMMf3FTzkTcyPMfwnVg96SZ7mtKR2b4AFJZxe+v1waun4aP1JG4dJfFpDty0RD+3o71AHoFZouup82o6Im8C7Wa6Fg4h4tQIzDUQLn2I4jLNnMk5Ip/KAMkoaLqMfaRkCUE4OwO5CWJRIZUMJagm2yO6fO8v3FwGerl9eUPJbfXKTVqy2DvesWgekOTPsiyYR6xBuSLwIINo/iqIpI6SFH946UY4Y3BStZNRytb7buFWcyQZZzjPlZmzlumSPItml3EqdUHX5Wc8yxuJPxLwaiSfBimVIDVynlWAx+f8PoizUxozyquy5VxC+MnjhcQ/FFlOMr4F9xhKFk8Gojd94UmDeA+GT1D+rH34eCFvtLL6UXxNiQo4GgWKEY+VbBi+Y8SOUTgNgKsdlAIcwQGF6kYzXMuLoYPGwSKnKDbN8Zjw06P42jBLOh52vacOdYsHd2ThSCx4SsIXt7fGTmNct7fNtBoxRa/YgqzBXWXBU/HaB22Jb93ZpRJXWgkX7bFGUEKXqoEn5wTp7cxB3ySOIvJoYsedT1cibOaUEqYmUUu0OidKP/VdhPB7oDA8Zw0pmX0PL3COjaU5IiIkhlxLCrGXoGPEDvhpkYoTQt7ppEhK1gGQucHAhCM7hjbEzJBH8oEqVMp9PGEYmnZoELYfvzTXXpDN6LrZAlT9ESBwwxS42e9XxQwS4hMYJIitqu2zevwlYXhSbQ6ZFRHqtxGhJRK7tG171bb6ZlmWpmHS9UwBh8loBOc/0qVzCtYyHEVOSq49PC3m9s6Ae6GkRxwcV7C3lT1AiesRdEeYJBb//DYCLxYUTf2kllASaTmJvNoZoOpGt3+YIjLmb5cgg83j1D6ZVo4D6TMGqdBhP5vy4QKW5bbgFn9mWCUbwSs3nQeRbaG5mxCPGx5ePsvzNJgWObZJNMYeSdR5e6/qtxeogJJLwB4gl2D6X2Ki9JjmOvLjxSK+/jF6lsCNABNj12rk/XKZ4uwyXnj2ALVS1/bg0KoEbRYjOXcGqEHTs8S7wB6Kqfch+Mm0dU73kZjWTcrT3hmUFV7QToDJK7h9b5xicnI+eiOxBAxvcnLuvOEywdCRjyd5eswp+CX0z5KxNmRFtLDPJg6B4KWffIGBIwFEO8MaCq3C+Px8hCdOSN/+6I3DAr+jGzqmOVaoDcNEz52TY2tsGLFzM7HOGzfKFV6Z43Eo05sW90TGavrOzeSkNzhv3DwtdU96A1obpg9LeHJ7ewLB1t8ck3VU+9lxnOdACEqJb3oDSH5mmutlxeV5SYbCZ/hrkIJrL6jyXZzS02vwTPnWNuBWhGnTIb3c3TXmzuQlDBy1l++9pBwZ7jdWw9jx+irMU91GnTCa7Xrm7S3pCmOY8gnbzCB7ln8beUbYb54V4E2C0r7VtRC7u0biYGyWpQx1joeWu7tGneizuunHvT2TgOoK6KAzxyKcZwXa6CKowDY5QW/OnXk3fK0J/NnPZfz2rGyekFPHIkdudPr0+e7uM+MUZncyMk/39mAkp0+fc/C+cm4mp+TtdcWTXjpXdF/23oxe7jhOY9C7u8aZ87LXSG8hRF+iFagOlGVprG5v4Zo2OgncyWo85+CD4PfgHC4DdHbOSawoBh105Z6htJ68AKbgIUs3LKXbSCvAXmUvCXc1xT/QNh9GhGyjo5u5i1mxcHP8M2FFm2hSf6NId+G4+ikxGO3qhmyU4gxGKoMzCY9Amh8wiIixCCBcen3C0BQqhSQjiZtvf84hluzuB152VCSUiJQefTwm3zfUdNwUlGfhHBeGyWrXHoiCgFSoHzYGH8TY0Dc7MzKxy2FIIYBnQMIvahRZ1ZjcMD1xm56YW79dGSC99wLvFREiExgSq8PWSzwR3l8Hi8WfiBBDerqI94Gyw+zWGiuJAoVyMrbC/60XWUwlZMpY0E7MKG8J/oWop2xtyolbFmZ3V4+vI5yexjMSkQXYk81S4+bKNQv1lYb6TPXv1wBf283qBn/ZbeKBFH3KKTWZy+6Wp4b8PFIhklO+nlkqsNtPiuzSUHaEFWWDAGqRinFEo9Jj3UPNZS/N+/UgXcCIheNbd7+k2fWyuytkWF6vuzQItw6BcHA2lNnu3d7JXKjzI8JxO23w1BuD6cG1m3rA3nNnV/RnB8PDa3KRQsEUKrLLFxVL44UPXq1WRhMDiPv5EnvFghHJoCISzYIF5hUa26fXGc3tB4Ffr14L8KJQwqkqh8X6GA6L2PfG6eFYtQ2FypLKrpbNzVwds0TzzqkM0I6xM40N8/a2eappdT6wtula3V03J1MTsa5/J5uqE4hKNDi0gIngtcgbcmiQPeVCKpjoKkVECHNSpoEOmASpVQChdAHtd5eiXZj/iEBjUnL7Pedbb3xmqNxyxmUV7Gr6LVRKfaeL3hSXZiez1W97HrK02jXNCXgPhYg4A/JH1XruzHd3DYB7voISmmJHQT4oUpIKrOY42XFAGcXYwKfdcxLT/uSraSL19fJLDE+stSg3L0H6tfGsCA2GTUxm8jT7XWukiDILp2PScfXCcpdqHsBF8dS6vS2OG8LTDUMfbcM7r+1IoS5++8255/gt0F2nZ6sndnBDictWatKo0yS177FKb0pMsLroYaIUlEVJ10zUdcmEXKZttIotLLOVL7Dd4FnZRsp4zBVh2ibCaoFrdaqmo4+7ixnsHLVmmk0iRJ2nTBJJM/2uSPNLnMkPe8coqIxPiOiZmMPjv4SCSejEvcEoPHasUdjrVapaxSQkb3i/f+lmoK3lRtg0wcdrEBV4xNUuCM+DZxP9C/5CnsOL7ziBf8ceXJ6sFAJtB0h9SvJ2d305E2Tzfh94hI6jvl4IR9OUWfia111wTP3egHWx4dHgLZlhmv0sTnPDCNGcBJ2m3CAymnFIZRW9Of1rk9weyYOIYPycoBY+CudWsxe/CseUv6ckycxpNUfmAaP6O42xZ1AbJKPmEjSTVDbrOTKIjAuxG0bwKoS8UWJ/NjLm/M5oyiE5bFZyy3m7dJPxngqVWx+rnHBf5kx5gv0cMhb3vI2BDNoMjN1Rmm07Vtur1u3k1HkbM+4cGZ9x8GhZnd2V0xwEkaCFXF3gDhG2qndAiCs+s5ooWmROzgUlUy8CB+64MCvm+43GEWO9KLGCNM2bY6dg8FMvwSRzN+aoFZK6Iaxd0m2hT78UbXBrdvQjabPv7u40R8jn0xxg26XQEOt09cqZDjf8TaJOE5DrDWGZ35DdI8JEundnTnOQY8uuzRni+dUIW1U/oTbJuxanUPZVUlQw0RxMJvgddKMuy9xN0HOn+GpI2NHPnHb9DHI0nt3ePuPuF59XPGi4+r5bxG7++IDqWT43R892d89AtPGSrNOzPpjVkDwLnX01NE30zHnZpQryrCSMcejyDDbdPHXiPX+EFxkWfZ71BqNT59nk5VfD8z3yZ29wvndT8uv3pXM2evm0GL3c2+MDBZGF8dJEr50VuVwxNlGKuS7f68q35fi1Sk0o2lIvzRHt1TlFvF8nxeh0z0nx3k3JefVnU6NAz5AntLI6QPQKXZVcfNWxw9x34ZmJ3kjarHMTmLkLgyk5CcrjxrFGN0/PRjfV1J87J5Ob89Hz3V3jzeQ5AZBz58Ysqypno5unhVrFM25gm1qPMazfDZWbnKIrIY1KiLSOCxlvb1eEyHlOHebVbz3lUj0emOtT5xlr6VfnzeT0HC2w82vlludk8iunFkZXzgKPF4T5vVeHZjve80sJWH5tIWOICdHJ5KY3OLc30HvGCboxR1fOr+NfO7tCpySbWATcfLENuZAdD3Z3X3bgboJW0Cl/FGPMoPU5AC6DVYwlYMV4A7TeEBC/2ns9AgDg1g43zMjhiho5vCbWDSkmNhDPqW3DaYlgC5ybcjP4nqCT7SQgQjChCGw2kQuCUGBPJcPsEE62rHBFg/HBUGWIQth27e7Gx9b4lERmlSTgBQTJwCn0J2gOpgfhM+LCQ4CxbKBwB2wTGziMsmHG7ZnUrIZJk++zdpKMF/YRC2pLUmECq4VKk4m9g+tSMqHpvrtrFI4vSZpQ7PhCpNRko0p2HUZTjAXRSbkEjk9AVehQt0ro7LcQ9tBWKVHSpnPsOfwNFHJD+Mm5XRCfYFxQBrQgk4eFld5KjChR6Vf061YLXteycgpJh7tGXMj6JERlFSypeYKkneuPiWn3iwjsgAaWaROCL17gPlhjGhevIPoZWJbxuhqEa9e+fLiOS3aAyy81MACiIOVpTFbSvzBRb8D5DZ0aWJVMltzpdc46Y9dIKX3O0gBHA1kYx5V5k1BE8J1GLTLSMdGwuFtBQa4Ijml+IYxL9lhvKGeQpsml4tcvFQJuFf9eWJS0wx3JnuFgYRgtEtVeqyzWfATkS1g1bSG/5wGNJQxTmuDcGyB/r6IFimMn3N0tnjrzki9kqZChMrxRQ5P1BrEYe3evQ1n57/bW6NL+C03Urc83NyuLiVKB9YbOYQG62R2vaaqe3dIN09AGnitQrB6nBprD5MzohrCvaB1/RizrwBCGQXWTLymBGVdD7NZUjFs0FeONmooFkhSJTcH5E9UF+4iriFf8S6cJM5yjSbhJwEgYcYubbhK9ndLZ3V1y7ZWkc78KAOHlpPhqeI5C+mNvcC5T3V0E6qSgRpYnfMSJc0IeRnPnhB4cFMIvMPXpfnwTwEhAcQqdOXFvRch/YKKbavd86E0FMVDJamxzXTmLIzNTwmZsnysMdUpCYs0wAfbIM8ynTgs7SVZXQm+cEyEf/SXO3QUlWWwIa20Y3l0D8zoGZkhASjnNFDp/iqm/iB+jF3wRyUO+IpfvWdHo3NvxmJGNBSEQE0Y8zinxGBLicb4XcptYdEbB354/bSfX9tqZ57u7/NgqUAEPiMRUzq8QScNKCXmzaY4ab2lOLxRPazlmi45Y0caXIlglQbG5SYEMsa0ntqFntYkZLVDRe2PaNwzq2iudmbIeFZhUlSVquUebDDeVQjTa2XSGSSlkmZc9OZetI1EoiOWR9zQkhpISE9s7R4kTT+bnI59KPhPhRdcvP5aolemCJqnVmAIxQ5KYQ+0ioDqa5LdSC5b0KdkO2CoxLBQLf8HIG8+dY38y/2p4bs+d4+vQgLmbDLsV4mInGaHQgmsKrYRobadF7YA1QthCzF6NmR+1qynIT7MW8U6L8g79IhZOvZbs2QJi0VCL3s7adEjd1Wn+aK3I9dSCnlAfYj+4HeQdMyrumkAQRTil4y/uHC4pTLObh6t2BZB9U6nJ2pFWn6q9jqeqJF/aqoeGzPOuhplOrbCp6b6y+KlsDgXuu0KMk3bxXZw+WzAuhUOfZY61DSwrqjTVUz7skPeOyIVOlIiJdwIglfe8MbniubI9CIUhaCdxOaSPiz3HABL80dCWqYGi53imMHztFiLLdoyw9IKMn6MC1rVsrgRBrmQV2FBJHDT5SSAaKVDba0Cwiv36unhdcvDwDuqLyy0IVhRrCCY8ROJ27Hh7fq8FJCrTDDOma0doPo1UJOv91KlxvpqGHlCV7g2tTIcy8VB8LsZDt6WoixX5irAqnfsETfHNFO2NwyZfrmFxwqbRu2MOo9oImnBvzFGMQsrtJ+wJRc2CI3hQFYAatliTSinAZ9tSOuuyunurPW/pE25pvvnEiG3UUIdl9Bm5u1jXUpeUXgNftUQ+pj6uwfNQDsIzooab2fwdWtd/8DYrn/ilkLJ3aOTVFkucoGqtqIeazqX6qOPVsrKkZ7Kq5NioaiiTEM3PHY9b2W1e74It9rx9sZPmYofbLXb4SRf7mxVZZ1E9boVBvwPv7BUcJW0NfA2oj6VV8Zqr4m+3Kv7mVYnvC4LklqyWpagtS/01TUDr2DLXTThuZ/gwyCZ4SrHRHimWkuok70FiwJW/Vm2/G5U6ddebxDTQ/X6DpK7xd2SZvLgm2snpigkn0dFhF+8iZBxz3wknHsj0yJ+9wbntEy3yeOKdN/TGQYVkPLZkYY/nUOFYi4oLlZYRLghzi1ENjJrghMR/lEedJc2d4zmPCTiqnj5A3o+8yZwJ0bjioFElUdUWFPZ6pV/hrH6/73HjC2iaM2TEs0mUbL2lqd7DXttmfBt5FZWSu1f4LHKT7DLOZQviwpGUD2ovXKrrI9wk3AkplZKCD0oKscm1L5svd2oMB96iyCOR4QqfshIJO4GUYCwFJoigjAWqrUS4C0QiQfkLPlnkyqVL0Y4snLrCXqygz42a+E6xZ8TjsWW2qRD9En8XKdjUR0o3dDimys2V1KxbBf0fqz/RwY2oMw7q1r9GYJbtXAfigCxgmqufRA12o/pr8PHar10KmKo3DLPsvAyqd4vidWDdamukYGyqyanoWdOkpsp1u/V5BQ11pdCUj4+xoEGnpGwkrtvlNt2vMHHwyRXea0hcxO18fIj371gCBTc05D2y3LNJdLXXaRh39MnFafIb3nficQyagndKipDnwBXjq5aQ7WXpm2l39y/gCanDSLYyyaqtliBW+Gnryj92uBOGRus7jsPx5j0pHUandNjFCV9ayvXcNUBg9FWXm0pg3N4+tiyUOMIiye9gPpho6XRKDR2ZnZ0cz0d3bo3joyWXXrW2yJ62H0UmLsd8NGIdyzvIRq6fkzA3TpRtSiiG6KkTCOLAd4xoLzAfDW8t8EzE/RA9jc3I8fcGgl7yjmMzcPzeQH4ra8J5IkS5j3oD25I8ZTf0FCJJTyFQ9RQKpqcQUz0Fv6wosUimxPzxCniaK+BprpzjaLI6Z/fvXM7bI3+AEuOFcEQkV5GIxMuV72VBvFUJ4j3qIjGhXFXQii0AYhLOGRgQtcfR8qm3uzs3lubTYi8Ymcu9PbFi8fGgcuDJfflReo4pPtHaK0q6nTjHJ9BIRbedONFkeT5aTU4YkXZCSIrl3l7JVfbUZklt0jB1KnpWtXzsFGrDyfnoTGqYrmHS65WJ/GRNeskXMcxbvF2hfyPuDXrLL2JBB8prmFRruORQeBZzLSDOO30K/udIELSX7iou8m99HzxhkBT6u4KjP02NdZHh78Du42wVzewI1Ds9IlA5jUNq8Ap+rYB5X0slSrSFoxNn5eCKR0f9fj8uK/AnXf6MvWKGU4hDutwbIMucDCByGsvzjTVX1g1QSD3/gvK7G0Q4pY9IQAcErsUXl1llNmi/vcbuFejcgxo00Wdh+jQjrz8rUnB8zZW3nQBVadCZw2nb0FlKmsCiELkUVrz67e3Oqi8Gp0oXzpxl7XXG5ZWrPp+Aua5+O2es4qmz3MYUYyT13Cfh40Eb6+Lh+qxMlhdCY3Bnp6019MZZ0fk6ynbdOCeK+zD0XBqMoiL/jE5PFjQphP8pEP7PKs8Ep4zF9hy9dJYtAv9T8g4ATxN0ScSOktyXcBle7e4a9UzA5y/RlYnejF/SVeiL6Tgn4wvyAXpN+57xcH1VJkukWUizzAtbybOQJrLNC5u1BRpxFzT9AsRrc2cNAC15nKH+csn7me/fiQQtgNXegDeOwDdOKvuyEHwicV3bJVUgAX8jfQGxozfOzvPb2+cyXQ0eeVU6+7mkdbLjODdj6RtyOXKgefwLvQHPGVVfzs1Yxik3srKXQC83VXVpCLYyoNKmFoDQfrS7uxqfWH2fIxLDN20fKAHjzImbbh3OuFuHJXW+CK6XCUYgN7shPVSXRMP1ZgouqtiFyKL/uOQZZCzBvIGdjJ+xD7fSmp9J5SCfVSfIWaGz6ihSrxWA01WTQO2keailaicj7ghn2xNMoewNgbITcm7LUnJHmshvsrmJzmgEy6RyUkBsBuSMyisBrDVLX4fEGSxKKv/Kz8HpOXcHD0i/afxu/zJFbQbr9vspqt669vUUsHwk+6p+yf1lbhdphAQPQEFbpJHXQYqJE041zMjOTrC7WwjD0NPQWVN37hZiEQvAk1Lkx/ZQopG+IXPmvoQn56gQ5lmxMM8iXC4KAfThJSG1EJBaVFHDYR+8QqHECfvgv/KHeJRdB/ns0piTCBXmGsIFQfwT8OEOcnwcee8XcZy8x2DTq9sCpOd9kjJakooEK8fxgpwJHYSaODeWfUg6cReLFx5a0/7sBIHun73sw5/StGv1U5wVixxamNdaMCs9kEI4apPz7YBxZDL8AadBvpKCQVwFkQeu5Ike4nuo9J71hMS4siIM3XRlX8RpculGmjQezY9T7cHDtdwf02HvHZnlBfJw7gYLElIgSfEME0f6YkG0DIKWKpPNcpz0p3geRPo4ZqtVFIG8TlDEXvbhDwKAfOFBffKj2RSVRMXVwkBj3I0d2VS/WCzez+IwcQlssc5t35F6jIt0hm1wrQ4/Slp91FodfkJXut2eTTyiQPMEIqWGkoUbvYebvE9lnbYnjSDw7Hk/8Mr2CrzRejvLIIdmGj3hlOx4HBEuROq9d5MkjT+4YvPtOdvgvodnAXG96Th6iv9C1A11CGRYByk4oxyeeEHRbhs0PVzPCdRANIPyAf9qwJDGGxPAJMbmY+yBTlDJ97OsHB2CcCf2tcK8E/gpVFPYJ6eUj3XOUIE65IgNN5THeOlmGgR7qE5GBfoVtNPFxp42LXIoHro54anVTlQReXopO21kc4nvnksQcfB7D9C/YSqQrcF84EepGQSzk01gB4nPiZ8iMaELOEGaugAX1U3nCwP7+4y2OiFizH7L8vv8BGpVBS2C9jXelHfBQq93DUIGVGjhA34vjozo3Gt0DmU0KKNlebBYaLSqrT1ce3AwL0gAWNk0VrhVPw0nYZ/3f947DSfz6pPY++5ILPlK9ZNfReMxi4Dx049nL3558eu371/88N2LH1788gatnPmdhfjGLHsrGGN1t/91ymOVNRWrtNp9GdD7EpxHi7ETwTzc1VxFnH1tUY0DAq/Iv9lY6R4xkuDfTTlJoLeHTtEFpaB3hWXWKQnRyIdUUy/Rm6mzruMBjhmq60pHLReluD3lUwz9KSdRBnaNHs1W6FfKyYeiDqm2XgEkzdNRA+3aOv8lsKgck+37qbEOsqzAhMcUnSziDDhMcfSHIkx+ie0CouwEEf41yILpAoPYldOAEsdBpuh9xyNOUa4w2eZvs5mbYLguDLMUenOUX970uHKFV158HekQbIO0ec056y2+U6TC8MYIzkV0v43h7qdFnpOInUAk2OJTh5hYPRKxUrf1GSyERldGJwsTzK7sQAoJp7vTLF6AwUYQZTjvWdpNb2BByLfpwp1dPRpauhRu0M2CenRvUT8lGmuWlscJxFeO8zwOSXNDGtlHu+5N9h+TsJUk0M8sXnRFo1Ii0GaXrhdf9ya9o2T53nq/P0yW79P51DUsRP7XHx5CzKE0XoBH1MBdxPP6MvAFaEamv8Suh9OWSHHZZRpEVz1rY5R6Mex6NK3uyOvdUb3rEcqqAGkvyPA1vTu2XGu0p3/7zyR4+sZ6NKKZGNBQCXrFQx5Xcbu7oK4VshpzP1DDqdFogdJa0Eh1DGiNb89OTHk6//VfdWUoHx9G6jNFGqNQpnk4Z/hJhBcrFmrbXvAh8HBvpbEfFGwaAccMaglIG7maIoribF/GaxWyq2M4RKgMoA7KHglgzW5X/VqHBK+8EPGZlThtVxyXElTKOgpER22olKPMfwdxEgVBgAjrnhIfoVMRMrFk/rQTG8QKSSJz5NywHh5zEdR2SZy0RuzJYdvhag8L2QwG2R7pequgrjRuRI9EjNAue5MjgJlr9legFRJ7Ans9eFXpiPBc7DU8AeYk5wQudtsvGSK7DDwPR8AATQusb4oy2RaL+w1sCwDCuURQjre5ZO6FabpDLNbRjBr/EZZL05FHAZEy0MPx/UZgUCUV7GlxkZsi6qM62U705QUZY+TMBSoj9/Z8dzcY9wFEC9DEaoka2YnONNHorEizOO1FcQ4Bi+Nr7FV5ceLOgnzVO5DKqw3tc7wYjnX67NKCTJOnCxrGhPWl5bG2p+Fl4oLyVrU4fynCRPvtb/9Br8Iybg4DTaMrX8epl2n3CVvKnhg0bCl5Y1UcnQ0dQmT5jV1uCB/KO2qLOPlLImJ3O3ririD8oU70dL5ZEYERkcaAE9BCClkoBwa3fQecxlTO1xQGsA+Y689TIzC3iuJ7d3DdNowlQFYBzNBInOOdRIVJRmUBStlMtlAUlyzhHxjY5pjRbbi1Bu2DTYTMnch2K7TatvneWP/tP/4/uq3/9h//z41YsT1OunWEw/P28Ogb+m8JAzunhMkORB7cEBY3T4to5uZYQhp3XjOFcKQxGFomEq4ABkNrrEMETxoPVpxrr/OgMbDLm7RqFxQM1bHEbWfszzLPPno6sIYH44uH66j85sKmn1/xNCN6BD/Nfh5/FyyxZwzM8o/fXNhVVi1/aJavvrmo+nqf0fiLxG/AiEZhJNKdwtEr/X8ep1CK11k4UVs2c0ZxeyulTamL6VpiMA+iHNo5Iy1C4FDSXp6u1oXzh7Mff+jTvkB7LTLLGfDD2KrokyLKcBq4i+AGEPy5XoqQV1wXIRgXdrXTgblHtraa+Z9IMPZ2boRgruNlDkahdNhBH76lNWB90nTOnYBQ9z1IMKXOfiVbyqQGkSw1kFjaNm2Bc9+iGvctEty3i1FVlZjDtFf87W//gXz5QRRklz9jN4uj8Vgf6yVrgEgsohyiRFdCisAhYaNzdRHGPBEkHHSJ7Xq5yyC6qgrCFy9pscUBKK4qlQ/XwfhCMyDM+tSEKMzlBeW6V0wVW6rImatRhzxhVNXlnGpWm352V4UMxjYOMhJylMUw0AzGRSKDG7FrjDWrccglsxmP9SK6ioDtYJbVzifYqGjASLpaAkfXK5ZJF468uINW0GDxLhR8W/V986n7HqivzU19F1gQClWJAF0HRHeEUABqKOa2K34WL3owLHg8VPfc1M0wIXDhptvf6qa7hqeypbwW7rqgyS/CgGm/zlqfPb2BBk/ma4XuCCgR/nGPg08/zDsAio1fph4bc6m2+ZvMWMOBJjG42/AbwR72XbTcnWSclmz5+AynvUHHowhSIRA8BIuX8BjSp7pEvidpjRt3fRnkOEtgGEmKe9epmyikdRvBRjsga8WwEkGO91wHJRr4zM17szjyg7l5/ujA+r2r09m2umQwbLFmEkr/TIsGPUirFoTuHL8v0oW6csY0QesiXdhRn5T4U7roF+mC13ILL4ibtT4d3N0H7KTRiOVKsDoIsi5EhCCtBaknZiaWBJhb/ziTq0Zzv8mResrkalcrbeskR2viWtaO5CfkKU7yS3tQyvfsH6fGmghmotKUKBk3nRPdyGw81vWRFOCvIluDiqIDiSD3C2ZKhChx1ATWEpQIZeHefudhBkok+x1nufnya32TNje4tkPt6ICNTtk1qnzCmVJtzdDHHOMw1J5d+gNAIgHjSW3klkjVCpXdVEFE0QoR9qdESAHj5VZQ9paoX2eZC8qmNQqqbY9oh6std3SeBp4G/8A+ZIxkeT/w03OylcCXhb+r3mTYyjktMGI0lw6iG6nAhgd06+7j5BKHOHUX6n3wpf4livrQNvpS/xJWBthSFUk9boyjyquPhsKPhPtFSWiW8hqjfpzCO7HZLk3XKT25Y9XblhAHa6IJLUO5H4Xsb/TGchtT2Go9K1m0vJac7VwNAciowF10DIHlfvwQuLR7wyDOyXbyVyFHhHXy/J40mAL6/MmpGXqjJ6Rr5CHayCA+OnVgyepIN/VNyIOfN5Ws4M0RARANv00b+CZDlHoNSvAXJjHLK3jMPs9CiObpUtR7+7iJVq3AVAM+zT9OEb0bgxIF/cCTZ6qgtz9MjTVRUOwm56uXOeEZsTs2k+5TmiCxSeAiDeSLlJbgtylvovyHRaZw77Wjr4+/RcXefgJk2WyJKpF8LKogii/qoAlHqX3QhCl154AZ64rJMbJZGhB95yaikzI70funem9IfSlIcONBv8851wGwK9q4upaC1vuIImAvyJKFuxp/ulGwFtsGIrprJadkLNHkrkl0dgv3DNSueXpc5EmRj/5hT/inO3mJC2YHfyqCO1uqSra3tD3REYzvIDu6DnpWzGY4UxEUY2oGJjusbAcZ1VsXPkrjZSU++6FVB/SZzi0FWGJbS2+1orrQ2NR/SSpqlJaW5KBcDtEmDv2Uy1GQ+d+HSigoGRQjv0kGxSXyzUp/pSZlkI57AUv/j3uc/7GvP1kes+E8ycXual8uq7wp0hhYLel323XZVvxONNZSRx5C6l5v2Xut5F0d14rLfS4W4XdBCg4ornD00s1xNFu9yjauNS+9/aO5LqPv6hbpWpjp57XxneUpdsPTIiX+IDYOLlOK/s4B1vutjw7QaFAbzCfBqARl6E0k/VkQQBAlRf5jfonTT4gGgn7VbDsy4PfAJ+ySNtlBIcBwiG0tROv79DMVTd/V/UmKW2HzEw2BN1+pVqpEqSxDbxFsb89SHn4UI3qoMKK/mxq8aF3Eemcr9IJ9OHXWIc5dMDq013kcwbnCuasjQWnBB+hZgxjUjpxjYx26wSaE0KWT2ZRWTmT2bsWvSNI4j0G//INO7xXy9f4DC9cl4ONGrqZqMdZanMGuYg9ES/haOwXtr6jPEt+7OeisvIxn7gIzipRTJWaJ/Di9wh5fm0Xg49lqBgxPjs7j9EpaIBLhW1qh2iCZjapGW9VpFzqVh7E4Fzrvi6aKjvinvBeS5SmnkMiC+cECw2ZIyrjM/uiC5TpEPaIqWV4AuQBWHItni8DNWmqSTFKvKkarza69lvKza4+Unl17tBgRuQXR/CX+gBctFXg+qaUUpvWzVZbj8Kc0DpO8pTrNNohSilSS68FPzQsTyTuzjYqXIltv6p2rm2tEseYHeOFlRIU16P8lDiJDJzJN2OiyRISGg3UPk1xsNNHlQRVdBHnt2wyKRBRZcZ3FT30YjQwjZVgfQIM5ym09i31F7s+Y3VRFfaP6cH1RFUpBB9UhHb3PjAAdWfzgldzIL2IPh/vR/J/yxv9s8oC7HpH35PISsKAcXvKzyeqWk383o5s2thWb+5wgOQL5WY7BorgN8GnW/4D7/wH3/x3CPTeMZyAm7I/53U6zO+ms9jd+9XS6qOttXti6AX5oMmJNwYkM2WcEZ6HxIYGnE3DInkukH2OytR7JiHPFUOEEbRKsi4frZnqpCbNWIyN6mJ/9PAdEiAxao0UGntvpHym5mvlY+i0VgCHrY/qHU8Rd6IHWqkD8RiVEDR0FXbLI4G5ZpNBfrDVLtKYKIVEMuCybIKnuOrSUo4sK3MxIPM+Jth88kJoy6wphUmtreUkWCkuaFSi5d8UmljPeTlGl7CDxW9FGVyct7xXI/8zvlfphJKOhWsbKy6R5E0hNgZY2rcn0PszGmvxhiriElJZU1oM4Ze1GKCR362cJa1Qj1ZR3CV/7xUp2jVA9USRvCfyZQpKw97l3oYI/eRRd51IaV+fhZPZPRAGR/a5UEIVmIjDcsm+wH6cYwSVeJT7zc5wiXctjoccoej0hAQbBmlcPsznhgrXu+ufmVtNpdV7ikqBBFK0kDY1l4RIHkfG5BDHCNK2dPaQu8ydlx6tNdwnTK5j4pJ3LDW/qmkDeZ+iZtMs10yp8UERe3Il6SOYnOfubzrzotfPAk3F0nfWIRfzUNf7eZinSpVvxgMjVD56S5+D2IX0PFMR7Sg7wNwMUEEsAX3sbqLmPWoINqpBb6IJM9D2CkEAjhM6qmlcR3Wtmvf9WM+tSLZvovc65ZTh/z1yW0Jl0zyvrpHSh4UzYd+2bjLGDNN0kmgc0n1G+++MLbY/bEPHU3n6phXGKuyje5E6CYRIQAo9RZB1ALXdJLiKY1TlnPdHNJkzG91kOuKqVhKJZ7Wsh9K7IS89x9Jj4SdR3d1kSV6HVDZpj6vb7zBDWS6SMiR5bn+0dXyPOrvCq8753NHARyB7eJZLdkQHYED9M4nlUORBjfDoM3k0/D5pTiTPiWofaf6M6GqyG1c2vgPoVKtvC51rXlKuEjnPS4qqN1sGePtaZBohuRxt9uo3Zc8HWI1zkqbv4XE/EupQgEl7gENUfb7PfO29uQrBh7WsTlbVcslmc4HHnM1Etxx6AQhZV/p3pRr4y26gs/fB3UDrk5P7mVmipeylT/C6aqdK8aFE1pBCwdYcN7wwNUGoA0ya5PuTfqeUgtSWrEDCh1TN2/H8uFhu7apbufG5sZcDT0qCi9MFGjRcEf7yEUWwcnlTu9w2sreeWkXHvkJt0QliRv5dCW+WwUsIpiOpNsGuhnTYomhzBz85fIdLGzks8iAjtR4deKU0gXXsE3lWkTKreQDJmIOrX0lpFoYOAdO26LY8rBzCzIkqKsWJnFTqX7ucaszAuclg2ZUuq2hWDDDajwx3snZyX3/72f/9jsV4i7ivTJLz4LE+LWaWPnW3BLCPGQOA+Qq6ODiyTmQYJEG5bt7tY77Tkf/3Xe3DLSIuLik/W7XqXdcpJn/pW/af/+T48OuFoVLTP+g+DWRrLAyAMu02w8m//B0v8B+PSkZl0c+hmRR77PuWskZ/SG73htrhTZQNKfrJTsmnKUtedM6aDVU1aJWmT1zbBO0AaCirw/N+MB8MG2j278ViWU7VsJfEc3XGKyD4qR+gzTVR03L2LMM77TtNbRW4YzN5fx+kVuDpsg96smLpzLEnleOG/CwBXvX8k/OZpMJ8DC/GOOavb3DXl+6BL1pHGK2vQR4Wy57G7YBpgGzAFlNr7+6HIbfAFDGmzC8FNagdRn7Jlgg8SS4CsRU35rH0tttA+y3I3L9rUx2iGQ30GwU+q1OXO8jhtKT5dkaIkm6mPFWmU/SnDbbpmJM/hAnBSilUifOyuWvEVrSMKlR+hIza4r47YjMYp6dQRo7C5Ub4HRe5DskB5VbonuV/+LpacgD2cTqLzSsX09dSg3hZlXwekAsj9x30ynvE4QjF8wWDH4zrjRqvBeVyHbuohMVJNTcQQXigxNqBvaiXKW5dUyIM+XxAjMm3lpNY5LMyBEvOFr5E5UgHfSrBbJFXbn8BcNcrTVbUQhCMrOxTUozjC+t3eBJMFuMIhLoKSdFv3bRM8MCISHGSDWnuYq+6Las4acOR1OTJ6HXHtnbo7NDB+QORFOzQr+jVZaal7rbe4FlYcHRZG7ByTyOlQekxXiJhf6Irzw6b/J7I8D9dBVbdii8geIOqeDMsL7oj49aWbaxBmTyMenBZxfAU/r7AWR+Cu80qDw+i52eU0dlOvB86xNRHqKZO9F0ujmGiXUC51rzUwdpto69/+5X8v2Td9FwJA0pmON+/Up/Wa0r6EG3wndjiRybTrNMhzHPF1arflBLDoMCxtWnsD8G48w+OGx9GApcBJpr1/N0WVujtTYM8Hzrf9EIexwfvi5zRA1JUq9oif5V/i+Rz8HVdOkX2UuEFqe+gymF8u4C2PPTtEcfQcQOonyJsLpJc4AY3Ms3ReTI3ERCsnGhhJPw9CbKIzigaAYwlsFeqJXr5x5vJvw5Oj4pRo4qH5uYlO7tcIDYzFKquYlgFIHL2KwTs+4AD7DLHPl9j9gO0T6fBNZJoFLIDy2WWHb3QdhWN9Ou9NKCM0DJZGEL2PrxbuFLULId8Pjr5A5FRRI1QwES1IG8JHvG7rCtA3/JUO9HN+T+pm0yap1ZX4tXAlvpVb1ngrT6wEeQqfm5PKrS0EuNOoB/PJ8GhbX9N3Ob3jjpeve5PDoeJRrtUVW8DcdW+iDu/R5WPF5TW/pSUVDwr91Ym9tviBsPVezyb/bSRVq7Zr7M7XU+bjOWF6VRunpDjD0+5+5iz7QBhs1N3tIuJbRjxZ9slGIK/O4F0MQCeUoJgK6XD2MC3iDtA6TijFw7iKxX1dwCrhFihsTuEf1bkRbfQn0Clj6FEek8JDMyvScDEw6BRkZ/ICKYLsGzQG4XakcUlipxhzLvNLGsEmIj486Afynf/v7IMs09Z/+9u/15HnxCycPfiYB51Rv9TGxKmsX0JAr7i8AP/y4wJoh/ZARSsMPrQ3Bfayi/EF8aKdK6GR4H4jpAHpxlaLkNpKAZQ4HWQKPzQP197HmMdLGhuNqArlBUft3u0tX6vbWyBIVM/cDRhO2IGdVzAQVvbbrDgNu7EAh4Eod6c8jK5EmD1cJ6XGfKAnMcSuTy9Eyxx7QmBTCIQWJz+lceLOCZfcMFFgxGaJ4uiPeHUaX0esHA/KQjgHu7tGV02gEKWhV9RENOABg8Qy3N7usLhDQQamumDhZfKoPiIwThBifSTk1cIUTFyf9SYCEpoxCCHy4Ri0mV+c/chNxMAo8YO7CDzabDU6d2DQMx01/Z5+mOtoTYI22vrAgvcbkSGQ36BJ9028tHVLs7QB/KfLGyHhaLhNSbTSniBXQfl7rKcxPER6TyzqRLY1/EBTtOTmlzpae7b+al8bai+/1g61l/vaEcQ1SOMrYN3SAJckqgFPfc0moSOIXWxTepegD7EQgUL08biIhRNLYXrIChsxiWTrcy/H/ogHj15XuIOiTRW1gGs8QUKSIH0x8kzklVLop5hFWISQzoSEI1cKkOjdqsUmZAVRgUd8nKwqDanot4ZUNHyZrJOwnhP3RSixtliKu7tGvbI8TVGdh2vWpNhZ2YA61SbRs7aYmBJQGPA3nVHgG4WoXk1qd7eWykasNhOzXSzkKZBA2+JG5MG6qwK2XJpGQSvalpVeLBDKj8azkgAilm8WBTBiZQGlZ/yPCURgI/LkF54dITjmgbt4BvzGFx44KwbSQBdP/ALFyhM/MEeNiFMxODBBkwgF5ywE75qY0/qlc5UBo4R+ehB5JXbhfrJDRAOJzUuHBDYtTJRU0aoJZY8mS7RS2Qu6iSZn6ERJhFN1hnMTTd6gGyVnZ2CiyXP0TC1Omj6lSa/g2QRT8MZM6JuNx5NzNKm+z010VSscDIxTE01Oz030spbFTvVbEfm0OoWv4BSecmzwvZMNjFf0QXVljr6XHjZvyTF+xSPPfC+A/m2JJqfo6txEGNdeSm+d4/Uz4y3swrmJXtdHFfjGsiWy3OmID3dJ7H+vcXriZhCAlxfo0zgdxivayKsKfagV+kE0WxQezoy3/M7ZsUbgi4y1VOOp0IbM7kYUD/Q7A4jgC5NfnpsoxbXpfTMV+/Gr89epkWITLbDzHJyUgkar/ZorBM5xTgMrs9i7Ng3wy7A7wlkehG6OSRx0yBoeIaBGspkb2QMLqkNQbgi2+9Y5fj15ez4WUYneliZ61rYtJ2Tx+K4zaDVeiTX+vn/pwozH3/PIqG9N+3sIyma8NdH3JdvTP+NtQA28xL1yrNGrp3zOo1d7zqCCuteTV+ct0PY9h7ZXCrS9PjfR27ZJMZB2/kwDjb81R6+kNo0F7vNgxoSgMl6htbsI5iAQE+KSE+N757iavc2X5nuTTR7In8mfMVrgcxNdY4fM+oQfeOM1sUF76xy/5bcDVHhYL2eWKModf9wnIhVyvu/kT6Z44YKEgD6lanG5ePC37f3Gtsdio0/pjghs9ScNDRLV1iWNEffxMaHajQrvF72ISqvk9yTRCNIr97Eiuvtbcl+87eduOsc5U4dVosBswRPcHPJD8q/ux7Mis5WmgL8TR3OWxfRReoR4k9235C0x1mLmKE/MSmbXM52nnEMlX4papbcUDl94UmdVmg5mnG8pB/4tc1rGrubxhfbb3/699nBdSwdLR2Yeh0RDpqk6/iUaPCJsFHETj5KFO8OX8cLDKSiFuenskskCMs3IiinF1iZI4mDgS3UPVxv38Lp3ZGmfciPlwcrRZ+6zvZvc4fBwWO1HZ//eR2dTGDG58OtKV/2RpqPT6hN/IAaPKRYGpbu7v9Zin3XIHG6MHcss2Rv14uFatFHSGH9SyvFgrGfkraT99i//izaDRoADEERZgmf5BboTrYiwUHwzrf5hFRCOZNFgcBd1jsDD9a+leYFmG3PrIeUuZL6rRvmuWmtdbWB9gTSZ8XpRdmM4YZZYxdK9vf1VjpA71n/7X/83YNv8T/9ls/i5scvV+lc+b7uERtf4TlwoLXQzxlXbWWgEqpTVNkBCoMELY4Mk6+Hfd1CzeLFwkwzrgiUYfqowlwv6/tCugxSTEEWmPf/YtrdwPD2vLHlVv4Ep9u0EfUzwz9efJ/gnx/yEwajll1hjVDEL1afXZ8BOOOPfLAgh+Eucuwugng0TJTHl0FTUlC4dP1r+1yDNC3cBRHVmmPzmFNTl68nbfgD0I4nNvfNKiYjNKdqXhALlzyUT/eTI5O1zx3G+l97ZrWIiHR4jPdKTbrMuEezQAvdD7GZFitmDgeO1anI8fLCO8jixLQRyENtCgsNlfaEjwaOyL8hPUIR6Y8BNnuVumpfJUkFNdGz5gDOqX1VyvDNCLleTFVI9gvef4SqrrNjVbzGV832vyPl+UuR8mNiavwXOpMlsTN8w6PqeB27N7BSLMKr8ohnUelKDq751jv+MGYnfJhUtBgZ7m8hMidIJAuCAOqvE0OGk6pyFUEgshJixEDiXgcBIZLZtcO1ktEfgjGKNO6cKPNBNIQycbdr7CPzDeqIoSGH9fT5k5FfIiKxV0XKePKcgykl4PGYyiYI9nMDtpWcsneMlJzLhciTMIpMVnsuFCd9ALr4jigvjaScc92F/fybIh9gX06bufKH97oeZeJNt+QA7gMtt/3eE5N3/OIHoQVe00qIvToy4uSsdFjXXG/cJMbghtuNmGB7UddE92qCsfr5BmLiZum55eKYDpCCEDRM8DVJFKYcw80vl/ZN1xYn9BNS+N2aKg96zfHy3j9C26JsTnbWg6egPsQESGea736vaNiES+r/9Z514UPiQdJYSu1H0QfHzNEi3G5WwOaCvBlEbNZys8JzzbaPxCn3BDtlhPVCwsrVNeD8NUgrSCzfLqYe78eZt7h7AoHUAntQ0Ut0yhgmLaiaXaa7EMkFrphr0izu16SUG8sXMnqwDjycwuH1NPiivMikRKcAkGKLMCf+mxYjgh5akGFcUfMY+abk5K0RwuihzRr+kpjaelW2RLVESIzNjpMOPSe0YM+88TiWi4UW/3VCUzZCVnE67S9JpsoJzKAgJNrvTqm1SiJB0YKgNdmk3Bt4CSJFJjPymTKFGWXZE6q7pBuogJiVxOxCXesxAe+ADdhdndEhGZPbzSxwZvAodRQnyNYgY4tFUnKakEQ+e7jlYHcS+RlwWjT1+73N3855pooyKdeMirzXMB1Ki4aFlEf1oSQZ759uv/YgxNUYY5Jb6Afd9PgqtR9HR7m48juH1AYupVX5HgYekBZH24xk8bbAWupE7V5yHkCb4zoz5LxKsWJrGb//pXzXfDcAKCnQ5/lWDcqx9XajnxQOHauECwIMsgfnuzYqprOgPbw4QVYJQgvlikPSTZ61kMnK50I4RzEWTYI4lgtlnBLOnEMy3tzuBQgh+AmK0i+olA65oXu/z07xejeaNW2je0IkVGnfuHM8lGjcw/7ulST9CJPBmATrs9sUjBojZIwiWPc7dqUPXUGEZbmDy/PYv/0UD5h5wGxmC3yp8udyG5PG4Y2DbjoZVuv8QfvuX/2urSs0YtdsosRPwIZbNn/09EQCbrcZf3hQ+tzJmGExCIrE477TZCblzuFCVXdTZ2ffW1lSoaNq0IJI3gcUjCnGPHq7DujQFtXn238CtrDdgbvEe2lIHVZ5dWH8oI53x7HTUyJMdJ9LMn5gP8V+pC3G+8BdAzH6AVWgpRCRLhPtKcr9dBlmeEVRo320I2TT0Bi4juZukCM0bfCi2NEA2TYviXPNBIFB75f0OoruLDXEPWvyTENBFRUCHQtNxR1l+OtXnoWzbxBZW8HD9OKVmH1wua7eS4w3dH/F0VOjze46Ee1LsGsS32w2iTqQ/D+Xw6g29wvYlZ0vdagrEPu9HyShR132gypRh/NmSFoNBw59XTdz5jPjKBQbxIx1hpovCOBwWWkva3lJJgcBssXz1ysXgHpU5DrQZqVNvbMYaoyrRjD04IOom10tjLSxfYC0ye/3XAqcBzmx4ci0wvC3swSMLzWf055FlfTXA+yjFwNLeGQB/HcwsfoxeB5EXX39H5LWg6VOa6OfE8eIZiSXch3ZXZ0SfIE4N/UEax7lOibmfEzO/TONrDcZE3josX8IR5ihbMjPXn+M4N35OQLcw8nBqMHjsw6tolr+KPVzfPeNmCUAVwKJ4g4aLlMZuGz5bM/ifOXr6iEa8O/4nTdO0p0R2oKV4Ae/U1QJnlxjnujZL4yyj3mePH321o4HJz3UQebMs0z4c9Pf7A+1We/XiF+1lMMNRhrVb7TLPk8x+9EgqCi4XtK8e/fPCXQFUp3GC0zzA2fqfsyJJ4jTPNMMwetd4ehXkvctVcomjzAa5uGlqIAI0YM2M0E3nAWjVBqEdRICATdPU4lQzjF4Y3/TilCwGy5IqUiluOp8afhqHWoo9LdXm2tQ0TXP9FbKn1F+o7RIXobYNpJ+Xxsm618uve0xtd2kzbDCSU1etqTdqanaFr+v1SVqtNvdpDc/CNMO2RZO9AOIqNdPFoxPEPsStL01fYPqmUtrm9mW1Hi9dL762Qaf5gWVZlpxK3y5t5XvuIrl0bRAg0eQgynDe0Zic19akkl9vGFgCbZVI+qb+uipKue3VSRYp1dJj7PtQnQrQrGTZzKJdPvB9v5nX3iFXA2ls43RR1AY/JaYsEc4yNR2utdTNaiOep+4qg7ApavJlgRmY1lfmA05rTcSJOwvyOpS6OQSNqlXPcBK4ahIcIT7proy2PZKz6wAh54HrW16zLEuGXnIIWL22AdUi+zLO8nWvRy7RzI0yuwjI316G08BHGg1F0isCpMnJ+rMkWWCNaHVo34bxXwJwJ3mG5zHW/vSiJeVsFU6BXNL0H+I8ViqOWPdwh9v6H3D+TeoGUaa9iqOYtPEd//kKR4sYaSdxlMULN0MaVCE4YcQZCdOFO7uyH1DggSw40f3hYYpDUibK3SDCaS/07OERTWR3YU+yS/BY0C+7PzjM2ovkQQiQy+kJe1ZMg1lvim8CnBr9A6RZSOsPkTYwpfpknr4bBouVTdkfYuHlYjCtrrKQZ/IlKwJb/x4HWa4jrQdec3CPbhjSvgGrmFfu7Ix8fxdHubQdurybYu2Eapf9wJpa3mDYyBnYDwbDweFg1sgZ2g8G7gAPv652giBf+8HAHx7sW7Vkxgi0Hwy9/f2DqiN4ntkP8AH+Gvty4sB+4B65/sySE4f2g8ezrw+OjuTEffvBweHB7LACiTjquTMgGBvTAsslmbFnPxgOvX2M6wX8YG4/cL8+mvqukiW8TNgPnhy4+9MjJZfzB+0HR9PDmf9YyeTOKO0H/vTJcH82ao0pbz/w3dlscKjkglMu+8Hj/ceP/cGoYYRlP/APn2BrKnLAJ679YP9o6vnV+ESggMaUoZFGG5XzI/uB/+Tr/UE1GSCg7QeDg+mRWyVyD4P2g8eWeyitmhRK1X5w4Hr4yBq1+MeFVZn6w4NRgz9oP/CPvh58PRDYbOpmGEgURppwSqUiUabxEhAhtCoYcssRg05Lo/cJpZxsa5S4HiENrNK2gcHcyxjx2qNCgI9u7jIPFxzdMjqOvFEAR/dcDx42FIvn7pQi7oMRufaYjs6gfzhqooQWxIL+/hjcZCPDcPHBkuV5EM2z1jHWyqAoTkN3wVog/AOCXje10SwlWhFL6yY9oS/DrlBJmbC8TLnukzXiF+wlToN8xK1U44RRMYNkWbrTaWpfX+IUGxMiqDg31V304PFOcUgBzxNidunFeY690V0FyssBuhyiy310eYAuD9Hl4zW9FejtTYdFUq7pkFla6a7VoXeNiOd3pJdTRLHxWu5kSkQh5QweVldTD2VumIDD/HU3ENavLYBEcUMj7ew7uMZ7P+M5sMfEbf4qjtyZcqvrL4MppmMURMBJXKQBTrUf8LUu3fvbQZ40sN8HflVDG2Cw2rwBDsssdBcLaUOPrC/KrJiirEjWQE8GM3fRo+YVgMoALqQWvj78QsEC1kgor3HFPGhtPY3zPA7tXn94CH0WyRoU2np9+Mrd6QKvyeYHVEZlcShvBX2uwGnzH6VNXo9EKRvI9TWjyG1QCymTNJ6nOMu6ZlOy4AdrFj3dXgRZ3gOuThkvULFAIY6KNUmkFD48a8sgnKPswxzBwy5GMzf64GbILbwgRoGfgpNjHE6xh6g/qHrfYeB5CzziPU4X8eyKNEmaW4fukh9uwLhsbclsKJZHROUeUdyP4iQHJeYEwRq6KXbJdqpHswGBSm4LYPH8Bc5zoIcYqcqT1Z3hz4zBqNKo5i8p8lBiW5e6XlBk3ZfXP/64KZKl47aDzJiExSIPkgU+RxM4EefA0GD70Yav7tWARs071uyW7lHGSI/oddrg4ahrHRmjhZXHkWcfkMKSocNazL2UmDiE2yIYOWDg6qYg7rcZ9Z64K9YH59ywJ0sviED8nwUzhlqSpblWO6TrKhs7yz2zXFn/HrTvU+zB/82Oxprq+qx9kqMdWqpzlLIsxRlJMRkoP5mlbfNpU3MV6SZatywIwQJVHVCR6rmRB+8u3CNaZGvCk2fU0eKSXm/0/PObTa1PqmIvyAUqYhsInOausj0aEpPI8RKcrmUCsb1xDk0E63SX662wm9LWt64SxlF+ec86nru6Z43LuLj3wIKoyPE9K2V4FkfevXtaLIKPq4nTwAtweGc1YAlFnpv2kmB2hVO4NYOZm8fpWiHI2bVYBD3mSYG+DSg7h4CvfJkI8hFsxGnGOaJfKc5wzj+yYhoG+bm5ls4CLd6FjNoK8qkEUUSQdBDx0vJlV5WLi3xDuQl1wXDO5mBHcW6wNKeI8mDRI+z6c9MUJwvmvxOEgHzcKC+l+4fTjspFo9C66h1CnXfYzHmHdJEAn5A/vKA76bFVp4zrlC/ZHf6ChMdtHIE0ZcRSijxYBIT/3mfd9oifgYyYoq3VNNpYnxNL6w+gPQ/1VxUB1f9AVerlTJZU9rkRQmWXwFPKvh8ssVdlkM+yz0m/dZMY7Gd5MLtaVTn0u+xTvq61ppxbiycseRK7zKqclcjhJ6QPjyKLUJbsY0A+KI3MKAGT5gxJzsxdzAw1W/tKG5ol9TnUs9bUCbnFEwYsod4kzR2y3O5mKR3cszhBbImkd5PeIFm+OxekMrzr+mDw0bPW1O6j7N/0Btb6hhqT2AOSMKwShiThoEo4sMi+wyCi3nDNY1UUYWRDkjZ8RP+U/XDZG6hUQ2OK4aoqQle8pcS+WqJ9IfahbN6z3vUPefHuzegf0tIDuWijZ1j7O5sa0pL7d5dkI3x8d8nHbHTW3UUHFpRNq5HeBSzgdlCsJ4WKxsSnVXOsyIb2FlV7BKIarS2q1kiBjW3tb1F0nxZ9B77c3p0rFQ4BvpnuvpJBUDqxNOrNFm6Y9PYF/VUl2vuCnQC3GpMccmJOvKykMiNu9WbTm6HsExhdq4+wPtBaIo0QXn04NyIJPso+bUK5TQAvwfhqxBtP7qm9yYll/7J3yC+09nWEQ3DZe7K50BNSaGBtLkXA8LI3OLij2AEp9g6C17w752WHsGmQui+n7vPUr+XUr3nqkZx6RFPB+R9PgkcupCVLkUJgw132oPoTHBLQWXLK5gkOq9zH1odLNRtSqvyjRv6Rkj+Ed5RagCSVfaZSI5PxFk99R9wQQrUqc0imdt3bZx6ouk/Ede9wY5lDUubI2ljoyCKl+AbRokM6BL4TNPFrnngkJR7xxEO5/qFo4LFc+LEoTYIciXTyRTP2h3LG/rDKeKxkPGYZBAAqJgfdEJas8j94VuhJGVw5t5KXmXRvrtmOMREvPdC9wRr+2IOyT9VgyWeP/q5Se5aSzivP0/h6LX5BceEIbM0EsU8sD8/LvrDFXFdWmXSkkhoCMrV62qol7UZOowoIjZQVMsu+6j1urdKjFHvRCFUDSgHkOEzASJSRAmB4mWA3NwYoDKLQXRoWGvipacpVh5urDjdUfVcFxnp33tEMKaEN/JSt+Cxe0AX3ghQz+SUpyvLhsbsWv2z4p+xTRSzO1qMuYQgfL6t4fX1ZW0spQpN4AcJiUfIpYBCL2r7iylUpxdLKPlcEm+L8GuNoLRTDaAxqm2qKsNyqOBtYvTQfnORpulGGDBBHXtknjhPWc7dJJ5EcILsgswPvE4KL6G9vKDVkZfY3lNlnZR5vKPPY5ByxvogHf0zebzZYQPWIBpRprju0a2Rik7HF2vupjo3ahmmqbQCrrL0FkjrQehuaas5l+Anm0rUBHzOrDW1tNb8+C2y3pqexd/fmr8QFlcbXpDzcMHyduGJU5zK1aE4JqSZ5NlGWvBizrFFlyrKybctJN+4gWcprXB+KadaG0qjaXNJmG2Z9LWibbQuiyERk4xSaAQ9bZopIhSmC3sWLRZBkQTYiQc3ovtpRDFizSRQL3xCELhfZlCgXmYz+7a69Uuv3VqwFwhnB3lplrFMFGJFLqQC1yH7/wBoeDfE+IUVo3lpRoNu8uZWwtM+3e/3R8CE3I0ZBeQDbNERLNpuaru8P3ArwyY2JBSQ8hG2aIgVbGhLk/u9ocF9q8B2YVr0zmkos78x35+8eHVgqqDPNlseP7yUu+MjOWiQKdV/CtLqpHTSECxv6FKo4HXOk+jmfbo539LfFNEUL95wpVyuCjrtxllzS3NQcURrqWDSqBfTpFm1DX1ssGPUxvf1iSfpEm9dKDsxd4QoFA264FDjyqoZUW0UQdJb96bz3rgo78E7EHXiHupbp/eDo3Rfv5LnSaTQkqXSbhvie2/TZBrT9XtaDK5Rbr5Oyue8H1pYrRbTKBu5nWamPGtIdayWDJnGJ1rZabQhY6DS2d9uCc0UNs7VZoQG4RYOibGtTjXO5qa3a2ZxTBd93j4bWulWlYP/eW7uhwTs2h9Q0tWHrtrBmH3eM88nHjvPxR4/zcfs4GZ7btAkVrpP8P96xb7yYWmuwXa2BWmu4XS14NFN9IM64WrNPP8htllT24eXNRYXdr294e28utU9KHdxR6oCUenxHKRBzJMBGU1VRGgwGKAP8hVqxTawG8O+wXfkhLb2/Xel9Wvpgu9IHtPTRdqWPoPSKSrBUOf0GIRa4Iq2VbizfSlqLTQ0OaeH9rQrv08IHWxU+oIW5xEatQmQ2EPlEpLdJ4yDqmihwl5ArSUFisU1pIriA4o+3K05gdiov6F0CsmQhregdUq0EnkUDxvRW6gwo75u8wRk/T1L94aw8EddIzoRvlkWmJueRhLIvjCzWmwwwWCPvJgOAUTJGSe3T6h+KEb4jjpJq+XLuoJ47kHOHLc0P1eaHjXw5d7+euy/nHtRzD6TcYb3l4ZDKzIn1HFR/1z98d75WbOpAT72mt16v0lanUemQcPtpxLd3E+tdf0DEVKqpHiTWNREhrV532FZ32FJ32KwLseaalSG1URsSG8Nu7XvQ1vmA9P5uIpg6wEF6Z7vRirCw3p2vlRyRUfanKXavetdgP14rU+XwUqCODN898mmLxLJP2FeUK5mkmAoBZJYWTxQAsuHlv+6kM+Ft392CMH3Z0Igos6Ed5ZV85/O4qxXxYO1ogj5jOuqrFPMdZHJbC8LopaOBymlOd32wwtlQHbI31GamOhsaYCU2tCGT+nfR98JFR0tJSJaKDNqLDKQiw/YiQ6nIfnsRuIBE5El6SVQCR5FR9t0ITCsDN8Oe0KGg6DKM4xxCfdlSkRE1ws6W9TLCELXsM63h3qElFIgBgTKDzuFysVbNkoeHyVI7BOleD/C+JF2QrEcR4UFY1oEFrEOhoyjKyhbGJqolS6bAUl7TbLee2UhlCWIy7ya9o2T53nq/P0yW79P51H1nWO8Q/68/PCQwI88XKmiWtv//l5kyQ4m1asIsyinJ5oh/Sixi6ol1Tf9I3OBFkcrC6sruWU7lVs9ymgA1ObGyeJZTqb2zIhNnhs1KGpg1ywmSATIymciemsySHcrWUgrzdLCiL1hUf+UhmZGG+PLQr5pKJ0uFYF4swhfi8/VAdakHjg1qSR8Ct5aSx6MNJr5iitjNMFItg7qrmeaozbK4Wi+W0t0gL2Gqy1lThqitqMhFwms1ohtPd/offaKSMcK7KmhEm5FCE3f/c4i9wDWo2yfyr7nuk7/v7G5esbhqaZXN/GJ+76qtqsbNdzSjFJaakrglvIlteSbNJob3aWJYNXEnPcLa3Wp1xLXeWYld7rXyg03lB8DAIjZpHctP8rZfftaUHHSENdHihYLomzeTyr4XZGBu572zmdpQFMMDcxFfY8/mmVyHSMqTa3Ia4MCqaghi4KAduqvatTXcF21sWsx9s+TtVspdB+AgwVz3Q++d/fGqSmVJXE1MiJt+4nvCIVa5jBrsZTNIs0lamysC3/Wn/qzNFQHG/kAyq5dcEeADfORbjZx9+4F34D3BBw0nBZ6HB9jtclIw9WePvYOakwLLHwwHT2pOCvbdg8HhQd1JweDx0dd1JwVPrCdfu0dtTgpqE246KbCOngymw1YnBV/P9l0gNNudFBx8fXj4+EmHk4LH3vDIe9LhpGA2PBhYnU4K3MHjofV1i5OCg/39o5nb5qRgenC4bz2pOSmw9h8/cQdtTgpqU6ZOCmptyE4KauOlTgos74nsLKJyUjA8fLyPp+1OCgaHR9a+1+6koDYAyUnB9MlgNpg1Id9z06sa4EMS9Q4wjb0VIi6wZM1d4UyghPwurlQRmKMtGfKjDvQ7UplENYbMwSGdDTX+aRxmUyOjk4YQhOC0uEgXBonQYZPvR9mH+d4yXIyK3D9CT7MPc20ZLqLM+RK8YtmPHl1fX/ev9/txOn80tCwLyn+pEXTkfDl4bH2p0QHRj+OnlDDWAs/5MoJP/EuRTosFjmZYI+ZUX/qpO8vdxQ9xkOEvibeI71L81wJHs5XzpdV/8qUWFeGPs9z9gDPny+GXWpYH+ezyl2AB3/Tjy0fQNDGpfOXmaQCPj0UB+eCqiPyv/0TTpI+h8nUkvvrW/qFmQXuP6NCPn4ICppihZX0xPJQmyb5pWedLWM0vhvuRSVrIPsyPdWXbKQ5mqLjJy6zb+uqLYO7qmoUgSmlOfmSZNdA1SzL/jQCFzElNaus0uoMxSt1frpUWogKs72Y2y4TvrPxqTeMMTl2uPgTP4VGVeNelLceHku1LeQNC/zlZjsSJSpZtRSlvbq1al7UXvCzCqVywe3zKiVwEiV1xyoXLEHjKEqchWpcld8cQGoRd807/iP5L26b2hWCW25RcUmcxB949LYs3NLmdQlAlnNb2G2JMuf0u0pKScj1uh8c9GAzE7Dd2Kkg9ygegb/O0WGBJxa2jLSE7pdd7D8BSUYIfUf3mDC98rt48knS6xDWraqSQ3jw3u8TNoVf80Ro0DdsaJHfg5scO4xI2arJLcnPlikPYqM8u0m01cxr1yWW73TstCbgXji6XWg1mPrDou1h/baqejbUeNfXfR6Che5Asha0qbOPjZDmqS48Uhy+HllW/jUdtJuz/zF//mmIQsc5WUe4ubf0rfcQsYjPbdxcZ7qixuneNm+1rUFOLe5a/x4hqatXdFUfMMx71JmBb9YbqysQf35KsvLl1KwSV1FtiErPtV4OLnO6xfoR1t/1kubPG9nboUbxv79Sroaj1FE4djnJ3jo/vGA4xaKq1KTN9f/+8mu46t59d05Xnp5pj5c7zHme3Yl5/qmX5mGE0mO+/fzCVj9J7robsv7TaGhZt9b7HvuH2dOuJgXvUTa19qnVSOFi/DzOBSGL7xa4EFtvX4eKM7WsIYcf2VSpRyH0AGAQl25dnvLx7IEUmd7lHDZDKbF9cktl8VKX7HvmGA9tPhQbr3m83DOnpIwL14N776aNL7HrkF3AxNOKJ3tEl0a3OvIB7wQdgNujEj/nx00de8IE2APWO/+npI2DkHP/T/wuUMexhmhwGAA=="; +export const DASHBOARD_WEB_GZIP_B64 = "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"; diff --git a/apps/pythinker-code/src/tui/commands/auth.ts b/apps/pythinker-code/src/tui/commands/auth.ts index 44627f09..3baa8440 100644 --- a/apps/pythinker-code/src/tui/commands/auth.ts +++ b/apps/pythinker-code/src/tui/commands/auth.ts @@ -1,35 +1,12 @@ import { - applyOpenAICodexOAuthConfig, - applyOpenPlatformConfig, - fetchOpenAICodexModels, - fetchOpenPlatformModels, - filterModelsByPrefix, - getOpenPlatformById, - OPENAI_CODEX_OAUTH_PLATFORM_ID, - OPENAI_CODEX_PROVIDER_ID, - OpenAICodexApiError, - OpenPlatformApiError, - runOpenAICodexOAuthFlow, - type ManagedKimiCodeModelInfo, - type ManagedKimiConfigShape, - type OpenPlatformDefinition, -} from '@pythoughts/pythinker-code-oauth'; -import { - applyCatalogProvider, - catalogBaseUrl, - catalogConnectionWire, - catalogProviderModels, - DEFAULT_CATALOG_URL, - fetchCatalog, - log, + connectCatalogProvider as connectCatalogProviderFlow, + runLogin, type CatalogProviderEntry, + type LoginUi, } from '@pythoughts/pythinker-code-sdk'; import type { ChoiceOption } from '../components/dialogs/choice-picker'; -import { catalogProviderIdFromPlatformValue } from '../components/dialogs/platform-selector'; import { DEFAULT_OAUTH_PROVIDER_NAME, PRODUCT_NAME } from '../constant/pythinker-tui'; -import { formatErrorMessage } from '../utils/event-payload'; -import type { LoginProgressSpinnerHandle } from '../types'; import { promptApiKey, promptLogoutProviderSelection, @@ -37,180 +14,53 @@ import { promptModelSelectionForOpenPlatform, promptPlatformSelection, } from './prompts'; +import { openUrl } from '#/utils/open-url'; + import type { SlashCommandHost } from './dispatch'; // --------------------------------------------------------------------------- // Auth: login / logout // --------------------------------------------------------------------------- -export async function handleLoginCommand(host: SlashCommandHost): Promise { - const selection = await promptPlatformSelection(host); - if (selection === undefined) return; - const { platformId, catalog } = selection; - - const catalogProviderId = catalogProviderIdFromPlatformValue(platformId); - if (catalogProviderId !== undefined) { - await connectCatalogProvider(host, catalogProviderId, catalog[catalogProviderId]); - return; - } - - if (platformId === 'kimi-code') { - await handlePythinkerCodeOAuthLogin(host); - return; - } - - if (platformId === OPENAI_CODEX_OAUTH_PLATFORM_ID) { - await handleOpenAICodexOAuthLogin(host); - return; - } - - const platform = getOpenPlatformById(platformId); - if (platform === undefined) return; - - if (platform.catalogProviderId !== undefined) { - await connectCatalogProvider( - host, - platform.catalogProviderId, - catalog[platform.catalogProviderId], - platform.name, - ); - return; - } - - await handleOpenPlatformLogin(host, platform); -} - -async function handlePythinkerCodeOAuthLogin(host: SlashCommandHost): Promise { - const status = await host.harness.auth.status(DEFAULT_OAUTH_PROVIDER_NAME); - const alreadyLoggedIn = status.providers.some( - (provider) => provider.providerName === DEFAULT_OAUTH_PROVIDER_NAME && provider.hasToken, - ); - - let spinner: LoginProgressSpinnerHandle | undefined; - const controller = new AbortController(); - const cancelLogin = (): void => { - controller.abort(); +function loginUiFromHost(host: SlashCommandHost): LoginUi { + return { + harness: host.harness, + sessionId: host.session?.id, + get cancelInFlight() { + return host.cancelInFlight; + }, + set cancelInFlight(cancelInFlight: (() => void) | undefined) { + host.cancelInFlight = cancelInFlight; + }, + openBrowser: (url) => { + openUrl(url); + }, + showStatus: (message) => { + host.showStatus(message); + }, + showError: (message) => { + host.showError(message); + }, + showLoginProgressSpinner: (label) => host.showLoginProgressSpinner(label), + showLoginAuthorizationPrompt: (auth) => host.showLoginAuthorizationPrompt(auth), + promptPlatformSelection: () => promptPlatformSelection(host), + promptApiKey: (platformName, subtitleLines, options) => + options === undefined + ? promptApiKey(host, platformName, subtitleLines) + : promptApiKey(host, platformName, subtitleLines, options), + promptModelSelectionForOpenPlatform: (models, platform) => + promptModelSelectionForOpenPlatform(host, models, platform), + promptModelSelectionForCatalog: (providerId, models) => + promptModelSelectionForCatalog(host, providerId, models), + refreshConfigAfterLogin: () => host.authFlow.refreshConfigAfterLogin(), + track: (event, props) => { + host.track(event, props); + }, }; - host.cancelInFlight = cancelLogin; - try { - await host.harness.auth.login(DEFAULT_OAUTH_PROVIDER_NAME, { - signal: controller.signal, - onDeviceCode: (data) => { - spinner = host.showLoginAuthorizationPrompt(data); - }, - }); - spinner?.stop({ ok: true, label: 'Logged in.' }); - spinner = undefined; - try { - await host.authFlow.refreshConfigAfterLogin(); - } catch (refreshError) { - const message = formatErrorMessage(refreshError); - host.showError(`Authentication successful, but failed to refresh config: ${message}`); - return; - } - host.track('login', { - provider: DEFAULT_OAUTH_PROVIDER_NAME, - already_logged_in: alreadyLoggedIn, - }); - if (alreadyLoggedIn) { - host.showStatus('Already logged in. Model configuration refreshed.'); - } - } catch (error) { - const cancelled = controller.signal.aborted; - spinner?.stop({ - ok: false, - label: cancelled ? 'Login cancelled.' : 'Login failed.', - }); - spinner = undefined; - if (cancelled) return; - log.warn('login failed', { - providerName: DEFAULT_OAUTH_PROVIDER_NAME, - alreadyLoggedIn, - sessionId: host.session?.id, - error, - }); - const message = formatErrorMessage(error); - host.showError(`Login failed: ${message}`); - } finally { - if (host.cancelInFlight === cancelLogin) { - host.cancelInFlight = undefined; - } - } } -async function handleOpenPlatformLogin( - host: SlashCommandHost, - platform: OpenPlatformDefinition, -): Promise { - const platformName = platform.name; - const subtitleLines = [ - `${'base_url'.padEnd(12)}${platform.baseUrl}`, - `${'saved to'.padEnd(12)}~/.pythinker-code/config.toml`, - ]; - const apiKey = await promptApiKey(host, platformName, subtitleLines); - if (apiKey === undefined) return; - - const controller = new AbortController(); - const cancelLogin = (): void => { - controller.abort(); - }; - host.cancelInFlight = cancelLogin; - - let models: ManagedKimiCodeModelInfo[]; - try { - models = await fetchOpenPlatformModels(platform, apiKey, fetch, controller.signal); - models = filterModelsByPrefix(models, platform); - } catch (error) { - if (controller.signal.aborted) return; - const msg = formatErrorMessage(error); - host.showError(`Failed to verify API key: ${msg}`); - if ( - error instanceof OpenPlatformApiError && - error.status === 401 - ) { - host.showStatus( - 'Hint: If your API key was obtained from Pythinker OAuth, please select "Pythinker (OAuth)" instead.', - ); - } - return; - } finally { - if (host.cancelInFlight === cancelLogin) { - host.cancelInFlight = undefined; - } - } - - if (models.length === 0) { - host.showError('No models available for this platform.'); - return; - } - - const selection = await promptModelSelectionForOpenPlatform(host, models, platform); - if (selection === undefined) return; - - const existingConfig = await host.harness.getConfig(); - if (existingConfig.providers[platform.id] !== undefined) { - await host.harness.removeProvider(platform.id); - } - - const config = await host.harness.getConfig(); - applyOpenPlatformConfig(config as ManagedKimiConfigShape, { - platform, - models, - selectedModel: selection.model, - thinking: selection.effort !== 'off', - apiKey, - }); - - await host.harness.setConfig({ - providers: config.providers, - models: config.models, - defaultModel: config.defaultModel, - defaultThinking: config.defaultThinking, - }); - - await host.authFlow.refreshConfigAfterLogin(); - host.track('login', { provider: platform.id, method: 'api_key' }); - host.showStatus(`Setup complete: ${platform.name} · ${selection.model.id}`); +export async function handleLoginCommand(host: SlashCommandHost): Promise { + await runLogin(loginUiFromHost(host)); } export async function connectCatalogProvider( @@ -219,195 +69,12 @@ export async function connectCatalogProvider( selectedCatalogEntry?: CatalogProviderEntry, displayName?: string, ): Promise { - let catalogEntry = selectedCatalogEntry; - if (catalogEntry === undefined) { - const controller = new AbortController(); - const cancelLogin = (): void => { - controller.abort(); - }; - host.cancelInFlight = cancelLogin; - try { - const catalog = await fetchCatalog(DEFAULT_CATALOG_URL, controller.signal); - catalogEntry = catalog[providerId]; - } catch (error) { - if (controller.signal.aborted) return; - host.showError(`Failed to load model catalog: ${formatErrorMessage(error)}`); - return; - } finally { - if (host.cancelInFlight === cancelLogin) { - host.cancelInFlight = undefined; - } - } - } - - if (catalogEntry === undefined) { - host.showError(`Catalog provider "${providerId}" was not found.`); - return; - } - const wire = catalogConnectionWire(catalogEntry); - if (wire === undefined) { - host.showError(`Catalog provider "${providerId}" is not supported for login.`); - return; - } - - const baseUrl = catalogBaseUrl(catalogEntry, wire); - const platformName = displayName ?? catalogEntry.name ?? providerId; - - const apiKeyEnvVar = catalogEntry.env?.[0]?.trim(); - const envVarHasValue = - apiKeyEnvVar !== undefined && - apiKeyEnvVar.length > 0 && - (process.env[apiKeyEnvVar]?.trim().length ?? 0) > 0; - let apiKey: string | undefined; - if (!envVarHasValue) { - const subtitleLines = [ - ...(baseUrl === undefined ? [] : [`${'base_url'.padEnd(12)}${baseUrl}`]), - `${'saved to'.padEnd(12)}~/.pythinker-code/config.toml`, - ]; - apiKey = await promptApiKey(host, platformName, subtitleLines); - if (apiKey === undefined) return; - } - - const models = catalogProviderModels(catalogEntry); - if (models.length === 0) { - host.showError('No models available for this platform.'); - return; - } - - const selection = await promptModelSelectionForCatalog(host, providerId, models); - if (selection === undefined) return; - - const existingConfig = await host.harness.getConfig(); - if (existingConfig.providers[providerId] !== undefined) { - await host.harness.removeProvider(providerId); - } - - const config = await host.harness.getConfig(); - applyCatalogProvider(config, { + await connectCatalogProviderFlow( + loginUiFromHost(host), providerId, - catalogUrl: DEFAULT_CATALOG_URL, - wire, - baseUrl, - apiKey, - apiKeyEnvVar: envVarHasValue ? apiKeyEnvVar : undefined, - models, - selectedModelId: selection.model.id, - thinking: selection.effort !== 'off', - }); - - await host.harness.setConfig({ - providers: config.providers, - models: config.models, - defaultModel: config.defaultModel, - defaultThinking: config.defaultThinking, - }); - - await host.authFlow.refreshConfigAfterLogin(); - host.track('login', { provider: providerId, method: envVarHasValue ? 'api_key_env' : 'api_key' }); - host.showStatus(`Setup complete: ${platformName} · ${selection.model.id}`); -} - -async function handleOpenAICodexOAuthLogin(host: SlashCommandHost): Promise { - const controller = new AbortController(); - const cancelLogin = (): void => { - controller.abort(); - }; - host.cancelInFlight = cancelLogin; - - host.showStatus('Opening browser for OpenAI Codex sign-in…'); - const { openUrl } = await import('#/utils/open-url'); - - let tokens; - try { - tokens = await runOpenAICodexOAuthFlow({ - signal: controller.signal, - openBrowser: (url) => { - host.showStatus('Opening browser for OpenAI Codex sign-in…'); - openUrl(url); - }, - onManualInput: async () => - promptApiKey( - host, - 'OpenAI Codex (OAuth)', - [ - 'Sign in with your ChatGPT account in the browser.', - 'If the browser callback fails, paste the full redirect URL here.', - ], - { - title: 'Paste OpenAI Codex redirect URL', - secret: false, - emptyMessage: 'Redirect URL cannot be empty.', - }, - ), - }); - } catch (error) { - if (controller.signal.aborted) { - host.showStatus('OpenAI Codex login cancelled.'); - return; - } - host.showError(`OpenAI Codex login failed: ${formatErrorMessage(error)}`); - return; - } finally { - if (host.cancelInFlight === cancelLogin) { - host.cancelInFlight = undefined; - } - } - - const existingConfig = await host.harness.getConfig(); - if (existingConfig.providers[OPENAI_CODEX_PROVIDER_ID] !== undefined) { - await host.harness.removeProvider(OPENAI_CODEX_PROVIDER_ID); - } - - let models: ManagedKimiCodeModelInfo[]; - try { - models = await fetchOpenAICodexModels({ - accessToken: tokens.accessToken, - accountId: tokens.accountId, - signal: controller.signal, - }); - } catch (error) { - if (controller.signal.aborted) return; - host.showError(`Failed to list OpenAI Codex models: ${formatErrorMessage(error)}`); - if (error instanceof OpenAICodexApiError && error.status === 401) { - host.showStatus('Hint: Sign in again with /login if your OpenAI Codex session expired.'); - } - return; - } - - if (models.length === 0) { - host.showError('No models available for OpenAI Codex.'); - return; - } - - const codexPlatform: OpenPlatformDefinition = { - id: OPENAI_CODEX_PROVIDER_ID, - name: 'OpenAI Codex (OAuth)', - defaultContextLength: 256_000, - }; - const selection = await promptModelSelectionForOpenPlatform(host, models, codexPlatform); - if (selection === undefined) return; - - const config = await host.harness.getConfig(); - applyOpenAICodexOAuthConfig(config as ManagedKimiConfigShape, { - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken, - accountId: tokens.accountId, - models, - selectedModel: selection.model, - thinking: selection.effort !== 'off', - }); - - await host.harness.setConfig({ - providers: config.providers, - models: config.models, - defaultModel: config.defaultModel, - defaultThinking: config.defaultThinking, - thinking: config.thinking, - }); - - await host.authFlow.refreshConfigAfterLogin(); - host.track('login', { provider: OPENAI_CODEX_OAUTH_PLATFORM_ID, method: 'oauth' }); - host.showStatus(`Logged in to OpenAI Codex · ${selection.model.id}`); + selectedCatalogEntry, + displayName, + ); } export async function handleLogoutCommand(host: SlashCommandHost): Promise { diff --git a/apps/pythinker-code/src/tui/commands/config.ts b/apps/pythinker-code/src/tui/commands/config.ts index 02e296c3..bff3709d 100644 --- a/apps/pythinker-code/src/tui/commands/config.ts +++ b/apps/pythinker-code/src/tui/commands/config.ts @@ -11,6 +11,7 @@ import type { Session, WorkspaceDirectory, } from '@pythoughts/pythinker-code-sdk'; +import { coerceEffortForModel, effortLevelsForModel } from '@pythoughts/pythinker-code-sdk'; import { disableTelemetry } from '@pythoughts/pythinker-telemetry'; import { ApiKeyInputDialogComponent } from '../components/dialogs/api-key-input-dialog'; @@ -42,7 +43,6 @@ import { } from '#/utils/process/external-editor'; import { LLM_NOT_SET_MESSAGE, NO_ACTIVE_SESSION_MESSAGE } from '../constant/pythinker-tui'; import { formatErrorMessage } from '../utils/event-payload'; -import { coerceEffortForModel, effortLevelsForModel } from '../utils/thinking-levels'; import { showUsage } from './info'; import { setExperimentalFeatures } from './experimental-flags'; import { showDirectoryInput } from './add-dir'; diff --git a/apps/pythinker-code/src/tui/commands/dynamic-workflow.ts b/apps/pythinker-code/src/tui/commands/dynamic-workflow.ts index 5e9e428a..4b16aa98 100644 --- a/apps/pythinker-code/src/tui/commands/dynamic-workflow.ts +++ b/apps/pythinker-code/src/tui/commands/dynamic-workflow.ts @@ -11,8 +11,13 @@ import { import { LLM_NOT_SET_MESSAGE, NO_ACTIVE_SESSION_MESSAGE } from '../constant/pythinker-tui'; import { formatErrorMessage } from '../utils/event-payload'; import type { SlashCommandHost } from './dispatch'; +import { isDynamicWorkflowDisabled } from './workflow-availability'; export async function handleDynamicWorkflowCommand(host: SlashCommandHost, args: string): Promise { + if (isDynamicWorkflowDisabled()) { + host.showError('Dynamic Workflow is disabled by configuration.'); + return; + } if (host.session === undefined) { host.showError(NO_ACTIVE_SESSION_MESSAGE); return; diff --git a/apps/pythinker-code/src/tui/commands/prompts.ts b/apps/pythinker-code/src/tui/commands/prompts.ts index 507108f4..4b2ce656 100644 --- a/apps/pythinker-code/src/tui/commands/prompts.ts +++ b/apps/pythinker-code/src/tui/commands/prompts.ts @@ -1,14 +1,17 @@ import { catalogModelToAlias, catalogConnectionWire, + coerceEffortForModel, DEFAULT_CATALOG_URL, + effortLevelsForModel, fetchCatalog, loadBuiltInCatalog, + managedModelToAlias, type Catalog, type CatalogModel, type ModelAlias, + type PlatformSelection, } from '@pythoughts/pythinker-code-sdk'; -import { capabilitiesForModel } from '@pythoughts/pythinker-code-oauth'; import type { ManagedKimiCodeModelInfo, OpenPlatformDefinition, @@ -20,15 +23,9 @@ import { FeedbackInputDialogComponent, type FeedbackInputDialogResult } from '.. import { ModelSelectorComponent } from '../components/dialogs/model-selector'; import { PlatformSelectorComponent } from '../components/dialogs/platform-selector'; import { BUILT_IN_CATALOG_JSON } from '#/built-in-catalog'; -import { coerceEffortForModel, effortLevelsForModel } from '../utils/thinking-levels'; import { formatErrorMessage } from '../utils/event-payload'; import type { SlashCommandHost } from './dispatch'; -export interface PlatformSelection { - readonly platformId: string; - readonly catalog: Catalog; -} - export async function promptPlatformSelection( host: SlashCommandHost, ): Promise { @@ -164,13 +161,7 @@ export async function promptModelSelectionForOpenPlatform( ): Promise<{ model: ManagedKimiCodeModelInfo; effort: string } | undefined> { const modelDict: Record = {}; for (const m of models) { - modelDict[`${platform.id}/${m.id}`] = { - provider: platform.id, - model: m.id, - maxContextSize: m.contextLength, - capabilities: capabilitiesForModel(m), - displayName: m.displayName, - }; + modelDict[`${platform.id}/${m.id}`] = managedModelToAlias(platform.id, m); } const selection = await runModelSelector(host, modelDict); if (selection === undefined) return undefined; diff --git a/apps/pythinker-code/src/tui/commands/workflow-availability.ts b/apps/pythinker-code/src/tui/commands/workflow-availability.ts new file mode 100644 index 00000000..5346b84c --- /dev/null +++ b/apps/pythinker-code/src/tui/commands/workflow-availability.ts @@ -0,0 +1,25 @@ +const DISABLE_WORKFLOWS_ENV = 'PYTHINKER_CODE_DISABLE_WORKFLOWS'; +const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']); +const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']); + +let disabled = false; + +/** Cache the resolved switch. Call once at startup with the value from `harness.getConfig()`. */ +export function setDynamicWorkflowDisabled(configValue: boolean | undefined, env = process.env): void { + const envValue = env[DISABLE_WORKFLOWS_ENV]?.trim().toLowerCase(); + if (envValue !== undefined && envValue.length > 0) { + if (TRUE_ENV_VALUES.has(envValue)) { + disabled = true; + return; + } + if (FALSE_ENV_VALUES.has(envValue)) { + disabled = false; + return; + } + } + disabled = configValue ?? false; +} + +export function isDynamicWorkflowDisabled(): boolean { + return disabled; +} diff --git a/apps/pythinker-code/src/tui/components/dialogs/model-selector.ts b/apps/pythinker-code/src/tui/components/dialogs/model-selector.ts index ef44051f..caf31316 100644 --- a/apps/pythinker-code/src/tui/components/dialogs/model-selector.ts +++ b/apps/pythinker-code/src/tui/components/dialogs/model-selector.ts @@ -1,4 +1,9 @@ -import type { ModelAlias } from '@pythoughts/pythinker-code-sdk'; +import { + coerceEffortForModel, + effortLevelsForModel, + thinkingAvailability, + type ModelAlias, +} from '@pythoughts/pythinker-code-sdk'; import { Container, Key, @@ -18,12 +23,7 @@ import { } from '#/tui/keybindings'; import { currentTheme } from '#/tui/theme'; import { SearchableList } from '#/tui/utils/searchable-list'; -import { - coerceEffortForModel, - effortLevelsForModel, - shortEffortLabel, - thinkingAvailability, -} from '#/tui/utils/thinking-levels'; +import { shortEffortLabel } from '#/tui/utils/thinking-levels'; import { combinedBindingHint, diff --git a/apps/pythinker-code/src/tui/components/dialogs/platform-selector.ts b/apps/pythinker-code/src/tui/components/dialogs/platform-selector.ts index e546d197..d398700c 100644 --- a/apps/pythinker-code/src/tui/components/dialogs/platform-selector.ts +++ b/apps/pythinker-code/src/tui/components/dialogs/platform-selector.ts @@ -1,84 +1,7 @@ -import { OPENAI_CODEX_OAUTH_LOGIN, OPEN_PLATFORMS } from '@pythoughts/pythinker-code-oauth'; -import { - catalogConnectionWire, - type Catalog, - type CatalogProviderEntry, -} from '@pythoughts/pythinker-code-sdk'; +import { buildPlatformOptions } from '@pythoughts/pythinker-code-sdk'; +import type { Catalog } from '@pythoughts/pythinker-code-sdk'; -import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; - -export const CATALOG_PLATFORM_VALUE_PREFIX = 'catalog:'; - -const FEATURED_CATALOG_PROVIDERS = [ - { id: 'deepseek', label: 'DeepSeek API' }, - { id: 'zai-coding-plan', label: 'GLM Coding Plan' }, - { id: 'minimax-coding-plan', label: 'MiniMax Token Plan' }, - { id: 'kimi-for-coding', label: 'Kimi For Coding' }, -] as const; - -const REPLACED_OPEN_PLATFORM_IDS = new Set(['moonshot-ai', 'minimax-token']); - -export function catalogProviderIdFromPlatformValue(value: string): string | undefined { - if (!value.startsWith(CATALOG_PLATFORM_VALUE_PREFIX)) return undefined; - const providerId = value.slice(CATALOG_PLATFORM_VALUE_PREFIX.length); - return providerId.length > 0 ? providerId : undefined; -} - -function catalogOption( - providerId: string, - entry: CatalogProviderEntry | undefined, - label = entry?.name ?? providerId, -): ChoiceOption { - return { - value: `${CATALOG_PLATFORM_VALUE_PREFIX}${providerId}`, - label, - description: - typeof entry?.api === 'string' && entry.api.length > 0 ? entry.api : 'API key', - }; -} - -function buildPlatformOptions(catalog: Catalog): readonly ChoiceOption[] { - const options: ChoiceOption[] = [ - { - value: OPENAI_CODEX_OAUTH_LOGIN.id, - label: OPENAI_CODEX_OAUTH_LOGIN.name, - description: 'OAuth', - }, - { value: 'kimi-code', label: 'Kimi (OAuth)', description: 'OAuth' }, - ]; - const seen = new Set(['kimi-code', OPENAI_CODEX_OAUTH_LOGIN.id]); - - for (const featured of FEATURED_CATALOG_PROVIDERS) { - const entry = catalog[featured.id]; - if (entry === undefined || catalogConnectionWire(entry) === undefined) continue; - options.push(catalogOption(featured.id, entry, featured.label)); - seen.add(featured.id); - } - - const catalogEntries = Object.entries(catalog) - .filter(([id, entry]) => !seen.has(id) && catalogConnectionWire(entry) !== undefined) - .toSorted(([aId, a], [bId, b]) => (a.name ?? aId).localeCompare(b.name ?? bId)); - for (const [id, entry] of catalogEntries) { - options.push(catalogOption(id, entry)); - seen.add(id); - } - - for (const platform of OPEN_PLATFORMS) { - if ( - platform.catalogProviderId !== undefined || - REPLACED_OPEN_PLATFORM_IDS.has(platform.id) || - seen.has(platform.id) - ) { - continue; - } - options.push({ - value: platform.id, - label: platform.name, - description: platform.baseUrl, - }); - } - return options; -} +import { ChoicePickerComponent } from './choice-picker'; export interface PlatformSelectorOptions { readonly catalog?: Catalog; diff --git a/apps/pythinker-code/src/tui/components/messages/dynamic-workflow-mission-control.ts b/apps/pythinker-code/src/tui/components/messages/dynamic-workflow-mission-control.ts index d7b8f69a..e64e0015 100644 --- a/apps/pythinker-code/src/tui/components/messages/dynamic-workflow-mission-control.ts +++ b/apps/pythinker-code/src/tui/components/messages/dynamic-workflow-mission-control.ts @@ -1,6 +1,7 @@ import { truncateToWidth, visibleWidth, type Component } from '@earendil-works/pi-tui'; import { + BRAILLE_SPINNER_FRAMES, BRAILLE_SPINNER_INTERVAL_MS, DYNAMIC_WORKFLOW_RENDERING, } from '#/tui/constant/rendering'; @@ -47,12 +48,15 @@ export interface DynamicWorkflowMember { startedAtMs?: number; endedAtMs?: number; /** - * Observed-stage progress heuristic (0-100): the protocol emits no per-task - * percentage, so stage floors map to observed events and streamed deltas - * creep asymptotically toward a ceiling. May hold fractional values - * internally; display floors it. Only a terminal event reaches 100. + * Tool calls observed for this agent. Real work done, monotonic — unlike a + * percentage, which would need a total nobody can know in advance. */ - progressPercent: number; + toolCalls: number; + /** + * When this agent last produced any observed event. Its age is the liveness + * signal: a working agent stays near zero, a wedged one climbs without bound. + */ + lastEventAtMs: number; } export interface DynamicWorkflowActivity { @@ -100,7 +104,9 @@ export interface DynamicWorkflowMissionControlOptions { const PHASE_TOKENS: Record = { pending: '◌ PEND', queued: '◌ WAIT', - running: '● RUN', + // Label only: a running row is the one phase that animates, so its symbol is + // a spinner supplied per frame by renderPhaseCell rather than a fixed glyph. + running: 'RUN', suspended: '! HOLD', completed: '✓ DONE', failed: '× FAIL', @@ -243,7 +249,7 @@ export class DynamicWorkflowMissionControlComponent implements Component { if (member.phase === 'running') return; member.phase = 'running'; member.startedAtMs ??= Date.now(); - this.advanceMemberProgress(member, DYNAMIC_WORKFLOW_RENDERING.startedProgress); + member.lastEventAtMs = Date.now(); delete member.statusDetail; this.recordActivity(member.index, 'Started'); } @@ -255,7 +261,8 @@ export class DynamicWorkflowMissionControlComponent implements Component { const member = this.findMemberByAgentId(input.agentId); if (member === undefined || isTerminalPhase(member.phase)) return; this.markStarted(input.agentId); - this.advanceMemberProgress(member, DYNAMIC_WORKFLOW_RENDERING.toolActivityProgress); + member.toolCalls += 1; + member.lastEventAtMs = Date.now(); const latest = input.name === undefined ? 'Using a tool' : `Using ${input.name}`; this.setLatest(member, latest, true); // Streamed text that follows starts a new line, never continues this label. @@ -267,10 +274,7 @@ export class DynamicWorkflowMissionControlComponent implements Component { if (member === undefined || isTerminalPhase(member.phase) || input.delta.length === 0) return; this.markStarted(input.agentId); const recordActivity = input.delta.includes('\n') || member.latest.length === 0; - // Progress reflects the observed stage only. The protocol emits no per-task - // completion signal, so streamed text never advances past its stage floor — - // elapsed time and the latest line carry liveness instead. - this.advanceMemberProgress(member, DYNAMIC_WORKFLOW_RENDERING.modelActivityProgress); + member.lastEventAtMs = Date.now(); const carried = member.latestFromTool === true ? '' : member.latest; const latest = latestNonEmptyLine(`${carried}${input.delta}`); member.latestFromTool = false; @@ -332,6 +336,10 @@ export class DynamicWorkflowMissionControlComponent implements Component { this.recordActivity('SYS', 'Workflow cancelled'); } + markWarning(message: string): void { + this.recordActivity('SYS', normalizeText(message)); + } + applyResult(output: string): boolean { const statuses = parseDynamicWorkflowResultStatuses(output); if (!isDynamicWorkflowResult(output)) return false; @@ -497,7 +505,9 @@ export class DynamicWorkflowMissionControlComponent implements Component { const label = terminal ? currentTheme.fg('text', requestPhaseLabel(this.model.requestPhase)) : shimmerText(finalizing ? FINALIZING_LABEL : ORCHESTRATING_LABEL, { - baseToken: 'text', + // `primary` / `primaryShimmer` are a designed pair, so the sweep stays + // periwinkle throughout; the old grey `text` base washed it out. + baseToken: 'primary', shimmerToken: 'primaryShimmer', frame: Math.floor( Math.max(0, nowMs - this.model.startedAtMs) / BRAILLE_SPINNER_INTERVAL_MS, @@ -538,7 +548,7 @@ export class DynamicWorkflowMissionControlComponent implements Component { const header = width >= DYNAMIC_WORKFLOW_RENDERING.memberProgressMinWidth ? [ padToWidth('ID', 3), - padToWidth('PROGRESS', DYNAMIC_WORKFLOW_RENDERING.memberProgressWidth), + padToWidth('WORK IDLE', DYNAMIC_WORKFLOW_RENDERING.memberProgressWidth), padToWidth('STATE', 6), 'TASK', ].join(' ') @@ -548,20 +558,20 @@ export class DynamicWorkflowMissionControlComponent implements Component { private renderMember(member: DynamicWorkflowMember, width: number, nowMs: number): string { const id = currentTheme.fg('primary', String(member.index).padStart(3, '0')); - const state = currentTheme.fg(PHASE_COLORS[member.phase], PHASE_TOKENS[member.phase]); - const progressPercent = member.progressPercent; - const showProgress = width >= DYNAMIC_WORKFLOW_RENDERING.memberProgressMinWidth; - const progress = `${renderProgressCube(progressPercent)} ${currentTheme.fg( - 'textMuted', - `${String(Math.floor(progressPercent)).padStart(3, ' ')}%`, - )}`; - const progressColumn = padToWidth( - progress, + // All running rows share the workflow's clock, so they spin in step instead + // of drifting apart by whenever each agent happened to start. + const state = renderPhaseCell( + member.phase, + Math.floor(Math.max(0, nowMs - this.model.startedAtMs) / BRAILLE_SPINNER_INTERVAL_MS), + ); + const showWork = width >= DYNAMIC_WORKFLOW_RENDERING.memberProgressMinWidth; + const workColumn = padToWidth( + renderWorkCell(member, nowMs), DYNAMIC_WORKFLOW_RENDERING.memberProgressWidth, ); const stateColumn = padToWidth(state, 6); - const prefix = showProgress - ? `${id} ${progressColumn} ${stateColumn} ` + const prefix = showWork + ? `${id} ${workColumn} ${stateColumn} ` : `${id} ${padToWidth(state, 6)} `; const task = member.item || 'Delegated agent'; const latest = member.latest.length > 0 && member.latest !== task ? member.latest : undefined; @@ -571,8 +581,8 @@ export class DynamicWorkflowMissionControlComponent implements Component { const elapsed = member.startedAtMs === undefined ? undefined : `${String(elapsedSeconds(member.startedAtMs, member.endedAtMs ?? nowMs))}s`; - const showDetail = showProgress && detail !== undefined && detail.length > 0; - const showElapsed = showProgress && elapsed !== undefined; + const showDetail = showWork && detail !== undefined && detail.length > 0; + const showElapsed = showWork && elapsed !== undefined; const tail = [ showDetail ? currentTheme.fg('textDim', detail) : '', showElapsed ? currentTheme.fg('textMuted', elapsed) : '', @@ -645,7 +655,8 @@ export class DynamicWorkflowMissionControlComponent implements Component { item: '', phase: this.model.inputComplete ? 'queued' : 'pending', latest: '', - progressPercent: 0, + toolCalls: 0, + lastEventAtMs: Date.now(), }); } } @@ -670,20 +681,12 @@ export class DynamicWorkflowMissionControlComponent implements Component { const normalizedDetail = normalizeText(detail); member.phase = phase; member.endedAtMs = Date.now(); - member.progressPercent = 100; + member.lastEventAtMs = Date.now(); member.statusDetail = normalizedDetail.length > 0 ? normalizedDetail : undefined; const label = phase === 'completed' ? 'Completed' : phase === 'failed' ? 'Failed' : 'Cancelled'; this.recordActivity(member.index, normalizedDetail.length > 0 ? `${label}: ${normalizedDetail}` : label); } - /** - * Progress only ever advances; stages map to observed events, never to time. - * Creep is per observed event too (each streamed delta), so no timers exist. - */ - private advanceMemberProgress(member: DynamicWorkflowMember, targetPercent: number): void { - member.progressPercent = Math.max(member.progressPercent, targetPercent); - } - private setLatest(member: DynamicWorkflowMember, latest: string, recordActivity: boolean): void { const normalized = normalizeText(latest); if (normalized.length === 0 || member.latest === normalized) return; @@ -706,7 +709,14 @@ export class DynamicWorkflowMissionControlComponent implements Component { /** Item list from the completed tool-call `items` argument. */ export function dynamicWorkflowItemsFromArgs(args: Record): string[] { const items = args['items']; - return Array.isArray(items) ? items.map(itemLabel) : []; + if (!Array.isArray(items)) return []; + // Blank entries are dropped by the engine before any agent is launched, so + // counting them here would leave a phantom row waiting forever and pin the + // header below its total. Non-strings are kept: the engine rejects those, and + // itemLabel renders them readably. + return items + .filter((item) => typeof item !== 'string' || item.trim().length > 0) + .map(itemLabel); } /** @@ -958,23 +968,6 @@ function decodeXmlEntities(value: string): string { } /** Maps a percent to one of the dotted cube levels; the cube fills bottom-up. */ -function renderProgressCube(percent: number): string { - const bounded = Math.min(100, Math.max(0, percent)); - const levels = DYNAMIC_WORKFLOW_RENDERING.cubeFillLevels; - const level = bounded <= 0 - ? 0 - : bounded >= 100 - ? levels.length - 1 - : Math.min( - levels.length - 2, - Math.ceil(bounded * (levels.length - 2) / 100), - ); - const fill = levels[level]!; - const fillToken = bounded >= 100 ? 'progressHead' : 'progressFill'; - return currentTheme.fg('progressEmpty', '▏') + - currentTheme.fg(fillToken, fill.repeat(2)) + - currentTheme.fg('progressEmpty', '▕'); -} function requestPhaseLabel(phase: DynamicWorkflowRequestPhase): string { const labels: Record = { @@ -1033,6 +1026,63 @@ function normalizeText(text: string | undefined): string { return text?.replaceAll(/\s+/g, ' ').trim() ?? ''; } +/** + * The WORK cell: tool calls done, and how long this agent has been silent. + * + * There is deliberately no percentage. Nothing knows how many steps an agent + * will take, so any percent is invented — the old one pinned every tool-using + * agent at 75% until it finished, which made a wedged agent look identical to a + * busy one. A count and an idle age are both real and answer the actual + * question: is this thing still working? + */ +function renderWorkCell(member: DynamicWorkflowMember, nowMs: number): string { + const tools = currentTheme.fg('textDim', `${String(member.toolCalls).padStart(3, ' ')}⚒`); + // A row that has not started has no silence to measure: its clock would run + // from the launch of the whole workflow, so a queue that is simply long would + // paint every waiting row red. Only a finished row and an unstarted one share + // the placeholder; the reason differs, but neither has an idle age. + if (isTerminalPhase(member.phase) || member.phase === 'pending' || member.phase === 'queued') { + return `${tools} ${currentTheme.fg('textMuted', ' –')}`; + } + const idleMs = Math.max(0, nowMs - member.lastEventAtMs); + const idleSeconds = Math.floor(idleMs / 1000); + const token = idleColor(member.phase, idleMs); + return `${tools} ${currentTheme.fg(token, `${String(idleSeconds)}s`.padStart(4, ' '))}`; +} + +/** + * How loud an idle age reads. + * + * Only a running row can stall. A suspended one is waiting on the user by + * design, so it keeps the count without the alarm colours. + */ +function idleColor( + phase: DynamicWorkflowPhase, + idleMs: number, +): 'textMuted' | 'warning' | 'error' { + if (phase === 'running') { + if (idleMs >= DYNAMIC_WORKFLOW_RENDERING.stalledIdleMs) return 'error'; + if (idleMs >= DYNAMIC_WORKFLOW_RENDERING.quietIdleMs) return 'warning'; + } + return 'textMuted'; +} + +/** + * The STATE cell for one row. + * + * Every phase but `running` is a fixed symbol plus its label. A running row + * spins a dim grey braille dot instead, so "this agent is working" reads as + * motion rather than as another coloured dot competing with the periwinkle the + * panel already uses for identity. + */ +function renderPhaseCell(phase: DynamicWorkflowPhase, frame: number): string { + const label = currentTheme.fg(PHASE_COLORS[phase], PHASE_TOKENS[phase]); + if (phase !== 'running') return label; + const spinner = + BRAILLE_SPINNER_FRAMES[frame % BRAILLE_SPINNER_FRAMES.length] ?? BRAILLE_SPINNER_FRAMES[0] ?? ''; + return `${currentTheme.fg('textDim', spinner)} ${label}`; +} + function padToWidth(text: string, width: number): string { return text + ' '.repeat(Math.max(0, width - visibleWidth(text))); } diff --git a/apps/pythinker-code/src/tui/constant/rendering.ts b/apps/pythinker-code/src/tui/constant/rendering.ts index 7798a817..e42fda80 100644 --- a/apps/pythinker-code/src/tui/constant/rendering.ts +++ b/apps/pythinker-code/src/tui/constant/rendering.ts @@ -28,11 +28,10 @@ export const DYNAMIC_WORKFLOW_RENDERING = { frameHorizontalInset: 4, memberProgressMinWidth: 60, memberProgressWidth: 9, - startedProgress: 20, - modelActivityProgress: 50, - toolActivityProgress: 75, - // Two 2×4 Braille cells form a compact 4×4 dotted cube that fills bottom-up. - cubeFillLevels: [' ', '⡀', '⣀', '⣄', '⣤', '⣦', '⣶', '⣷', '⣿'], + /** Idle age at which a row's silence is worth noticing. */ + quietIdleMs: 60_000, + /** Idle age at which a row has almost certainly stalled. */ + stalledIdleMs: 180_000, } as const; /** Live activity labels: one shown at a time, rotating on a fixed cadence. */ diff --git a/apps/pythinker-code/src/tui/controllers/auth-flow.ts b/apps/pythinker-code/src/tui/controllers/auth-flow.ts index 35a55bea..2f0c44cd 100644 --- a/apps/pythinker-code/src/tui/controllers/auth-flow.ts +++ b/apps/pythinker-code/src/tui/controllers/auth-flow.ts @@ -1,4 +1,8 @@ -import type { PythinkerHarness, Session } from '@pythoughts/pythinker-code-sdk'; +import { + coerceEffortForModel, + type PythinkerHarness, + type Session, +} from '@pythoughts/pythinker-code-sdk'; import type { SkillListSession } from '../commands'; import { OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE } from '../constant/pythinker-tui'; @@ -7,7 +11,6 @@ import { type RefreshProviderScope, type RefreshResult, } from '../utils/refresh-providers'; -import { coerceEffortForModel } from '../utils/thinking-levels'; import type { SessionEventHandler } from './session-event-handler'; import type { AppState, PythinkerTUIOptions } from '../types'; import type { TUIState } from '../tui-state'; diff --git a/apps/pythinker-code/src/tui/controllers/editor-keyboard.ts b/apps/pythinker-code/src/tui/controllers/editor-keyboard.ts index e64a6bb0..d8c8fbfd 100644 --- a/apps/pythinker-code/src/tui/controllers/editor-keyboard.ts +++ b/apps/pythinker-code/src/tui/controllers/editor-keyboard.ts @@ -1,5 +1,9 @@ import { Editor, parseKey } from '@earendil-works/pi-tui'; -import type { Session } from '@pythoughts/pythinker-code-sdk'; +import { + coerceEffortForModel, + effortLevelsForModel, + type Session, +} from '@pythoughts/pythinker-code-sdk'; import { ClipboardMediaError, readClipboardMedia } from '#/utils/clipboard/clipboard-image'; import { parseImageMeta } from '#/utils/image/image-mime'; @@ -13,7 +17,6 @@ import { } from '../constant/pythinker-tui'; import { formatErrorMessage } from '../utils/event-payload'; import type { ImageAttachmentStore } from '../utils/image-attachment-store'; -import { coerceEffortForModel, effortLevelsForModel } from '../utils/thinking-levels'; import type { AppState, PendingExit } from '../types'; import type { TUIState } from '../tui-state'; import type { FooterEvent } from '../runtime/footer/footer-model'; diff --git a/apps/pythinker-code/src/tui/controllers/session-event-handler.ts b/apps/pythinker-code/src/tui/controllers/session-event-handler.ts index 383bde48..ef66d18a 100644 --- a/apps/pythinker-code/src/tui/controllers/session-event-handler.ts +++ b/apps/pythinker-code/src/tui/controllers/session-event-handler.ts @@ -310,6 +310,8 @@ export class SessionEventHandler { case 'subagent.completed': case 'subagent.failed': this.subAgentEventHandler.handleLifecycleEvent(event); break; + case 'workflow.warning': + this.subAgentEventHandler.handleWorkflowWarning(event); break; case 'background.task.started': case 'background.task.terminated': this.handleBackgroundTaskEvent(event); break; diff --git a/apps/pythinker-code/src/tui/controllers/subagent-event-handler.ts b/apps/pythinker-code/src/tui/controllers/subagent-event-handler.ts index 0b31df70..a8c69186 100644 --- a/apps/pythinker-code/src/tui/controllers/subagent-event-handler.ts +++ b/apps/pythinker-code/src/tui/controllers/subagent-event-handler.ts @@ -212,6 +212,18 @@ export class SubAgentEventHandler { } } + handleWorkflowWarning(event: Extract): void { + const missionControl = this.dynamicWorkflowMissionControls.get(event.parentToolCallId); + if (missionControl !== undefined) { + missionControl.markWarning(event.message); + this.requestRender(); + return; + } + // The tool call was retired or the warning arrived without a card: never + // drop it silently. + this.host.showStatus(event.message, 'warning'); + } + // Retires every live mission control: the tool call ids are recorded so // any late events for them are dropped, and their subagents are forgotten. clearDynamicWorkflowMissionControls(): void { diff --git a/apps/pythinker-code/src/tui/pythinker-tui.ts b/apps/pythinker-code/src/tui/pythinker-tui.ts index 29071aa8..c5af20f1 100644 --- a/apps/pythinker-code/src/tui/pythinker-tui.ts +++ b/apps/pythinker-code/src/tui/pythinker-tui.ts @@ -50,6 +50,10 @@ import { onExperimentalFeaturesChanged, setExperimentalFeatures, } from './commands/experimental-flags'; +import { + isDynamicWorkflowDisabled, + setDynamicWorkflowDisabled, +} from './commands/workflow-availability'; import * as slashCommands from './commands/dispatch'; import { BannerComponent } from './components/chrome/banner'; import { DeviceCodeBoxComponent } from './components/chrome/device-code-box'; @@ -380,7 +384,10 @@ export class PythinkerTUI { private getSlashCommands(): readonly PythinkerSlashCommand[] { const builtins = sortSlashCommands(BUILTIN_SLASH_COMMANDS).filter( - (command) => command.hidden !== true && isExperimentalFlagEnabled(command.experimentalFlag), + (command) => + command.hidden !== true && + (command.name !== 'workflow' || !isDynamicWorkflowDisabled()) && + isExperimentalFlagEnabled(command.experimentalFlag), ); return [...builtins, ...this.skillCommands]; } @@ -643,6 +650,7 @@ export class PythinkerTUI { private async init(): Promise { setExperimentalFeatures(await this.harness.getExperimentalFeatures()); + setDynamicWorkflowDisabled((await this.harness.getConfig()).disableWorkflows); await this.authFlow.refreshAvailableModels(); void this.refreshProviderModelsInBackground(); diff --git a/apps/pythinker-code/src/tui/utils/event-payload.ts b/apps/pythinker-code/src/tui/utils/event-payload.ts index 52961e32..f9b3d374 100644 --- a/apps/pythinker-code/src/tui/utils/event-payload.ts +++ b/apps/pythinker-code/src/tui/utils/event-payload.ts @@ -1,14 +1,13 @@ -import { - isPythinkerError, - type PythinkerErrorPayload, -} from '@pythoughts/pythinker-code-sdk'; - import { STREAMING_ARGS_FIELD_RE, STREAMING_ARGS_PREVIEW_MAX_CHARS, } from '#/tui/constant/streaming'; import type { TodoItem } from '#/tui/components/chrome/todo-panel'; +// Generic error formatting lives in the SDK so non-TUI surfaces (login flows, +// CLI) share it; re-exported here for the TUI's existing importers. +export { formatErrorMessage, formatErrorPayload } from '@pythoughts/pythinker-code-sdk'; + export function appendStreamingArgsPreview( current: string | undefined, next: string | null | undefined, @@ -117,45 +116,6 @@ export function normalizeTodoList(value: unknown): TodoItem[] { return todos.length > 0 && todos.every((todo) => todo.status === 'done') ? [] : todos; } -export function formatErrorMessage(error: unknown): string { - if (isPythinkerError(error)) { - return formatErrorPayload({ - code: error.code, - message: error.message, - details: error.details, - }); - } - return error instanceof Error ? error.message : String(error); -} - -export function formatErrorPayload( - error: Pick, -): string { - const filteredMessage = formatProviderFilteredMessage(error.details); - if (filteredMessage !== undefined) return `[${error.code}] ${filteredMessage}`; - return `[${error.code}] ${error.message}`; -} - -function formatProviderFilteredMessage( - details: Record | undefined, -): string | undefined { - const finishReason = stringDetail(details, 'finishReason'); - const rawFinishReason = stringDetail(details, 'rawFinishReason'); - if (finishReason !== 'filtered' && rawFinishReason !== 'content_filter') return undefined; - - const normalizedFinishReason = finishReason ?? 'filtered'; - const raw = rawFinishReason === undefined ? '' : `, rawFinishReason=${rawFinishReason}`; - return `Provider filtered the response before visible output (finishReason=${normalizedFinishReason}${raw}).`; -} - -function stringDetail( - details: Record | undefined, - key: string, -): string | undefined { - const value = details?.[key]; - return typeof value === 'string' ? value : undefined; -} - export function stringValue(value: unknown): string | undefined { return typeof value === 'string' ? value : undefined; } diff --git a/apps/pythinker-code/src/tui/utils/thinking-levels.ts b/apps/pythinker-code/src/tui/utils/thinking-levels.ts index fca3146b..0821802b 100644 --- a/apps/pythinker-code/src/tui/utils/thinking-levels.ts +++ b/apps/pythinker-code/src/tui/utils/thinking-levels.ts @@ -1,14 +1,12 @@ /** - * Thinking-effort level helpers (TUI-only). + * Thinking-effort presentation helpers (TUI-only). * - * Models declare their selectable effort levels via `ModelAlias.supportEfforts`; - * when that metadata is missing the fallback set is low / medium / high. - * Clamping semantics mirror the Web UI's `coerceThinkingForModel` - * (apps/pythinker-web/src/lib/modelThinking.ts). + * Only theme-bound presentation lives here, because the SDK must not depend on + * the TUI theme. The level rules themselves are + * `@pythoughts/pythinker-code-sdk`'s, so the terminal and VS Code renderers + * offer the same levels for the same model; import them from there. */ -import type { ModelAlias } from '@pythoughts/pythinker-code-sdk'; - import type { ColorToken } from '#/tui/theme'; const EFFORT_COLOR_TOKENS = { @@ -20,71 +18,6 @@ const EFFORT_COLOR_TOKENS = { max: 'effortMax', } as const satisfies Record; -export type ThinkingAvailability = 'toggle' | 'always-on' | 'unsupported'; - -export const CANONICAL_EFFORT_ORDER = [ - 'off', - 'minimal', - 'low', - 'medium', - 'high', - 'xhigh', - 'max', -] as const; - -/** Fallback effort set for models without `supportEfforts` metadata. */ -export const DEFAULT_SUPPORTED_EFFORTS: readonly string[] = ['low', 'medium', 'high']; - -export function thinkingAvailability(model: ModelAlias | undefined): ThinkingAvailability { - if (model === undefined) return 'toggle'; - const caps = model.capabilities ?? []; - if (caps.includes('always_thinking')) return 'always-on'; - if (caps.includes('thinking') || model.adaptiveThinking === true) return 'toggle'; - return 'unsupported'; -} - -/** Effort levels the model actually supports ('off' excluded), canonical order. */ -function baseEffortsForModel(model: ModelAlias | undefined): string[] { - const supportEfforts = model?.supportEfforts; - if (supportEfforts?.length === 0) return ['high']; - const declared = new Set((supportEfforts ?? []).filter((effort) => - (CANONICAL_EFFORT_ORDER as readonly string[]).includes(effort), - )); - const ordered = CANONICAL_EFFORT_ORDER.filter( - (effort) => effort !== 'off' && declared.has(effort), - ); - return ordered.length > 0 ? [...ordered] : [...DEFAULT_SUPPORTED_EFFORTS]; -} - -/** Selectable levels for a model, in order. `['off']` when thinking is unsupported. */ -export function effortLevelsForModel(model: ModelAlias | undefined): string[] { - const availability = thinkingAvailability(model); - if (availability === 'unsupported') return ['off']; - const base = baseEffortsForModel(model); - return availability === 'always-on' ? base : ['off', ...base]; -} - -/** - * Clamp a requested effort to what the model supports: - * always-on + 'off' → first supported level; unsupported → 'off'; a level the - * model lacks → nearest lower supported level (else the first selectable one). - * The legacy boolean-era value 'on' maps to 'high' before clamping. - */ -export function coerceEffortForModel(model: ModelAlias | undefined, requested: string): string { - const availability = thinkingAvailability(model); - if (availability === 'unsupported') return 'off'; - const levels = effortLevelsForModel(model); - const normalized = requested === 'on' ? 'high' : requested; - if (availability === 'always-on' && normalized === 'off') return levels[0]!; - if (levels.includes(normalized)) return normalized; - const requestedIdx = (CANONICAL_EFFORT_ORDER as readonly string[]).indexOf(normalized); - for (let i = requestedIdx - 1; i >= 0; i--) { - const candidate = CANONICAL_EFFORT_ORDER[i]!; - if (levels.includes(candidate)) return candidate; - } - return levels[0]!; -} - export function effortColorToken(level: string): ColorToken { return EFFORT_COLOR_TOKENS[level as keyof typeof EFFORT_COLOR_TOKENS] ?? 'primary'; } diff --git a/apps/pythinker-code/test/cli/acp.test.ts b/apps/pythinker-code/test/cli/acp.test.ts index 2b63af1a..a95c5533 100644 --- a/apps/pythinker-code/test/cli/acp.test.ts +++ b/apps/pythinker-code/test/cli/acp.test.ts @@ -170,19 +170,38 @@ describe('pythinker acp', () => { }); it('exits without starting the ACP server when --login is passed', async () => { - // Stub the harness module so runLoginFlow doesn't hit a real OAuth - // endpoint: harness.auth.login resolves immediately and triggers exit 0. - // `importOriginal` preserves the other named exports (`ErrorCodes`, etc.) - // that constant/app.ts depends on at module load. - const loginStub = vi.fn(async () => ({ providerName: 'pythinker-code' })); + // `acp --login` pivots into the same `runLoginFlow` as `pythinker login`: + // the terminal picker (mocked to choose Kimi OAuth) runs, then the login + // resolves and triggers exit 0. `importOriginal` preserves the other named + // exports (`ErrorCodes`, etc.) that constant/app.ts depends on at module + // load, and the fetchCatalog stub keeps the flow offline. + const originalAcpIsTTY = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY'); + Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: true }); + const loginStub = vi.fn(async () => ({ providerName: 'managed:kimi-code' })); + vi.doMock(import('@clack/prompts'), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + select: vi.fn().mockResolvedValue('kimi-code'), + spinner: vi.fn(() => ({ + start: vi.fn(), + stop: vi.fn(), + error: vi.fn(), + })) as unknown as typeof actual.spinner, + }; + }); vi.doMock(import('@pythoughts/pythinker-code-sdk'), async (importOriginal) => { const actual = await importOriginal(); return { ...actual, createPythinkerHarness: () => ({ - auth: { login: loginStub }, + auth: { + login: loginStub, + status: vi.fn(async () => ({ providers: [] })), + }, }) as unknown as ReturnType, + fetchCatalog: vi.fn().mockRejectedValue(new Error('offline')), }; }); vi.resetModules(); @@ -199,7 +218,13 @@ describe('pythinker acp', () => { expect(runAcpServer).not.toHaveBeenCalled(); expect(exitSpy).toHaveBeenCalledWith(0); } finally { + vi.doUnmock('@clack/prompts'); vi.doUnmock('@pythoughts/pythinker-code-sdk'); + if (originalAcpIsTTY === undefined) { + delete (process.stdin as { isTTY?: boolean }).isTTY; + } else { + Object.defineProperty(process.stdin, 'isTTY', originalAcpIsTTY); + } vi.resetModules(); } }); diff --git a/apps/pythinker-code/test/cli/login.test.ts b/apps/pythinker-code/test/cli/login.test.ts index c3ca7b2d..519be705 100644 --- a/apps/pythinker-code/test/cli/login.test.ts +++ b/apps/pythinker-code/test/cli/login.test.ts @@ -1,15 +1,32 @@ /** * `pythinker login` * - * Verifies that the login sub-command is registered on the program and - * that the action drives `harness.auth.login`, prints the device code to - * stderr, and exits with the right code on success / failure. + * Verifies that the login sub-command is registered on the program and that + * the action drives the multi-provider login flow: the provider picker + * (`@clack/prompts`) is offered unless `--provider ` skips it, the device + * code is printed to stderr, and the process exits with the right code on + * success / cancellation / failure. Non-TTY stdin refuses to prompt. */ import { Command } from 'commander'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { DeviceAuthorization } from '@pythoughts/pythinker-code-oauth'; + +import { password, select, text } from '@clack/prompts'; +import { + createPythinkerHarness, + fetchCatalog, + type Catalog, +} from '@pythoughts/pythinker-code-sdk'; + +import { registerLoginCommand } from '#/cli/sub/login'; +import { openUrl } from '#/utils/open-url'; const mockLogin = vi.fn(); +const mockStatus = vi.fn(); +const mockGetConfig = vi.fn(); +const mockSetConfig = vi.fn(); +const mockRemoveProvider = vi.fn(); vi.mock('@pythoughts/pythinker-code-sdk', async () => { const actual = await vi.importActual( @@ -20,17 +37,64 @@ vi.mock('@pythoughts/pythinker-code-sdk', async () => { createPythinkerHarness: vi.fn(() => ({ auth: { login: mockLogin, + status: mockStatus, }, + getConfig: mockGetConfig, + setConfig: mockSetConfig, + removeProvider: mockRemoveProvider, })), + // Deterministic offline fallback: login must work from the bundled + // catalog without touching the network. + fetchCatalog: vi.fn().mockRejectedValue(new Error('offline')), }; }); -vi.mock('#/utils/open-url', () => ({ openUrl: vi.fn() })); +vi.mock('@clack/prompts', async () => { + const actual = await vi.importActual('@clack/prompts'); + // clack's log caches its own write reference at import time, which would + // bypass the per-test process.stderr.write spy; route through the live + // stream instead so the assertions below observe the output. + const logWrite = (message: string): void => { + process.stderr.write(`${message}\n`); + }; + // clack 1.7's real cancel sentinel is a private local symbol, so the mock + // pairs its own sentinel with a matching isCancel predicate. + const cancelSymbol = Symbol.for('clack:cancel'); + return { + ...actual, + isCancel: (value: unknown): boolean => value === cancelSymbol, + log: { + message: logWrite, + info: logWrite, + success: logWrite, + warn: logWrite, + error: logWrite, + }, + select: vi.fn(), + password: vi.fn(), + text: vi.fn(), + spinner: vi.fn(() => ({ + start: vi.fn(), + stop: vi.fn(), + error: vi.fn(), + })), + }; +}); -import { createPythinkerHarness } from '@pythoughts/pythinker-code-sdk'; +// The oauth package is imported transitively before this module's body runs, +// so the spy has to exist at hoist time. +const { mockFetchOpenPlatformModels } = vi.hoisted(() => ({ + mockFetchOpenPlatformModels: vi.fn(), +})); -import { registerLoginCommand } from '#/cli/sub/login'; -import { openUrl } from '#/utils/open-url'; +vi.mock('@pythoughts/pythinker-code-oauth', async () => { + const actual = await vi.importActual( + '@pythoughts/pythinker-code-oauth', + ); + return { ...actual, fetchOpenPlatformModels: mockFetchOpenPlatformModels }; +}); + +vi.mock('#/utils/open-url', () => ({ openUrl: vi.fn() })); class ExitCalled extends Error { constructor(public code: number | string | null | undefined) { @@ -38,14 +102,34 @@ class ExitCalled extends Error { } } +const CANCEL = Symbol.for('clack:cancel'); + describe('pythinker login', () => { let exitSpy: ReturnType; + let originalIsTTY: PropertyDescriptor | undefined; let stderrSpy: ReturnType; beforeEach(() => { mockLogin.mockReset(); + mockStatus.mockReset(); + mockGetConfig.mockReset(); + mockSetConfig.mockReset(); + mockRemoveProvider.mockReset(); vi.mocked(openUrl).mockReset(); vi.mocked(createPythinkerHarness).mockClear(); + vi.mocked(select).mockReset(); + // Every prompt and catalog mock resets too: a test that sets a persistent + // implementation (the API-key prompt, the model catalog) would otherwise + // leak it into whichever test runs next. + vi.mocked(password).mockReset(); + vi.mocked(text).mockReset(); + vi.mocked(fetchCatalog).mockReset(); + vi.mocked(fetchCatalog).mockRejectedValue(new Error('offline')); + mockFetchOpenPlatformModels.mockReset(); + // Capture the real descriptor so teardown restores it exactly; assigning + // `undefined` would leave a fake own-property behind for later suites. + originalIsTTY = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY'); + Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: true }); exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number | string | null) => { throw new ExitCalled(code); }) as never); @@ -64,123 +148,301 @@ describe('pythinker login', () => { afterEach(() => { exitSpy.mockRestore(); stderrSpy.mockRestore(); + if (originalIsTTY === undefined) { + delete (process.stdin as { isTTY?: boolean }).isTTY; + } else { + Object.defineProperty(process.stdin, 'isTTY', originalIsTTY); + } }); - it('registers a `login` subcommand on the program', () => { + function pickerProgram(): Command { + const program = new Command('pythinker').exitOverride(); + registerLoginCommand(program); + return program; + } + + async function runLogin(args: readonly string[]): Promise { + await expect(pickerProgram().parseAsync(['node', 'pythinker', ...args])).rejects.toThrow( + ExitCalled, + ); + } + + function writtenChunks(): string[] { + return stderrSpy.mock.calls.map((call: unknown[]) => String(call[0])); + } + + /** One connectable catalog provider, enough for `buildPlatformOptions` to list it. */ + function catalogWithDeepSeek(): Catalog { + return { + deepseek: { + id: 'deepseek', + name: 'DeepSeek', + npm: '@ai-sdk/openai-compatible', + api: 'https://api.example.com/v1', + models: { + 'deepseek-chat': { + id: 'deepseek-chat', + name: 'DeepSeek Chat', + tool_call: true, + limit: { context: 128_000, output: 8_192 }, + }, + }, + }, + }; + } + + it('registers a `login` subcommand with a --provider option on the program', () => { const program = new Command('pythinker'); registerLoginCommand(program); const login = program.commands.find((c) => c.name() === 'login'); expect(login).toBeDefined(); - expect(login?.description()).toMatch(/[Aa]uthenticat/); + expect(login?.description()).toMatch(/[Ll]og in/u); + expect(login?.options.some((option) => option.attributeName() === 'provider')).toBe(true); }); - it('invokes harness.auth.login and exits 0 on success', async () => { - mockLogin.mockResolvedValue({ providerName: 'pythinker-code', ok: true }); - - const program = new Command('pythinker').exitOverride(); - registerLoginCommand(program); + it('shows the provider picker and exits 0 after a successful OAuth login', async () => { + vi.mocked(select).mockResolvedValueOnce('kimi-code'); + mockStatus.mockResolvedValue({ providers: [] }); + mockLogin.mockResolvedValue({ providerName: 'managed:kimi-code', ok: true }); - await expect(program.parseAsync(['node', 'pythinker', 'login'])).rejects.toThrow(ExitCalled); + await runLogin(['login']); + expect(select).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Select a provider' }), + ); expect(mockLogin).toHaveBeenCalledTimes(1); expect(mockLogin).toHaveBeenCalledWith( - undefined, + 'managed:kimi-code', expect.objectContaining({ signal: expect.any(AbortSignal), onDeviceCode: expect.any(Function), }), ); - expect(exitSpy).toHaveBeenCalledWith(0); + expect(exitSpy.mock.calls[0]?.[0]).toBe(0); }); - it('prints device code prompt to stderr', async () => { + it('prints the device code and verification URL to stderr', async () => { + vi.mocked(select).mockResolvedValueOnce('kimi-code'); + mockStatus.mockResolvedValue({ providers: [] }); mockLogin.mockImplementation( async ( - _providerName: string | undefined, - options: { - onDeviceCode?: (data: { - userCode: string; - verificationUri: string; - verificationUriComplete: string; - expiresIn: number | null; - }) => void | Promise; - }, + _providerName: string, + options: { onDeviceCode?: (data: DeviceAuthorization) => void | Promise }, ) => { await options.onDeviceCode?.({ userCode: 'ABCD-EFGH', + deviceCode: 'device-code', verificationUri: 'https://example.com/v', verificationUriComplete: 'https://example.com/v?code=ABCD-EFGH', expiresIn: 600, + interval: 5, }); - return { providerName: 'pythinker-code', ok: true }; + return { providerName: 'managed:kimi-code', ok: true }; }, ); - const program = new Command('pythinker').exitOverride(); - registerLoginCommand(program); - - await expect(program.parseAsync(['node', 'pythinker', 'login'])).rejects.toThrow(ExitCalled); + await runLogin(['login']); - const writtenChunks = stderrSpy.mock.calls.map((call: unknown[]) => String(call[0])); - expect(writtenChunks.some((chunk: string) => chunk.includes('ABCD-EFGH'))).toBe(true); - expect(writtenChunks.some((chunk: string) => chunk.includes('https://example.com/v'))).toBe( - true, - ); + const chunks = writtenChunks(); + expect(chunks.some((chunk: string) => chunk.includes('ABCD-EFGH'))).toBe(true); + expect(chunks.some((chunk: string) => chunk.includes('https://example.com/v'))).toBe(true); expect(openUrl).toHaveBeenCalledWith('https://example.com/v?code=ABCD-EFGH'); - expect(exitSpy).toHaveBeenCalledWith(0); + expect(exitSpy.mock.calls[0]?.[0]).toBe(0); }); - it('still prints device code prompt when opening the browser fails', async () => { + it('still prints the device code when opening the browser fails', async () => { vi.mocked(openUrl).mockImplementation(() => { throw new Error('no browser'); }); + vi.mocked(select).mockResolvedValueOnce('kimi-code'); + mockStatus.mockResolvedValue({ providers: [] }); mockLogin.mockImplementation( async ( - _providerName: string | undefined, - options: { - onDeviceCode?: (data: { - userCode: string; - verificationUri: string; - verificationUriComplete: string; - expiresIn: number | null; - }) => void | Promise; - }, + _providerName: string, + options: { onDeviceCode?: (data: DeviceAuthorization) => void | Promise }, ) => { await options.onDeviceCode?.({ userCode: 'ABCD-EFGH', + deviceCode: 'device-code', verificationUri: 'https://example.com/v', verificationUriComplete: 'https://example.com/v?code=ABCD-EFGH', expiresIn: 600, + interval: 5, }); - return { providerName: 'pythinker-code', ok: true }; + return { providerName: 'managed:kimi-code', ok: true }; }, ); - const program = new Command('pythinker').exitOverride(); - registerLoginCommand(program); + await runLogin(['login']); - await expect(program.parseAsync(['node', 'pythinker', 'login'])).rejects.toThrow(ExitCalled); - - const writtenChunks = stderrSpy.mock.calls.map((call: unknown[]) => String(call[0])); - expect(writtenChunks.some((chunk: string) => chunk.includes('ABCD-EFGH'))).toBe(true); - expect(writtenChunks.some((chunk: string) => chunk.includes('https://example.com/v'))).toBe( - true, - ); + const chunks = writtenChunks(); + expect(chunks.some((chunk: string) => chunk.includes('ABCD-EFGH'))).toBe(true); + expect(chunks.some((chunk: string) => chunk.includes('https://example.com/v'))).toBe(true); expect(openUrl).toHaveBeenCalledWith('https://example.com/v?code=ABCD-EFGH'); - expect(exitSpy).toHaveBeenCalledWith(0); + expect(exitSpy.mock.calls[0]?.[0]).toBe(0); }); it('exits 1 when auth.login throws', async () => { + vi.mocked(select).mockResolvedValueOnce('kimi-code'); + mockStatus.mockResolvedValue({ providers: [] }); mockLogin.mockRejectedValue(new Error('boom')); - const program = new Command('pythinker').exitOverride(); - registerLoginCommand(program); + await runLogin(['login']); + + const chunks = writtenChunks(); + expect(chunks.some((chunk: string) => chunk.includes('boom'))).toBe(true); + expect(exitSpy.mock.calls[0]?.[0]).toBe(1); + }); + + it('--provider skips the picker entirely and logs in directly', async () => { + mockStatus.mockResolvedValue({ providers: [] }); + mockLogin.mockResolvedValue({ providerName: 'managed:kimi-code', ok: true }); + + await runLogin(['login', '--provider', 'kimi-code']); + + expect(select).not.toHaveBeenCalled(); + expect(mockLogin).toHaveBeenCalledTimes(1); + expect(exitSpy.mock.calls[0]?.[0]).toBe(0); + }); + + it('--provider matches a display name case-insensitively', async () => { + mockStatus.mockResolvedValue({ providers: [] }); + mockLogin.mockResolvedValue({ providerName: 'managed:kimi-code', ok: true }); + + await runLogin(['login', '-p', 'kimi (oauth)']); + + expect(select).not.toHaveBeenCalled(); + expect(mockLogin).toHaveBeenCalledTimes(1); + expect(exitSpy.mock.calls[0]?.[0]).toBe(0); + }); + + it('--provider matches a catalog provider by its bare id', async () => { + // A catalog provider's option value carries the internal `catalog:` prefix + // and its label is a product name, so the id printed everywhere else — + // `deepseek` — used to match neither and the login failed outright. + mockStatus.mockResolvedValue({ providers: [] }); + mockGetConfig.mockResolvedValue({ providers: {}, models: {} }); + vi.mocked(fetchCatalog).mockResolvedValueOnce(catalogWithDeepSeek()); + vi.mocked(password).mockResolvedValue('sk-test-key'); + vi.mocked(select) + .mockResolvedValueOnce('deepseek/deepseek-chat') + .mockResolvedValueOnce('off'); + + await runLogin(['login', '--provider', 'DeepSeek API']); + + expect(select).not.toHaveBeenCalledWith( + expect.objectContaining({ message: 'Select a provider' }), + ); + expect(mockSetConfig).toHaveBeenCalled(); + expect(exitSpy.mock.calls[0]?.[0]).toBe(0); + + // The bare id resolves the same option the label does. + vi.mocked(select).mockReset(); + mockSetConfig.mockClear(); + exitSpy.mockClear(); + vi.mocked(fetchCatalog).mockResolvedValueOnce(catalogWithDeepSeek()); + vi.mocked(select) + .mockResolvedValueOnce('deepseek/deepseek-chat') + .mockResolvedValueOnce('off'); + + await runLogin(['login', '--provider', 'deepseek']); + + expect(select).not.toHaveBeenCalledWith( + expect.objectContaining({ message: 'Select a provider' }), + ); + expect(mockSetConfig).toHaveBeenCalled(); + expect(exitSpy.mock.calls[0]?.[0]).toBe(0); + }); - await expect(program.parseAsync(['node', 'pythinker', 'login'])).rejects.toThrow(ExitCalled); + it('persists the picked thinking effort, not just the on/off bit', async () => { + // The apply step writes config.thinking.effort, but the setConfig patch used + // to list only providers/models/defaultModel/defaultThinking — so the level + // never reached disk and every session reopened at the default. + mockStatus.mockResolvedValue({ providers: [] }); + mockGetConfig.mockResolvedValue({ providers: {}, models: {} }); + vi.mocked(fetchCatalog).mockResolvedValueOnce(catalogWithDeepSeek()); + vi.mocked(password).mockResolvedValue('sk-test-key'); + vi.mocked(select) + .mockResolvedValueOnce('deepseek/deepseek-chat') + .mockResolvedValueOnce('medium'); - const writtenChunks = stderrSpy.mock.calls.map((call: unknown[]) => String(call[0])); - expect(writtenChunks.some((chunk: string) => chunk.includes('boom'))).toBe(true); - expect(exitSpy).toHaveBeenCalledWith(1); + await runLogin(['login', '--provider', 'deepseek']); + + expect(mockSetConfig).toHaveBeenCalledWith( + expect.objectContaining({ + defaultThinking: true, + thinking: expect.objectContaining({ effort: 'medium' }), + }), + ); + expect(exitSpy.mock.calls[0]?.[0]).toBe(0); + }); + + it('--provider with an unknown value exits non-zero, names the input, and never runs the picker', async () => { + await runLogin(['login', '--provider', 'nope']); + + expect(select).not.toHaveBeenCalled(); + const chunks = writtenChunks(); + expect(chunks.some((chunk: string) => chunk.includes('Unknown provider "nope"'))).toBe(true); + // The valid ids follow, so the user can retry with a real value. + expect(chunks.some((chunk: string) => chunk.includes('kimi-code'))).toBe(true); + expect(exitSpy.mock.calls[0]?.[0]).toBe(1); + }); + + it('refuses to prompt when stdin is not a TTY and exits non-zero', async () => { + Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: false }); + + await runLogin(['login']); + + expect(select).not.toHaveBeenCalled(); + expect(mockLogin).not.toHaveBeenCalled(); + const chunks = writtenChunks(); + // The TTY gate is unconditional — --provider cannot rescue a non-interactive + // run, so the message must not imply that it can. + expect(chunks.some((chunk: string) => chunk.includes('interactive terminal'))).toBe(true); + expect(chunks.some((chunk: string) => chunk.includes('--provider'))).toBe(false); + expect(exitSpy.mock.calls[0]?.[0]).toBe(1); + }); + + it('exits 1 when the provider picker is cancelled, without writing config', async () => { + vi.mocked(select).mockResolvedValueOnce(CANCEL); + mockStatus.mockResolvedValue({ providers: [] }); + + await runLogin(['login']); + + expect(mockLogin).not.toHaveBeenCalled(); + expect(mockSetConfig).not.toHaveBeenCalled(); + expect(mockRemoveProvider).not.toHaveBeenCalled(); + const chunks = writtenChunks(); + expect(chunks.some((chunk: string) => chunk.includes('Login cancelled.'))).toBe(true); + expect(exitSpy.mock.calls[0]?.[0]).toBe(1); + }); + + // Every other case here drives the OAuth path. API-key login is the path most + // providers use — and the only one left once managed OAuth is gone — so its + // exit code needs its own cover. + it('exits 0 after a successful API-key login on an open platform', async () => { + mockStatus.mockResolvedValue({ providers: [] }); + mockGetConfig.mockResolvedValue({ providers: {}, models: {} }); + vi.mocked(password).mockResolvedValue('sk-test-key'); + mockFetchOpenPlatformModels.mockResolvedValue([ + { id: 'glm-4', name: 'GLM 4', contextLength: 128_000, supportsReasoning: true }, + ]); + // The model select's value is the `/` alias, not the bare id. + vi.mocked(select) + .mockResolvedValueOnce('glm-zai-coding/glm-4') + .mockResolvedValueOnce('medium'); + + await runLogin(['login', '--provider', 'glm-zai-coding']); + + // This path has its own setConfig patch, which omitted `thinking` the same + // way the catalog one did, so the picked level needs asserting here too. + expect(mockSetConfig).toHaveBeenCalledWith( + expect.objectContaining({ + defaultThinking: true, + thinking: expect.objectContaining({ effort: 'medium' }), + }), + ); + expect(exitSpy.mock.calls[0]?.[0]).toBe(0); }); }); diff --git a/apps/pythinker-code/test/tui/commands/auth.test.ts b/apps/pythinker-code/test/tui/commands/auth.test.ts index c1f7587d..d44bdfa1 100644 --- a/apps/pythinker-code/test/tui/commands/auth.test.ts +++ b/apps/pythinker-code/test/tui/commands/auth.test.ts @@ -125,3 +125,60 @@ describe('connectCatalogProvider credential acquisition', () => { expect(promptModelSelectionForCatalog).not.toHaveBeenCalled(); }); }); + +describe('OpenAI Codex login keeps the existing provider until it is replaced', () => { + afterEach(() => { + vi.resetModules(); + vi.doUnmock('@pythoughts/pythinker-code-oauth'); + }); + + it('leaves the configured provider intact when the model picker is cancelled', async () => { + const oauth = await import('@pythoughts/pythinker-code-oauth'); + vi.doMock('@pythoughts/pythinker-code-oauth', () => ({ + ...oauth, + runOpenAICodexOAuthFlow: vi.fn(async () => ({ + accessToken: 'access', + refreshToken: 'refresh', + accountId: 'account', + })), + fetchOpenAICodexModels: vi.fn(async () => [{ id: 'gpt-5-codex', name: 'GPT-5 Codex' }]), + })); + vi.resetModules(); + const { runLogin } = await import('@pythoughts/pythinker-code-sdk'); + const { OPENAI_CODEX_OAUTH_PLATFORM_ID, OPENAI_CODEX_PROVIDER_ID } = oauth; + + let config = { + providers: { [OPENAI_CODEX_PROVIDER_ID]: { apiKey: 'already-signed-in' } }, + } as unknown as PythinkerConfig; + const removeProvider = vi.fn(async (id: string) => { + delete config.providers[id]; + return config; + }); + const setConfig = vi.fn(); + + await runLogin({ + harness: { getConfig: async () => config, setConfig, removeProvider }, + cancelInFlight: undefined, + showStatus: vi.fn(), + showError: vi.fn(), + showLoginProgressSpinner: vi.fn(), + showLoginAuthorizationPrompt: vi.fn(), + promptPlatformSelection: async () => ({ + platformId: OPENAI_CODEX_OAUTH_PLATFORM_ID, + catalog: {}, + }), + promptApiKey: async () => undefined, + // The user backs out at the model picker — the most likely early return. + promptModelSelectionForOpenPlatform: async () => undefined, + promptModelSelectionForCatalog: async () => undefined, + refreshConfigAfterLogin: async () => undefined, + track: vi.fn(), + } as never); + + // Removing the provider before the replacement is certain would have + // signed the user out of a working Codex setup for nothing. + expect(removeProvider).not.toHaveBeenCalled(); + expect(config.providers[OPENAI_CODEX_PROVIDER_ID]).toBeDefined(); + expect(setConfig).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/pythinker-code/test/tui/commands/dynamic-workflow.test.ts b/apps/pythinker-code/test/tui/commands/dynamic-workflow.test.ts index 03eafaa8..3e2a6424 100644 --- a/apps/pythinker-code/test/tui/commands/dynamic-workflow.test.ts +++ b/apps/pythinker-code/test/tui/commands/dynamic-workflow.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { handleDynamicWorkflowCommand } from '#/tui/commands/index'; import type { SlashCommandHost } from '#/tui/commands/dispatch'; +import { setDynamicWorkflowDisabled } from '#/tui/commands/workflow-availability'; import { currentTheme } from '#/tui/theme'; const ENTER = '\r'; @@ -78,6 +79,23 @@ function expectDynamicWorkflowMarker(host: SlashCommandHost, text: string): void } describe('handleDynamicWorkflowCommand', () => { + it('refuses to run when Dynamic Workflow is disabled by configuration', async () => { + setDynamicWorkflowDisabled(true, {}); + + try { + const { host, session } = makeHost({ permissionMode: 'auto' }); + + await handleDynamicWorkflowCommand(host, 'Ship feature X'); + + expect(host.showError).toHaveBeenCalledWith('Dynamic Workflow is disabled by configuration.'); + expect(host.sendNormalUserInput).not.toHaveBeenCalled(); + expect(session.setDynamicWorkflowMode).not.toHaveBeenCalled(); + expect(markerAddChild(host)).not.toHaveBeenCalled(); + } finally { + setDynamicWorkflowDisabled(false, {}); + } + }); + it('sends the Dynamic Workflow prompt as a normal prompt after enabling Dynamic Workflow mode', async () => { const { host, session } = makeHost({ permissionMode: 'auto' }); diff --git a/apps/pythinker-code/test/tui/components/dialogs/platform-selector.test.ts b/apps/pythinker-code/test/tui/components/dialogs/platform-selector.test.ts index 41c4081a..a8957bcc 100644 --- a/apps/pythinker-code/test/tui/components/dialogs/platform-selector.test.ts +++ b/apps/pythinker-code/test/tui/components/dialogs/platform-selector.test.ts @@ -1,9 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import { - CATALOG_PLATFORM_VALUE_PREFIX, - PlatformSelectorComponent, -} from '#/tui/components/dialogs/platform-selector'; +import { CATALOG_PLATFORM_VALUE_PREFIX } from '@pythoughts/pythinker-code-sdk'; +import { PlatformSelectorComponent } from '#/tui/components/dialogs/platform-selector'; import { promptPlatformSelection } from '#/tui/commands/prompts'; const SGR = new RegExp(`${String.fromCodePoint(27)}\\[[0-9;]*m`, 'gu'); diff --git a/apps/pythinker-code/test/tui/components/messages/dynamic-workflow-mission-control.test.ts b/apps/pythinker-code/test/tui/components/messages/dynamic-workflow-mission-control.test.ts index b1428cbd..cb88c423 100644 --- a/apps/pythinker-code/test/tui/components/messages/dynamic-workflow-mission-control.test.ts +++ b/apps/pythinker-code/test/tui/components/messages/dynamic-workflow-mission-control.test.ts @@ -20,6 +20,9 @@ function renderText(component: DynamicWorkflowMissionControlComponent, width = 1 return strip(component.render(width).join('\n')); } +/** The STATE cell of a running row: a grey braille spinner frame, then the label. */ +const RUNNING_CELL = /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] RUN/u; + function memberLine(output: string, index: number): string { const id = String(index).padStart(3, '0'); const line = output.split('\n').find( @@ -35,11 +38,6 @@ function memberRowCount(output: string): number { ).length; } -function displayedPercent(output: string, index: number): number { - const match = /(\d+)%/u.exec(memberLine(output, index)); - if (match === null) throw new Error(`Missing percent for member ${String(index)}`); - return Number(match[1]); -} function aggregateLine(output: string): string { const line = output.split('\n').find((candidate) => @@ -123,6 +121,56 @@ describe('DynamicWorkflowMissionControlComponent', () => { } }); + it('ignores blank items so no phantom row waits forever', () => { + const component = createComponent(); + // The engine drops the blank before launching anything, so counting it here + // would leave a third row queued for good and pin the header at 2/3. + component.updateArgs({ items: ['Layout hierarchy', 'Interaction audit', ' '] }); + component.markInputComplete(); + register(component, 'agent-1'); + component.markStarted('agent-1'); + component.markCompleted('agent-1', 'Done one'); + register(component, 'agent-2'); + component.markStarted('agent-2'); + component.markCompleted('agent-2', 'Done two'); + + const output = renderText(component, 120); + expect(memberLine(output, 1)).toContain('✓ DONE'); + expect(memberLine(output, 2)).toContain('✓ DONE'); + // memberRowCount also counts activity lines, so assert the row's absence. + expect(() => memberLine(output, 3)).toThrow(/Missing Dynamic Workflow member 003/u); + expect(aggregateLine(output)).toContain('2/2 complete'); + }); + + it('still parses a result that carries the dropped-items note', () => { + // agent-core appends this note when it ignores blank items. It used to be + // prepended, which made the envelope regex miss and rendered a successful + // run as "Unsupported Dynamic Workflow result". + const result = [ + '', + 'completed: 1', + 'Layout hierarchy', + '', + 'Note: 1 empty item was ignored; the workflow ran without them.', + ].join('\n'); + + expect(dynamicWorkflowResultSummaryFromOutput(result)).toEqual({ + completed: 1, + failed: 0, + aborted: 0, + parsed: true, + }); + + const component = createComponent(); + component.updateArgs({ items: ['Layout hierarchy'] }); + component.markInputComplete(); + expect(component.applyResult(result)).toBe(true); + + const output = renderText(component, 120); + expect(memberLine(output, 1)).toContain('✓ DONE'); + expect(output).not.toContain('Unsupported'); + }); + it('decodes escaped XML fields and preserves literal closing-tag text', () => { const result = [ '', @@ -248,8 +296,10 @@ describe('DynamicWorkflowMissionControlComponent', () => { expect(aggregateLine(output)).toContain('2/3 complete'); expect(aggregateLine(output)).not.toMatch(/\b\d+%/u); expect(aggregateLine(output)).not.toContain('━'); - expect(memberLine(output, 1)).toMatch(/▏⣀⣀▕\s+20%\s+● RUN\s+Layout hierarchy/u); - expect(memberLine(output, 2)).toMatch(/▏⣿⣿▕\s+100%\s+✓ DONE\s+Interaction audit/u); + expect(memberLine(output, 1)).toMatch( + /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] RUN\s+Layout hierarchy/u, + ); + expect(memberLine(output, 2)).toMatch(/–\s+✓ DONE\s+Interaction audit/u); expect(output).not.toMatch(/[⣿⣷⣯⣟⡿⢿⣻⣽]{4,}/u); }); @@ -265,7 +315,7 @@ describe('DynamicWorkflowMissionControlComponent', () => { expect(output).toContain('Waiting for delegated agents'); expect(aggregateLine(output)).toMatch(/\b\d+s elapsed\b/); expect(aggregateLine(output)).not.toMatch(/\b\d+%/); - expect(memberLine(output, 1)).toContain('▏ ▕ 0%'); + expect(memberLine(output, 1)).toContain('0⚒'); expect(output).not.toMatch(/[⣿⣷⣯⣟⡿⢿⣻⣽]{4,}/u); expect(aggregateLine(output)).not.toContain('━'); }); @@ -309,6 +359,54 @@ describe('DynamicWorkflowMissionControlComponent', () => { } }); + it('spins a grey dot on running rows and shimmers Orchestrating in periwinkle', () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + const previousLevel = chalk.level; + const previousPalette = currentTheme.palette; + chalk.level = 3; + currentTheme.setPalette(darkColors); + + try { + const component = createComponent(); + component.updateArgs({ items: ['Layout hierarchy'] }); + component.markInputComplete(); + component.setActivitySpinnerText(() => '⠋'); + register(component, 'agent-1'); + component.markStarted('agent-1'); + + // memberLine expects stripped text; these assertions need the escapes, so + // the row is located by its stripped form and returned coloured. + const colouredMemberLine = (): string => { + const line = component.render(100).find( + (candidate) => strip(candidate).replace(/^│\s*/u, '').startsWith('001'), + ); + if (line === undefined) throw new Error('Missing Dynamic Workflow member 001'); + return line; + }; + + const first = colouredMemberLine(); + vi.setSystemTime(BRAILLE_SPINNER_INTERVAL_MS); + const second = colouredMemberLine(); + + // The dot is grey and it moves; the label keeps the panel's periwinkle. + expect(first).toContain(chalk.hex(darkColors.textDim)('⠋')); + expect(second).toContain(chalk.hex(darkColors.textDim)('⠙')); + expect(first).toContain(chalk.hex(darkColors.primary)('RUN')); + // The periwinkle it must NOT be: the old dot took the label's colour. + expect(first).not.toContain(chalk.hex(darkColors.primary)('●')); + + // Orchestrating shimmers periwinkle-on-periwinkle, not periwinkle-on-grey. + const aggregate = aggregateLine(component.render(100).join('\n')); + expect(aggregate).toContain(chalk.hex(darkColors.primary)('rchestrating')); + expect(aggregate).not.toContain(chalk.hex(darkColors.text)('rchestrating')); + } finally { + vi.useRealTimers(); + chalk.level = previousLevel; + currentTheme.setPalette(previousPalette); + } + }); + it('cancels the request without inventing terminal child states', () => { const component = createComponent(); component.updateArgs({ items: ['Running work', 'Queued work'] }); @@ -322,8 +420,8 @@ describe('DynamicWorkflowMissionControlComponent', () => { const output = renderText(component, 120); expect(output).toContain('– Cancelled'); - expect(memberLine(output, 1)).toMatch(/20%\s+● RUN\s+Running work/); - expect(memberLine(output, 2)).toMatch(/0%\s+◌ WAIT\s+Queued work/); + expect(memberLine(output, 1)).toMatch(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] RUN\s+Running work/u); + expect(memberLine(output, 2)).toMatch(/◌ WAIT\s+Queued work/); expect(output).not.toContain('– STOP'); expect(output).not.toContain('⠋ Orchestrating'); }); @@ -337,7 +435,7 @@ describe('DynamicWorkflowMissionControlComponent', () => { component.markFailed('agent-1', 'Late failure'); const output = renderText(component, 100); - expect(memberLine(output, 1)).toMatch(/100%\s+✓ DONE\s+Layout hierarchy/u); + expect(memberLine(output, 1)).toMatch(/✓ DONE\s+Layout hierarchy/u); expect(output).toContain('Finished first'); expect(output).not.toContain('Late failure'); }); @@ -357,10 +455,10 @@ describe('DynamicWorkflowMissionControlComponent', () => { ].join('\n')); const output = renderText(component, 120); - expect(memberLine(output, 1)).toMatch(/100%\s+✓ DONE\s+Observed first/u); + expect(memberLine(output, 1)).toMatch(/✓ DONE\s+Observed first/u); expect(output).toContain('Observed completion'); expect(output).not.toContain('Late result failure'); - expect(memberLine(output, 2)).toMatch(/100%\s+× FAIL\s+Result-only second/u); + expect(memberLine(output, 2)).toMatch(/× FAIL\s+Result-only second/u); expect(output).toContain('Result failure'); }); @@ -381,7 +479,7 @@ describe('DynamicWorkflowMissionControlComponent', () => { expect(memberLine(output, 1)).toContain('✓ DONE'); expect(output).not.toContain('002'); expect(output).not.toContain('Phantom failure'); - expect(output).not.toContain('200%'); + expect(output).not.toMatch(/\d+%/u); }); it('prefers a suspension detail over stale model progress in the member row', () => { @@ -394,7 +492,7 @@ describe('DynamicWorkflowMissionControlComponent', () => { component.markSuspended({ agentId: 'agent-1', reason: 'Rate limited' }); const output = renderText(component, 120); - expect(memberLine(output, 1)).toMatch(/50%\s+! HOLD\s+Throttle-sensitive work/); + expect(memberLine(output, 1)).toMatch(/! HOLD\s+Throttle-sensitive work/); expect(output).toContain('Rate limited'); expect(output).not.toContain('Stale model progress'); }); @@ -409,7 +507,7 @@ describe('DynamicWorkflowMissionControlComponent', () => { component.markFailed('agent-1', 'Provider exhausted'); const output = renderText(component, 120); - expect(memberLine(output, 1)).toMatch(/100%\s+× FAIL\s+Failure-sensitive work/); + expect(memberLine(output, 1)).toMatch(/× FAIL\s+Failure-sensitive work/); expect(output).toContain('Provider exhausted'); expect(output).not.toContain('Stale model progress'); }); @@ -451,7 +549,7 @@ describe('DynamicWorkflowMissionControlComponent', () => { it('renders every observed member and request phase without inventing lifecycle events', () => { const pending = createComponent(); pending.updateArgs({}, { streamingArguments: '{"items":["Pending work"' }); - expect(memberLine(renderText(pending, 100), 1)).toMatch(/0%\s+◌ PEND\s+Pending work/); + expect(memberLine(renderText(pending, 100), 1)).toMatch(/◌ PEND\s+Pending work/); const component = createComponent(); component.updateArgs({ @@ -468,9 +566,11 @@ describe('DynamicWorkflowMissionControlComponent', () => { component.markCancelled('agent-6'); const output = renderText(component, 140); - for (const token of ['◌ WAIT', '● RUN', '! HOLD', '✓ DONE', '× FAIL', '– STOP']) { + for (const token of ['◌ WAIT', '! HOLD', '✓ DONE', '× FAIL', '– STOP']) { expect(output).toContain(token); } + // Running is the one animated phase, so its symbol varies by frame. + expect(output).toMatch(RUNNING_CELL); expect(output).toContain('Orchestrating'); const failed = createComponent(); @@ -496,38 +596,146 @@ describe('DynamicWorkflowMissionControlComponent', () => { }, ); - it('advances member cubes through observed work stages', () => { - const component = createComponent(); - component.updateArgs({ items: ['Live work'] }); - component.markInputComplete(); - component.registerSubagent({ agentId: 'agent-1' }); + it('counts real work and shows how long a row has been silent', () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + try { + const component = createComponent(); + component.updateArgs({ items: ['Live work'] }); + component.markInputComplete(); + component.registerSubagent({ agentId: 'agent-1' }); + component.markStarted('agent-1'); + + // No percentage anywhere: nothing knows how many steps an agent will take. + expect(renderText(component, 100)).not.toMatch(/\b\d+%/u); + + component.recordToolCall({ agentId: 'agent-1', name: 'Read' }); + component.recordToolCall({ agentId: 'agent-1', name: 'Bash' }); + expect(memberLine(renderText(component, 100), 1)).toMatch(/2⚒\s+0s/u); + + // The old bar froze at 75% here; the idle age keeps moving instead. + vi.setSystemTime(45_000); + expect(memberLine(renderText(component, 100), 1)).toMatch(/2⚒\s+45s/u); + + // Any observed event resets the silence, tool call or streamed text. + component.appendModelDelta({ agentId: 'agent-1', delta: 'Summarizing' }); + expect(memberLine(renderText(component, 100), 1)).toMatch(/2⚒\s+0s/u); + + // A finished row has no idle age to report. + component.markCompleted('agent-1', 'Done'); + expect(memberLine(renderText(component, 100), 1)).toMatch(/2⚒\s+–\s+✓ DONE/u); + } finally { + vi.useRealTimers(); + } + }); - const expectProgress = (percent: number, fill: string) => { - const output = renderText(component, 100); - expect(memberLine(output, 1)).toContain( - `▏${fill.repeat(2)}▕ ${String(percent).padStart(3, ' ')}%`, + it('colours a silent row amber, then red once it has almost certainly stalled', () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + const previousLevel = chalk.level; + const previousPalette = currentTheme.palette; + chalk.level = 3; + currentTheme.setPalette(darkColors); + + try { + const component = createComponent(); + component.updateArgs({ items: ['Live work'] }); + component.markInputComplete(); + component.registerSubagent({ agentId: 'agent-1' }); + component.markStarted('agent-1'); + component.recordToolCall({ agentId: 'agent-1', name: 'Bash' }); + + const workCell = (): string => { + const line = component.render(100).find( + (candidate) => strip(candidate).replace(/^│\s*/u, '').startsWith('001'), + ); + if (line === undefined) throw new Error('Missing Dynamic Workflow member 001'); + return line; + }; + + expect(workCell()).toContain(chalk.hex(darkColors.textMuted)(' 0s')); + vi.setSystemTime(DYNAMIC_WORKFLOW_RENDERING.quietIdleMs); + expect(workCell()).toContain(chalk.hex(darkColors.warning)(' 60s')); + vi.setSystemTime(DYNAMIC_WORKFLOW_RENDERING.stalledIdleMs); + expect(workCell()).toContain(chalk.hex(darkColors.error)('180s')); + } finally { + vi.useRealTimers(); + chalk.level = previousLevel; + currentTheme.setPalette(previousPalette); + } + }); + + it('keeps a suspended row muted however long it stays silent', () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + const previousLevel = chalk.level; + const previousPalette = currentTheme.palette; + chalk.level = 3; + currentTheme.setPalette(darkColors); + + try { + const component = createComponent(); + component.updateArgs({ items: ['Held work'] }); + component.markInputComplete(); + component.registerSubagent({ agentId: 'agent-1' }); + component.markStarted('agent-1'); + // The last event lands a minute in, so the idle age and the elapsed age + // read as different numbers and the assertion cannot match the wrong cell. + vi.setSystemTime(60_000); + component.recordToolCall({ agentId: 'agent-1', name: 'Bash' }); + component.markSuspended({ agentId: 'agent-1', reason: 'Waiting for approval' }); + + // A suspended agent waits on the user by design, so its silence is not a + // stall and must never borrow the alarm colours. + vi.setSystemTime(60_000 + DYNAMIC_WORKFLOW_RENDERING.stalledIdleMs * 2); + const line = component.render(100).find( + (candidate) => strip(candidate).replace(/^│\s*/u, '').startsWith('001'), ); - return output; - }; + if (line === undefined) throw new Error('Missing Dynamic Workflow member 001'); + expect(strip(line)).toContain('1⚒ 360s'); + expect(line).toContain(chalk.hex(darkColors.textMuted)('360s')); + } finally { + vi.useRealTimers(); + chalk.level = previousLevel; + currentTheme.setPalette(previousPalette); + } + }); - expectProgress(0, ' '); - component.markStarted('agent-1'); - expectProgress(20, '⣀'); - component.appendModelDelta({ agentId: 'agent-1', delta: 'Inspecting' }); - expectProgress(50, '⣤'); - component.appendModelDelta({ agentId: 'agent-1', delta: ' more' }); - expectProgress(50, '⣤'); - component.recordToolCall({ agentId: 'agent-1', name: 'Read' }); - expectProgress(75, '⣶'); - component.appendModelDelta({ agentId: 'agent-1', delta: 'Summarizing' }); - const activeOutput = expectProgress(75, '⣶'); - expect(aggregateLine(activeOutput)).toContain('0/1 complete'); - expect(aggregateLine(activeOutput)).not.toMatch(/\b\d+%/u); - expect(aggregateLine(activeOutput)).not.toContain('━'); + it('never marks a row that has not started as stalled', () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + const previousLevel = chalk.level; + const previousPalette = currentTheme.palette; + chalk.level = 3; + currentTheme.setPalette(darkColors); - component.markCompleted('agent-1', 'Done'); - const completedOutput = expectProgress(100, '⣿'); - expect(aggregateLine(completedOutput)).toContain('1/1 complete'); + try { + const component = createComponent(); + component.updateArgs({ items: ['First', 'Second'] }); + component.markInputComplete(); + component.registerSubagent({ agentId: 'agent-1' }); + component.registerSubagent({ agentId: 'agent-2' }); + component.markStarted('agent-1'); + + // A queued row waits behind the concurrency limit; its clock would run + // from the launch of the workflow, so a long queue used to paint every + // waiting row red while nothing was wrong. + vi.setSystemTime(DYNAMIC_WORKFLOW_RENDERING.stalledIdleMs * 2); + const queued = memberLine(renderText(component, 100), 2); + expect(queued).toMatch(/0⚒\s+–\s+◌ WAIT/u); + expect(queued).not.toMatch(/\d+s/u); + + const queuedRaw = component.render(100).find( + (candidate) => strip(candidate).replace(/^│\s*/u, '').startsWith('002'), + ); + expect(queuedRaw).toContain(chalk.hex(darkColors.textMuted)(' –')); + // The running row still reports its silence, so the alarm is not simply gone. + expect(memberLine(renderText(component, 100), 1)).toMatch(/0⚒\s+\d+s/u); + } finally { + vi.useRealTimers(); + chalk.level = previousLevel; + currentTheme.setPalette(previousPalette); + } }); it('reports aggregate completion counts without estimating overall progress', () => { @@ -559,7 +767,7 @@ describe('DynamicWorkflowMissionControlComponent', () => { const lines = renderText(component, 100).split('\n'); expect(lines).toHaveLength(6); - expect(memberLine(lines.join('\n'), 1)).toMatch(/0%\s+◌ WAIT\s+One/u); + expect(memberLine(lines.join('\n'), 1)).toMatch(/◌ WAIT\s+One/u); expect(lines.join('\n')).toContain('+ 4 more agents'); expect(lines.join('\n')).not.toContain('Recent activity'); }); @@ -596,49 +804,42 @@ describe('DynamicWorkflowMissionControlComponent', () => { const rendered = component.render(width); const output = strip(rendered.join('\n')); - const showsProgress = width >= 64; + const showsWork = width >= 64; expect(rendered.every((line) => visibleWidth(line) <= width)).toBe(true); - expect(memberLine(output, 1)).toContain('● RUN'); - expect(memberLine(output, 1).includes('▏⣀⣀▕ 20%')).toBe(showsProgress); - expect(output.includes('PROGRESS')).toBe(showsProgress); + expect(memberLine(output, 1)).toMatch(RUNNING_CELL); + // The work cell is the first thing dropped when the frame gets narrow. + expect(/\d⚒/u.test(memberLine(output, 1))).toBe(showsWork); + expect(output.includes('WORK IDLE')).toBe(showsWork); }, ); - it('holds the observed stage across streamed deltas and completes only on the terminal event', () => { - const component = createComponent(); - component.updateArgs({ items: ['Long streaming work'] }); - component.markInputComplete(); - register(component, 'agent-1'); - component.markStarted('agent-1'); - component.recordToolCall({ agentId: 'agent-1', name: 'Read' }); - - for (let index = 0; index < 200; index += 1) { - component.appendModelDelta({ agentId: 'agent-1', delta: `chunk ${String(index)} ` }); - } - // No invented progress: text after a tool call never climbs toward 100. - expect(displayedPercent(renderText(component, 100), 1)) - .toBe(DYNAMIC_WORKFLOW_RENDERING.toolActivityProgress); - - component.markCompleted('agent-1', 'Done'); - expect(displayedPercent(renderText(component, 100), 1)).toBe(100); - }); + it('counts tool calls as work and streamed text only as liveness', () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + try { + const component = createComponent(); + component.updateArgs({ items: ['Long streaming work'] }); + component.markInputComplete(); + register(component, 'agent-1'); + component.markStarted('agent-1'); + component.recordToolCall({ agentId: 'agent-1', name: 'Read' }); + + vi.setSystemTime(30_000); + for (let index = 0; index < 200; index += 1) { + component.appendModelDelta({ agentId: 'agent-1', delta: `chunk ${String(index)} ` }); + } - it('keeps streamed text at the model stage until a tool call lifts it', () => { - const component = createComponent(); - component.updateArgs({ items: ['Chatty work'] }); - component.markInputComplete(); - register(component, 'agent-1'); - component.markStarted('agent-1'); + // 200 deltas are not 200 units of work — the count tracks tool calls only. + // But they prove the agent is alive, so the idle age resets. + const line = memberLine(renderText(component, 100), 1); + expect(line).toMatch(/1⚒\s+0s/u); + expect(renderText(component, 100)).not.toMatch(/\b\d+%/u); - for (let index = 0; index < 300; index += 1) { - component.appendModelDelta({ agentId: 'agent-1', delta: 'more ' }); + component.recordToolCall({ agentId: 'agent-1', name: 'Bash' }); + expect(memberLine(renderText(component, 100), 1)).toMatch(/2⚒/u); + } finally { + vi.useRealTimers(); } - expect(displayedPercent(renderText(component, 100), 1)) - .toBe(DYNAMIC_WORKFLOW_RENDERING.modelActivityProgress); - - component.recordToolCall({ agentId: 'agent-1', name: 'Read' }); - expect(displayedPercent(renderText(component, 100), 1)) - .toBe(DYNAMIC_WORKFLOW_RENDERING.toolActivityProgress); }); it('starts a new line for model text after a tool label instead of fusing them', () => { @@ -721,7 +922,7 @@ describe('DynamicWorkflowMissionControlComponent', () => { component.markCompleted('agent-2', 'Done'); const output = renderText(component, 100); - expect(output).toContain('● RUN'); + expect(output).toMatch(RUNNING_CELL); expect(output).toContain('Orchestrating'); expect(output).not.toContain('Finalizing'); diff --git a/apps/pythinker-code/test/tui/parity/feature-matrix.ts b/apps/pythinker-code/test/tui/parity/feature-matrix.ts index f83a2be5..07557788 100644 --- a/apps/pythinker-code/test/tui/parity/feature-matrix.ts +++ b/apps/pythinker-code/test/tui/parity/feature-matrix.ts @@ -81,6 +81,7 @@ const SESSION_EVENTS = [ 'skill.activated', 'error', 'warning', + 'workflow.warning', 'compaction.started', 'compaction.completed', 'compaction.blocked', diff --git a/apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts b/apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts index 3c219d92..465724e9 100644 --- a/apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts +++ b/apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts @@ -1841,7 +1841,7 @@ command = "vim" ); transcript = stripSgr(renderTranscript(driver)); - expect(transcript).toMatch(/001\s+▏ ▕\s+0%\s+◌ WAIT\s+Fresh work/u); + expect(transcript).toMatch(/001\s+0⚒\s+–\s+◌ WAIT\s+Fresh work/u); expect(transcript).not.toContain('Late completion from undone work'); }); @@ -4005,8 +4005,9 @@ command = "vim" const transcript = stripSgr(renderTranscript(driver)); expect(transcript).toContain('Dynamic Workflow'); - expect(transcript).toMatch(/001\s+▏⣤⣤▕\s+50%\s+● RUN\s+src\/a.ts/u); - expect(transcript).toMatch(/002\s+▏⣿⣿▕\s+100%\s+✓ DONE\s+src\/b.ts/u); + // The running row spins a grey braille dot, so its symbol varies by frame. + expect(transcript).toMatch(/001\s+\d+⚒\s+\d+s\s+[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] RUN\s+src\/a.ts/u); + expect(transcript).toMatch(/002\s+\d+⚒\s+–\s+✓ DONE\s+src\/b.ts/u); expect(transcript).toMatch(/Orchestrating\s+1\/2 complete/u); expect(transcript).not.toContain('━'); expect(transcript).toContain('Completed before spawn'); @@ -4014,6 +4015,50 @@ command = "vim" expect(driver.streamingUI.getToolComponent('call_dynamic_workflow')).toBeUndefined(); }); + it('surfaces a workflow.warning on the live Dynamic Workflow mission control', async () => { + const { driver } = await makeDriver(); + const sendQueued = vi.fn(); + const dispatch = (event: Event): void => driver.sessionEventHandler.handleEvent(event, sendQueued); + + dispatch({ + type: 'tool.call.started', agentId: 'main', sessionId: 'ses-1', turnId: 1, + toolCallId: 'call_warn_workflow', name: 'DynamicWorkflow', + args: { description: 'Review changed files', items: ['src/a.ts', 'src/b.ts'] }, + } as Event); + dispatch({ + type: 'workflow.warning', agentId: 'main', sessionId: 'ses-1', + workflowRunId: 'run-1', parentToolCallId: 'call_warn_workflow', + agentCount: 12, threshold: 8, + message: 'This Dynamic Workflow will launch 12 subagents, above the advisory ceiling of 8; the run is proceeding anyway.', + } as Event); + + const transcript = stripSgr(renderTranscript(driver)); + expect(transcript).toContain('Dynamic Workflow'); + expect(transcript).toContain('12 subagents'); + expect(transcript).toContain('advisory ceiling of 8'); + }); + + it('falls back to the status line when a workflow.warning has no mission control', async () => { + const { driver } = await makeDriver(); + const sendQueued = vi.fn(); + const showStatus = vi + .spyOn(driver as unknown as { showStatus: (message: string, color?: unknown) => void }, 'showStatus') + .mockImplementation(() => {}); + const dispatch = (event: Event): void => driver.sessionEventHandler.handleEvent(event, sendQueued); + + dispatch({ + type: 'workflow.warning', agentId: 'main', sessionId: 'ses-1', + workflowRunId: 'run-1', parentToolCallId: 'call_retired_workflow', + agentCount: 12, threshold: 8, + message: 'This Dynamic Workflow will launch 12 subagents, above the advisory ceiling of 8; the run is proceeding anyway.', + } as Event); + + expect(showStatus).toHaveBeenCalledWith( + 'This Dynamic Workflow will launch 12 subagents, above the advisory ceiling of 8; the run is proceeding anyway.', + 'warning', + ); + }); + it('mounts the framed workflow on the first named delta before the denominator is known', async () => { const { driver } = await makeDriver(makeSession(), {}, 'fixed'); driver.state.editorContainer.addChild(driver.state.editor); @@ -4047,7 +4092,7 @@ command = "vim" transcript = stripSgr(renderTranscript(driver)); expect(transcript).toContain('0/2 complete'); - expect(transcript).toMatch(/001\s+▏ ▕\s+0%\s+◌ WAIT\s+src\/a.ts/u); + expect(transcript).toMatch(/001\s+0⚒\s+–\s+◌ WAIT\s+src\/a.ts/u); }); it('keeps terminal Dynamic Workflow results static and does not fabricate child failures', async () => { @@ -4074,8 +4119,8 @@ command = "vim" const transcript = stripSgr(renderTranscript(driver)); expect(transcript).toContain('✓ Completed'); - expect(transcript).toMatch(/001\s+▏⣿⣿▕\s+100%\s+✓ DONE\s+src\/a.ts/u); - expect(transcript).toMatch(/002\s+▏⣿⣿▕\s+100%\s+× FAIL\s+src\/b.ts/u); + expect(transcript).toMatch(/001\s+\d+⚒\s+–\s+✓ DONE\s+src\/a.ts/u); + expect(transcript).toMatch(/002\s+\d+⚒\s+–\s+× FAIL\s+src\/b.ts/u); expect(transcript).toContain('Agent timed out after 30s.'); expect(transcript).not.toContain('⠋ Orchestrating'); }); @@ -4116,7 +4161,7 @@ command = "vim" } as Event); const transcript = stripSgr(renderTranscript(driver)); - expect(transcript).toMatch(/001\s+▏ ▕\s+0%\s+◌ WAIT\s+src\/fresh.ts/u); + expect(transcript).toMatch(/001\s+0⚒\s+–\s+◌ WAIT\s+src\/fresh.ts/u); expect(transcript).not.toContain('must not leak'); }, ); @@ -4209,7 +4254,7 @@ command = "vim" } as Event); const transcript = stripSgr(renderTranscript(driver)); - expect(transcript).toMatch(/001\s+▏⣿⣿▕\s+100%\s+× FAIL\s+src\/generic.ts/u); + expect(transcript).toMatch(/001\s+\d+⚒\s+–\s+× FAIL\s+src\/generic.ts/u); expect(transcript).toContain('Early generic failure'); }); @@ -4235,7 +4280,7 @@ command = "vim" const transcript = stripSgr(renderTranscript(driver)); expect(transcript).toContain('× Failed'); - expect(transcript).toMatch(/001\s+▏⣿⣿▕\s+100%\s+✓ DONE\s+src\/a.ts/u); + expect(transcript).toMatch(/001\s+\d+⚒\s+–\s+✓ DONE\s+src\/a.ts/u); expect(transcript).toContain('Child completed before request error'); }); diff --git a/apps/pythinker-code/test/tui/pythinker-tui-startup.test.ts b/apps/pythinker-code/test/tui/pythinker-tui-startup.test.ts index b710340b..606d795b 100644 --- a/apps/pythinker-code/test/tui/pythinker-tui-startup.test.ts +++ b/apps/pythinker-code/test/tui/pythinker-tui-startup.test.ts @@ -7,7 +7,11 @@ import { resetCapabilitiesCache, setCapabilities, } from '@earendil-works/pi-tui'; -import { log, type GoalSnapshot } from '@pythoughts/pythinker-code-sdk'; +import { + CATALOG_PLATFORM_VALUE_PREFIX, + log, + type GoalSnapshot, +} from '@pythoughts/pythinker-code-sdk'; import type { MigrationPlan } from '@pythoughts/migration-legacy'; import { describe, expect, it, vi } from 'vitest'; @@ -20,7 +24,6 @@ import { promptPlatformSelection, promptLogoutProviderSelection, } from '#/tui/commands/prompts'; -import { CATALOG_PLATFORM_VALUE_PREFIX } from '#/tui/components/dialogs/platform-selector'; import { BannerComponent } from '#/tui/components/chrome/banner'; import { WelcomeComponent } from '#/tui/components/chrome/welcome'; import { DEFAULT_STATUS_LINE_CONFIG } from '#/tui/config'; diff --git a/apps/pythinker-code/test/tui/utils/thinking-levels.test.ts b/apps/pythinker-code/test/tui/utils/thinking-levels.test.ts index 18fbbb1e..59c40c55 100644 --- a/apps/pythinker-code/test/tui/utils/thinking-levels.test.ts +++ b/apps/pythinker-code/test/tui/utils/thinking-levels.test.ts @@ -1,113 +1,6 @@ -import type { ModelAlias } from '@pythoughts/pythinker-code-sdk'; import { describe, expect, it } from 'vitest'; -import { - coerceEffortForModel, - effortColorToken, - effortLevelsForModel, - shortEffortLabel, -} from '#/tui/utils/thinking-levels'; - -function model(overrides: Partial = {}): ModelAlias { - return { - provider: 'managed:kimi-code', - model: 'k2', - maxContextSize: 200_000, - capabilities: ['thinking'], - ...overrides, - } as ModelAlias; -} - -describe('effortLevelsForModel', () => { - it('falls back to low/medium/high without supportEfforts metadata', () => { - expect(effortLevelsForModel(model())).toEqual(['off', 'low', 'medium', 'high']); - expect(effortLevelsForModel(undefined)).toEqual(['off', 'low', 'medium', 'high']); - }); - - it('uses declared supportEfforts in canonical order, filtering unknown values', () => { - expect( - effortLevelsForModel( - model({ supportEfforts: ['max', 'none', 'low', 'bogus', 'minimal', 'high'] }), - ), - ).toEqual(['off', 'minimal', 'low', 'high', 'max']); - }); - - it('uses one fixed level for explicit empty effort metadata', () => { - expect( - effortLevelsForModel( - model({ capabilities: ['always_thinking'], supportEfforts: [] }), - ), - ).toEqual(['high']); - }); - - it('falls back when supportEfforts only contains unknown values', () => { - expect(effortLevelsForModel(model({ supportEfforts: ['bogus'] }))).toEqual([ - 'off', - 'low', - 'medium', - 'high', - ]); - }); - - it('returns only off for unsupported models', () => { - expect(effortLevelsForModel(model({ capabilities: ['tool_use'] }))).toEqual(['off']); - }); - - it('excludes off for always-on models', () => { - expect( - effortLevelsForModel(model({ capabilities: ['always_thinking'], supportEfforts: ['high'] })), - ).toEqual(['high']); - }); - - it('treats adaptiveThinking models as toggleable', () => { - expect(effortLevelsForModel(model({ capabilities: [], adaptiveThinking: true }))).toEqual([ - 'off', - 'low', - 'medium', - 'high', - ]); - }); -}); - -describe('coerceEffortForModel', () => { - it('passes through supported levels', () => { - expect(coerceEffortForModel(model(), 'medium')).toBe('medium'); - expect(coerceEffortForModel(model(), 'off')).toBe('off'); - }); - - it('clamps an unsupported level down to the nearest lower one', () => { - expect(coerceEffortForModel(model({ supportEfforts: ['low', 'high'] }), 'max')).toBe('high'); - expect(coerceEffortForModel(model({ supportEfforts: ['low', 'high'] }), 'medium')).toBe('low'); - }); - - it('falls back to the first level when nothing lower is supported', () => { - expect(coerceEffortForModel(model({ supportEfforts: ['max'] }), 'low')).toBe('off'); - expect( - coerceEffortForModel( - model({ capabilities: ['always_thinking'], supportEfforts: ['max'] }), - 'low', - ), - ).toBe('max'); - }); - - it('maps always-on + off to the first supported level', () => { - expect( - coerceEffortForModel( - model({ capabilities: ['always_thinking'], supportEfforts: ['high', 'max'] }), - 'off', - ), - ).toBe('high'); - }); - - it('maps unsupported models to off', () => { - expect(coerceEffortForModel(model({ capabilities: ['tool_use'] }), 'high')).toBe('off'); - }); - - it('maps the legacy on value to high before clamping', () => { - expect(coerceEffortForModel(model(), 'on')).toBe('high'); - expect(coerceEffortForModel(model({ supportEfforts: ['low'] }), 'on')).toBe('low'); - }); -}); +import { effortColorToken, shortEffortLabel } from '#/tui/utils/thinking-levels'; describe('effortColorToken', () => { it.each([ diff --git a/apps/pythinker-web/src/api/daemon/agentEventProjector.ts b/apps/pythinker-web/src/api/daemon/agentEventProjector.ts index 994dea5c..e1a93ede 100644 --- a/apps/pythinker-web/src/api/daemon/agentEventProjector.ts +++ b/apps/pythinker-web/src/api/daemon/agentEventProjector.ts @@ -1063,6 +1063,14 @@ export function createAgentProjector(): AgentProjector { break; } + case 'workflow.warning': { + out.push({ + type: 'unknown', + raw: { _agentWarning: true, message: p?.message }, + }); + break; + } + // ----------------------------------------------------------------------- // Background tasks (e.g. a backgrounded Bash command). Real daemon shape: // payload.info = { taskId, description, status, startedAt(ms), endedAt, @@ -1255,6 +1263,7 @@ const KNOWN_AGENT_CORE_TYPES = new Set([ 'subagent.suspended', 'subagent.completed', 'subagent.failed', + 'workflow.warning', 'background.task.started', 'background.task.terminated', ]); diff --git a/apps/pythinker-web/test/subagent-goal.test.ts b/apps/pythinker-web/test/subagent-goal.test.ts index d1509b9c..b6a7b8c2 100644 --- a/apps/pythinker-web/test/subagent-goal.test.ts +++ b/apps/pythinker-web/test/subagent-goal.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createAgentProjector } from '../src/api/daemon/agentEventProjector'; +import { classifyFrame, createAgentProjector } from '../src/api/daemon/agentEventProjector'; import { createInitialState, reduceAppEvent } from '../src/api/daemon/eventReducer'; import { toAppEvent } from '../src/api/daemon/mappers'; @@ -218,4 +218,37 @@ describe('subagent and goal projection', () => { }), })); }); + + it('projects workflow.warning through the agent-warning path and recognises the raw type', () => { + const projector = createAgentProjector(); + const sid = 'ses_1'; + + const events = projector.project('workflow.warning', { + workflowRunId: 'run_1', + parentToolCallId: 'tc_agent', + agentCount: 12, + threshold: 8, + message: 'This Dynamic Workflow will launch 12 subagents, above the advisory ceiling of 8; the run is proceeding anyway.', + }, sid); + expect(events).toEqual([ + { + type: 'unknown', + raw: { + _agentWarning: true, + message: 'This Dynamic Workflow will launch 12 subagents, above the advisory ceiling of 8; the run is proceeding anyway.', + }, + }, + ]); + + // KNOWN_AGENT_CORE_TYPES: raw (unprefixed) and "event."-prefixed frames + // both route to the agent projector. + expect(classifyFrame('workflow.warning', {})).toEqual({ + route: 'agent', + agentType: 'workflow.warning', + }); + expect(classifyFrame('event.workflow.warning', {})).toEqual({ + route: 'agent', + agentType: 'workflow.warning', + }); + }); }); diff --git a/apps/vscode/shared/legacy-sdk.ts b/apps/vscode/shared/legacy-sdk.ts index 49e4b6e5..ce07bfdf 100644 --- a/apps/vscode/shared/legacy-sdk.ts +++ b/apps/vscode/shared/legacy-sdk.ts @@ -144,6 +144,14 @@ export interface SubagentStatusPayload { result_summary?: string; } +export interface WorkflowWarningPayload { + parent_tool_call_id: string; + workflow_run_id: string; + agent_count: number; + threshold: number; + message: string; +} + export type LegacyWireEvent = | { type: 'TurnBegin'; payload: TurnBegin & { forkable?: boolean } } | { type: 'TurnEnd'; payload: Record } @@ -159,6 +167,7 @@ export type LegacyWireEvent = | { type: 'SteerInput'; payload: { user_input: string | ContentPart[] } } | { type: 'SubagentEvent'; payload: SubagentEvent } | { type: 'SubagentStatus'; payload: SubagentStatusPayload } + | { type: 'WorkflowWarning'; payload: WorkflowWarningPayload } | { type: string; payload: unknown }; export type StreamEvent = diff --git a/apps/vscode/src/auth/vscode-login-ui.ts b/apps/vscode/src/auth/vscode-login-ui.ts new file mode 100644 index 00000000..292ed1df --- /dev/null +++ b/apps/vscode/src/auth/vscode-login-ui.ts @@ -0,0 +1,263 @@ +/** + * VS Code renderer for the multi-provider login flows (`runLogin` from + * `@pythoughts/pythinker-code-sdk`). + * + * The extension host builds a `LoginUi` backed by VS Code's own UI: the + * provider picker, API-key input box, model and effort selection, and the + * OAuth device-code prompt are rendered with `vscode.window` widgets, and + * browsers open through `vscode.env.openExternal` instead of shelling out. + * Catalog resolution mirrors the terminal renderer's + * `promptPlatformSelection` — bundled catalog seed first, live fetch with a + * bundled fallback, so login still works offline. + * + * The `ManagedKimiCodeModelInfo` / `OpenPlatformDefinition` / + * `DeviceAuthorization` types live in `@pythoughts/pythinker-code-oauth`, + * which the extension does not depend on, so the port members are written + * inline and contextually typed against `LoginUi`. Effort levels come from the + * SDK's shared rule, reached through the two model-alias converters, so this + * renderer offers exactly the levels the terminal renderer offers. + */ + +import * as vscode from "vscode"; + +import { + buildPlatformOptions, + catalogModelToAlias, + DEFAULT_CATALOG_URL, + effortLevelsForModel, + fetchCatalog, + formatErrorMessage, + loadBuiltInCatalog, + managedModelToAlias, + type ApiKeyPromptOptions, + type LoginProgressSpinnerHandle, + type LoginUi, +} from "@pythoughts/pythinker-code-sdk"; + +import { Events } from "../../shared/bridge"; +import type { HandlerContext } from "../handlers/types"; + +// Filled by the tsdown define in release builds (same env var the CLI's +// release build reads). Source stays empty so the generated models.dev +// snapshot is not committed; `typeof` keeps dev builds safe when the +// identifier is not replaced. +declare const __PYTHINKER_CODE_BUILT_IN_CATALOG__: string | undefined; + +const BUILT_IN_CATALOG_JSON: string | undefined = + typeof __PYTHINKER_CODE_BUILT_IN_CATALOG__ === "string" + ? __PYTHINKER_CODE_BUILT_IN_CATALOG__ + : undefined; + +const CATALOG_FETCH_TIMEOUT_MS = 10_000; + +/** One quick pick for the effort level, from a list of supported levels. */ +async function promptEffortLevel( + levels: readonly string[], + token: vscode.CancellationToken, +): Promise { + const selected = await vscode.window.showQuickPick( + levels.map((level) => ({ label: level })), + { title: "Select effort level", placeHolder: "Select effort level", ignoreFocusOut: true }, + token, + ); + return selected?.label; +} + +/** + * A `LoginProgressSpinnerHandle` backed by a notification progress bar. The + * `withProgress` task resolves when `stop` is called; the final message is + * shown as a toast so the outcome stays visible after the notification ends. + */ +function createProgressHandle(title: string): LoginProgressSpinnerHandle { + let resolveFinished: (() => void) | undefined; + const finished = new Promise((resolve) => { + resolveFinished = resolve; + }); + // Deliberately not cancellable: these track one step of a login, but the only + // thing a cancel could do is abort the whole flow, and a Cancel button that + // silently means more than its label is worse than none. The login-wide + // progress notification the handler opens owns cancellation for every step. + void vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title, cancellable: false }, + () => finished, + ); + let settled = false; + return { + stop({ ok, label }) { + if (settled) return; + settled = true; + if (ok) { + void vscode.window.showInformationMessage(label); + } else { + void vscode.window.showErrorMessage(label); + } + resolveFinished?.(); + }, + }; +} + +/** + * Build a `LoginUi` for the extension host, rendering every prompt with VS + * Code's own widgets. The webview keeps receiving the OAuth device URL it + * already renders (`Events.LoginUrl`), so the pending-login screen still works. + * + * `token` is the login-wide cancellation token owned by the caller. Every quick + * pick and input box receives it, so cancelling closes whichever prompt is open + * instead of leaving the user staring at a widget the flow has already given + * up on; network waits are cancelled through `cancelInFlight`. + */ +export function createVscodeLoginUi( + ctx: HandlerContext, + token: vscode.CancellationToken, +): LoginUi { + let cancelInFlight: (() => void) | undefined; + + function openBrowser(url: string): void { + void vscode.env.openExternal(vscode.Uri.parse(url)); + } + + function showStatus(message: string): void { + void vscode.window.showInformationMessage(message); + } + + function showError(message: string): void { + void vscode.window.showErrorMessage(message); + } + + function showLoginProgressSpinner(label: string): LoginProgressSpinnerHandle { + return createProgressHandle(label); + } + + const ui: LoginUi = { + harness: ctx.harness, + get cancelInFlight() { + return cancelInFlight; + }, + set cancelInFlight(value: (() => void) | undefined) { + cancelInFlight = value; + }, + openBrowser, + showStatus, + showError, + showLoginProgressSpinner, + showLoginAuthorizationPrompt(auth) { + const url = auth.verificationUriComplete || auth.verificationUri; + ctx.broadcast(Events.LoginUrl, { url }, ctx.webviewId); + openBrowser(url); + return createProgressHandle(`Waiting for authorization — enter code ${auth.userCode}`); + }, + async promptPlatformSelection() { + let catalog = loadBuiltInCatalog(BUILT_IN_CATALOG_JSON) ?? {}; + const controller = new AbortController(); + const cancel = (): void => { + controller.abort(); + }; + cancelInFlight = cancel; + const catalogSpinner = showLoginProgressSpinner("Loading provider catalog"); + try { + // The editor progress notification is not cancellable, so a hung + // request would strand the login with no way out. The bundled catalog + // is a working fallback, so time the fetch out rather than wait. + catalog = await fetchCatalog( + DEFAULT_CATALOG_URL, + AbortSignal.any([controller.signal, AbortSignal.timeout(CATALOG_FETCH_TIMEOUT_MS)]), + ); + catalogSpinner.stop({ ok: true, label: "Provider catalog loaded." }); + } catch (error) { + if (controller.signal.aborted) { + catalogSpinner.stop({ ok: false, label: "Aborted." }); + return undefined; + } + // A benign fallback, not a failure: one informational toast, because in + // an editor every `stop` is a popup rather than an inline spinner step. + catalogSpinner.stop({ + ok: true, + label: `Using bundled provider catalog (live catalog unavailable: ${formatErrorMessage(error)}).`, + }); + } finally { + if (cancelInFlight === cancel) cancelInFlight = undefined; + } + + const options = buildPlatformOptions(catalog); + const items = options.map((option) => ({ + label: option.label, + description: option.description, + value: option.value, + })); + const selected = await vscode.window.showQuickPick( + items, + { title: "Select a provider", placeHolder: "Select a provider", ignoreFocusOut: true }, + token, + ); + if (selected === undefined) return undefined; + return { platformId: selected.value, catalog }; + }, + async promptApiKey(platformName, subtitleLines, promptOptions: ApiKeyPromptOptions = {}) { + const emptyMessage = promptOptions.emptyMessage ?? "API key cannot be empty."; + return vscode.window.showInputBox( + { + title: promptOptions.title ?? `Enter API key for ${platformName}`, + // An input box prompt is a single line, so newlines would collapse. + prompt: subtitleLines?.join(" — "), + password: promptOptions.secret !== false, + ignoreFocusOut: true, + validateInput: (input: string) => (input.length === 0 ? emptyMessage : undefined), + }, + token, + ); + }, + async promptModelSelectionForOpenPlatform(models, platform) { + const items = models.map((model) => ({ + label: model.displayName ?? model.id, + model, + })); + const selected = await vscode.window.showQuickPick( + items, + { + title: `Select a model for ${platform.name}`, + placeHolder: "Select a model", + ignoreFocusOut: true, + }, + token, + ); + if (selected === undefined) return undefined; + const effort = await promptEffortLevel( + effortLevelsForModel(managedModelToAlias(platform.id, selected.model)), + token, + ); + if (effort === undefined) return undefined; + return { model: selected.model, effort }; + }, + async promptModelSelectionForCatalog(providerId, models) { + const items = models.map((model) => ({ + label: model.name ?? model.id, + model, + })); + const selected = await vscode.window.showQuickPick( + items, + { + title: `Select a model for ${providerId}`, + placeHolder: "Select a model", + ignoreFocusOut: true, + }, + token, + ); + if (selected === undefined) return undefined; + const effort = await promptEffortLevel( + effortLevelsForModel(catalogModelToAlias(providerId, selected.model)), + token, + ); + if (effort === undefined) return undefined; + return { model: selected.model, effort }; + }, + async refreshConfigAfterLogin() { + // No live extension session to refresh: `Methods.Login` refreshes the + // login context afterwards, and the config on disk is already current. + }, + track() { + // No extension telemetry sink yet. Success is reported by runLogin's + // return value, never inferred from a telemetry call. + }, + }; + return ui; +} diff --git a/apps/vscode/src/handlers/auth.handler.ts b/apps/vscode/src/handlers/auth.handler.ts index 44b1fcb4..138de03e 100644 --- a/apps/vscode/src/handlers/auth.handler.ts +++ b/apps/vscode/src/handlers/auth.handler.ts @@ -1,10 +1,66 @@ +import { runLogin } from "@pythoughts/pythinker-code-sdk"; import * as vscode from "vscode"; -import { Events, Methods } from "../../shared/bridge"; +import { Methods } from "../../shared/bridge"; import type { LoginResult } from "../../shared/legacy-sdk"; import type { LoginStatus } from "../../shared/types"; +import { createVscodeLoginUi } from "../auth/vscode-login-ui"; import { updateLoginContext } from "../utils/context"; -import type { Handler } from "./types"; +import type { Handler, HandlerContext } from "./types"; + +/** + * The login in flight, if any. Login owns modal UI — a second concurrent flow + * would open a competing set of quick picks and race to write credentials — so + * a repeated request joins the running one instead of starting a rival. + */ +let loginInFlight: Promise | undefined; + +async function runLoginOnce(ctx: HandlerContext): Promise { + // Owned here rather than inside the UI: the token has to exist before + // `runLogin` starts, and cancelling has to reach whichever prompt is open, + // not only the step that happens to hold a spinner. + const cancellation = new vscode.CancellationTokenSource(); + const ui = createVscodeLoginUi(ctx, cancellation.token); + try { + // One login-wide progress notification, cancellable: the OAuth flows wait + // minutes on a browser round trip with no per-step spinner of their own, + // so this is the only surface that can offer a way out of them. + const success = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Signing in to Pythinker", + cancellable: true, + }, + async (_progress, token) => { + token.onCancellationRequested(() => { + ui.cancelInFlight?.(); + cancellation.cancel(); + }); + // `runLogin` resolves true only when credentials were written; a false + // result is a user cancellation or a failure the flow already reported. + return runLogin(ui); + }, + ); + // Credentials are already on disk at this point, so a status refresh that + // fails is a stale badge, not a failed login — reporting it as one would + // send the webview back to the sign-in screen the user just completed. + await updateLoginContext(ctx.harness).catch((statusError: unknown) => { + ctx.logError("Unable to refresh login status after a successful login", statusError); + }); + return { success }; + } catch (error) { + ctx.logError("Pythinker login failed", error); + await updateLoginContext(ctx.harness).catch((statusError: unknown) => { + ctx.logError("Unable to refresh login status after a failed login", statusError); + }); + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } finally { + cancellation.dispose(); + } +} export const authHandlers: Record> = { [Methods.CheckLoginStatus]: async (_, ctx): Promise => { @@ -12,26 +68,15 @@ export const authHandlers: Record> = { }, [Methods.Login]: async (_, ctx): Promise => { - try { - await ctx.harness.auth.login(undefined, { - onDeviceCode: async (authorization) => { - const url = authorization.verificationUriComplete || authorization.verificationUri; - ctx.broadcast(Events.LoginUrl, { url }, ctx.webviewId); - await vscode.env.openExternal(vscode.Uri.parse(url)); - }, - }); - await updateLoginContext(ctx.harness); - return { success: true }; - } catch (error) { - ctx.logError("Pythinker login failed", error); - await updateLoginContext(ctx.harness).catch((statusError: unknown) => { - ctx.logError("Unable to refresh login status after a failed login", statusError); - }); - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; - } + if (loginInFlight !== undefined) return loginInFlight; + const pending = runLoginOnce(ctx); + loginInFlight = pending; + void pending.finally(() => { + // Identity-checked so a slow flow that lost the slot cannot clear a newer + // one and strand every later login on a cached result. + if (loginInFlight === pending) loginInFlight = undefined; + }); + return pending; }, [Methods.Logout]: async (_, ctx): Promise => { diff --git a/apps/vscode/src/runtime/event-adapter.ts b/apps/vscode/src/runtime/event-adapter.ts index 0cfa7c32..0fc7a1d3 100644 --- a/apps/vscode/src/runtime/event-adapter.ts +++ b/apps/vscode/src/runtime/event-adapter.ts @@ -7,6 +7,7 @@ import type { SubagentStatusPayload, TokenUsage, TurnBegin, + WorkflowWarningPayload, } from '../../shared/legacy-sdk'; import type { ErrorPhase, UIStreamEvent } from '../../shared/types'; import { toLegacyDisplay } from './tool-display'; @@ -345,6 +346,26 @@ function mapLegacyWireEvent( return mapSubagentStatus(state, sdkEvent, 'failed'); case 'subagent.suspended': return mapSubagentStatus(state, sdkEvent, 'suspended'); + case 'workflow.warning': { + // The warning names its DynamicWorkflow parent tool call directly, so + // session membership is decided by routing (same as every other event): + // unknown agents are dropped by routeSubagentEvent below. + const payload: WorkflowWarningPayload = { + parent_tool_call_id: scopedToolCallId( + sdkEvent.agentId, + sdkEvent.parentToolCallId, + mainAgentId, + ), + workflow_run_id: sdkEvent.workflowRunId, + agent_count: sdkEvent.agentCount, + threshold: sdkEvent.threshold, + message: sdkEvent.message, + }; + return { + state, + event: { type: 'WorkflowWarning', payload }, + }; + } case 'compaction.started': return { state, diff --git a/apps/vscode/test/bridge-handler.test.ts b/apps/vscode/test/bridge-handler.test.ts index 417c0cd9..f31d455e 100644 --- a/apps/vscode/test/bridge-handler.test.ts +++ b/apps/vscode/test/bridge-handler.test.ts @@ -11,8 +11,9 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest"; import type * as vscode from "vscode"; -import { Methods } from "../shared/bridge"; +import { Events, Methods } from "../shared/bridge"; import { BridgeHandler } from "../src/bridge-handler"; +import { loginOutcomeState } from "../webview-ui/src/components/login-outcome"; const host = vi.hoisted(() => { const watcher = { @@ -32,38 +33,105 @@ const host = vi.hoisted(() => { resumeSession: vi.fn(), forkSession: vi.fn(), deleteSession: vi.fn(async () => undefined), + auth: { + status: vi.fn(), + login: vi.fn(), + logout: vi.fn(), + }, }; const showWarningMessage = vi.fn(async () => undefined as string | undefined); + const showQuickPick = vi.fn(); + const showInputBox = vi.fn(); + const showInformationMessage = vi.fn(); + const showErrorMessage = vi.fn(); + // Minimal stand-in for VS Code's own token source: enough to observe that a + // token was handed to a prompt, and to fire the cancellation listeners. + class CancellationTokenSource { + private readonly listeners: Array<() => void> = []; + readonly token = { + isCancellationRequested: false, + onCancellationRequested: (listener: () => void) => { + this.listeners.push(listener); + return { dispose: () => {} }; + }, + }; + + cancel(): void { + this.token.isCancellationRequested = true; + for (const listener of this.listeners) listener(); + } + + dispose(): void {} + } + const withProgress = vi.fn( + async ( + _options: unknown, + task: (progress: unknown, token: unknown) => Promise, + ) => task({ report: vi.fn() }, new CancellationTokenSource().token), + ); + // Typed to match the real `env.openExternal`, so a test can read back the URI + // it was handed rather than an empty argument tuple. + const openExternal = vi.fn(async (_target: Uri) => true); + const executeCommand = vi.fn(async () => undefined); class Uri { - readonly scheme = "file"; - readonly authority = ""; + readonly scheme: string; + readonly authority: string; readonly path: string; - constructor(readonly fsPath: string) { - this.path = fsPath; + constructor( + readonly fsPath: string, + parts?: { scheme: string; authority: string; path: string }, + ) { + this.scheme = parts?.scheme ?? "file"; + this.authority = parts?.authority ?? ""; + this.path = parts?.path ?? fsPath; } static joinPath(base: Uri, ...segments: string[]): Uri { return new Uri(join(base.fsPath, ...segments)); } + // Keeps the scheme the caller parsed. Collapsing every input to `file:` + // hid what `openExternal` actually received, which is the one thing an + // OAuth assertion needs to see. + static parse(input: string): Uri { + const match = /^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)(.*)$/iu.exec(input); + if (match === null) return new Uri(input); + return new Uri(input, { + scheme: match[1]!.toLowerCase(), + authority: match[2]!, + path: match[3]!, + }); + } + toString(): string { - return `file://${this.path}`; + return this.scheme === "file" && this.authority === "" + ? `file://${this.path}` + : `${this.scheme}://${this.authority}${this.path}`; } } return { Uri, + CancellationTokenSource, watcher, harness, showWarningMessage, + showQuickPick, + showInputBox, + showInformationMessage, + showErrorMessage, + withProgress, + openExternal, + executeCommand, workspaceFolders: [] as Array<{ uri: Uri }>, }; }); vi.mock("vscode", () => ({ Uri: host.Uri, + CancellationTokenSource: host.CancellationTokenSource, workspace: { get workspaceFolders() { return host.workspaceFolders; @@ -72,7 +140,17 @@ vi.mock("vscode", () => ({ createFileSystemWatcher: () => host.watcher, textDocuments: [], }, - window: { showWarningMessage: host.showWarningMessage }, + window: { + showWarningMessage: host.showWarningMessage, + showQuickPick: host.showQuickPick, + showInputBox: host.showInputBox, + showInformationMessage: host.showInformationMessage, + showErrorMessage: host.showErrorMessage, + withProgress: host.withProgress, + }, + env: { openExternal: host.openExternal }, + commands: { executeCommand: host.executeCommand }, + ProgressLocation: { Notification: 15 }, })); vi.mock("@pythoughts/pythinker-code-sdk", async (importOriginal) => { @@ -82,6 +160,7 @@ vi.mock("@pythoughts/pythinker-code-sdk", async (importOriginal) => { let bridge: BridgeHandler; let root: string; +let broadcast: Mock; let showLogs: Mock<() => void>; let writeLog: Mock<(message: string) => void>; let workspaceState: { get: ReturnType; update: ReturnType }; @@ -94,11 +173,29 @@ beforeEach(async () => { host.harness.resumeSession.mockReset(); host.harness.getConfig.mockReset(); host.harness.getConfig.mockResolvedValue({ models: {} }); + host.harness.auth.status.mockReset(); + host.harness.auth.status.mockResolvedValue({ providers: [] } as never); + host.harness.auth.login.mockReset(); + host.harness.auth.logout.mockReset(); host.showWarningMessage.mockReset(); host.showWarningMessage.mockResolvedValue(undefined); + host.showQuickPick.mockReset(); + host.showInputBox.mockReset(); + host.showInformationMessage.mockReset(); + host.showErrorMessage.mockReset(); + host.withProgress.mockReset(); + host.withProgress.mockImplementation( + async (_options: unknown, task: (progress: unknown, token: unknown) => Promise) => + task({ report: vi.fn() }, new host.CancellationTokenSource().token), + ); + host.openExternal.mockReset(); + host.openExternal.mockResolvedValue(true); + host.executeCommand.mockReset(); + host.executeCommand.mockResolvedValue(undefined); workspaceState = { get: vi.fn((_key, fallback) => fallback), update: vi.fn() }; + broadcast = vi.fn(); bridge = new BridgeHandler( - vi.fn(), + broadcast, workspaceState as unknown as vscode.Memento, join(root, "global-storage"), vi.fn(), @@ -694,3 +791,312 @@ describe("Webview provider management (writes the same config.toml the CLI reads expect(host.harness.removeProvider).toHaveBeenCalledWith("anthropic"); }); }); + +const CATALOG_RESPONSE = { + anthropic: { + id: "anthropic", + name: "Anthropic", + npm: "@ai-sdk/anthropic", + api: "https://api.anthropic.com", + env: ["PYTHINKER_TEST_ANTHROPIC_KEY"], + models: { + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + limit: { context: 200_000, output: 64_000 }, + tool_call: true, + reasoning: true, + modalities: { input: ["text"], output: ["text"] }, + }, + }, + }, +}; + +// The single-flight guard behind Methods.Login is module state, held for as +// long as a login runs. A test that hangs never releases it, so every later +// login here joins the hung one and times out too: when several of these fail +// at once, fix the first and the rest usually go with it. +describe("Webview login (multi-provider picker behind Methods.Login)", () => { + beforeEach(() => { + // Default to an unreachable models.dev: login must fall back to the bundled + // catalog seed and still offer the built-in OAuth platforms. Tests that + // exercise a catalog provider stub a live response over this. + vi.stubGlobal( + "fetch", + vi.fn(async () => { + throw new Error("offline"); + }), + ); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("writes the picked provider's credentials and reports success", async () => { + // A live catalog, so the API-key flow runs end to end: provider pick, key + // entry, model pick, effort pick. + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ + ok: true, + status: 200, + json: async () => CATALOG_RESPONSE, + })), + ); + host.harness.getConfig.mockResolvedValue({ providers: {}, models: {} } as never); + host.showInputBox.mockResolvedValue("sk-typed-in" as never); + + let offeredEffortLevels: string[] = []; + host.showQuickPick + .mockImplementationOnce( + async (items: Array<{ value: string }>) => + items.find((item) => item.value === "catalog:anthropic"), + ) + .mockImplementationOnce(async (items: unknown[]) => items[0]) + .mockImplementationOnce(async (items: Array<{ label: string }>) => { + offeredEffortLevels = items.map((item) => item.label); + return items.find((item) => item.label === "medium"); + }); + + const result = await bridge.handle({ id: "rpc-login", method: Methods.Login }, "view-1"); + + expect(result).toEqual({ id: "rpc-login", result: { success: true } }); + const platformItems = host.showQuickPick.mock.calls[0]?.[0] as Array<{ label: string }>; + expect(platformItems.map((item) => item.label)).toEqual( + expect.arrayContaining(["OpenAI Codex (OAuth)", "Anthropic"]), + ); + // The effort levels come from the SDK's shared rule, so the extension + // offers exactly what the terminal renderer offers for the same model. + // `claude-opus-4-7` declares reasoning and no `supportEfforts`, which is + // the low/medium/high fallback plus the `off` toggle. + expect(offeredEffortLevels).toEqual(["off", "low", "medium", "high"]); + expect(host.harness.setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + providers: expect.objectContaining({ + anthropic: expect.objectContaining({ apiKey: "sk-typed-in" }), + }), + defaultModel: "anthropic/claude-opus-4-7", + // The picked level, not just the on/off bit: the patch used to omit + // `thinking`, so every session reopened at the default effort. + defaultThinking: true, + thinking: expect.objectContaining({ effort: "medium" }), + }), + ); + }); + + it("still offers the built-in OAuth platforms when the live catalog is unreachable", async () => { + host.showQuickPick.mockResolvedValue(undefined as never); + + await bridge.handle({ id: "rpc-login", method: Methods.Login }, "view-1"); + + // Offline is a benign fallback, not a failure: the picker still opens with + // the built-in OAuth platforms and no error toast is shown. + const platformItems = host.showQuickPick.mock.calls[0]?.[0] as Array<{ label: string }>; + expect(platformItems.map((item) => item.label)).toEqual( + expect.arrayContaining(["OpenAI Codex (OAuth)"]), + ); + expect(host.showErrorMessage).not.toHaveBeenCalled(); + expect(host.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining("Using bundled provider catalog"), + ); + }); + + it("opens the OAuth verification URL externally with its scheme intact", async () => { + host.harness.auth.status.mockResolvedValue({ providers: [] } as never); + host.harness.auth.login.mockImplementation( + async (_provider: string, options: { onDeviceCode?: (auth: unknown) => void }) => { + options.onDeviceCode?.({ + userCode: "WDJB-MJHT", + deviceCode: "devcode123", + verificationUri: "https://auth.kimi.com/verify", + verificationUriComplete: "https://auth.kimi.com/verify?user_code=WDJB-MJHT", + expiresIn: 600, + interval: 5, + }); + return { providerName: "managed:kimi-code", ok: true }; + }, + ); + host.showQuickPick.mockImplementationOnce( + async (items: Array<{ value: string }>) => + items.find((item) => item.value === "kimi-code"), + ); + + await bridge.handle({ id: "rpc-login", method: Methods.Login }, "view-1"); + + // The webview and the browser must be handed the same https URL. An earlier + // mock collapsed every scheme to `file:`, which would have hidden a URL the + // extension opened with the wrong one. + expect(broadcast).toHaveBeenCalledWith( + Events.LoginUrl, + { url: "https://auth.kimi.com/verify?user_code=WDJB-MJHT" }, + "view-1", + ); + const opened = host.openExternal.mock.calls[0]?.[0]; + expect(opened?.scheme).toBe("https"); + expect(opened?.toString()).toBe("https://auth.kimi.com/verify?user_code=WDJB-MJHT"); + }); + + it("reports cancellation without writing config when the picker is dismissed", async () => { + host.showQuickPick.mockResolvedValue(undefined as never); + + const result = await bridge.handle({ id: "rpc-login", method: Methods.Login }, "view-1"); + + // No `error` field: the webview renders a cancellation as idle, not as a + // failed login. + expect(result).toEqual({ id: "rpc-login", result: { success: false } }); + expect(host.harness.setConfig).not.toHaveBeenCalled(); + expect(broadcast).not.toHaveBeenCalledWith(Events.LoginUrl, expect.anything(), "view-1"); + }); + + it("renders a dismissed picker as idle, and only a real failure as an error", () => { + // Before login was multi-provider this branch was unreachable, so a + // cancelled picker used to fall through to the "Login failed" screen. + expect(loginOutcomeState({ success: false })).toEqual({ state: "idle", error: null }); + expect(loginOutcomeState({ success: false, error: "boom" })).toEqual({ + state: "error", + error: "boom", + }); + }); + + it("opens one cancellable progress notification and hands its token to every prompt", async () => { + // Driven all the way through the credential flow rather than dismissed at + // the picker: a token that only reaches the first widget still strands the + // user on the key, model, and effort prompts. + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ + ok: true, + status: 200, + json: async () => CATALOG_RESPONSE, + })), + ); + host.harness.getConfig.mockResolvedValue({ providers: {}, models: {} } as never); + host.showInputBox.mockResolvedValue("sk-typed-in" as never); + host.showQuickPick + .mockImplementationOnce( + async (items: Array<{ value: string }>) => + items.find((item) => item.value === "catalog:anthropic"), + ) + .mockImplementationOnce(async (items: unknown[]) => items[0]) + .mockImplementationOnce(async (items: Array<{ label: string }>) => + items.find((item) => item.label === "medium"), + ); + + await bridge.handle({ id: "rpc-login", method: Methods.Login }, "view-1"); + + // The OAuth flows wait minutes on a browser round trip with no spinner of + // their own, so this notification is the only way out of them. + const loginProgress = host.withProgress.mock.calls.find( + (call) => (call[0] as { title?: string }).title === "Signing in to Pythinker", + ); + expect(loginProgress?.[0]).toMatchObject({ cancellable: true }); + + // The token has to reach each prompt itself: cancelling has to close the + // widget the user is looking at, not just abort a background fetch. + // Provider, model, and effort pickers, then the API-key box. + expect(host.showQuickPick).toHaveBeenCalledTimes(3); + for (const call of host.showQuickPick.mock.calls) { + expect(call[2]).toBeDefined(); + } + expect(host.showInputBox).toHaveBeenCalled(); + for (const call of host.showInputBox.mock.calls) { + expect(call[1]).toBeDefined(); + } + }); + + it("cancelling the progress notification aborts the catalog fetch", async () => { + // A catalog fetch that only ever settles by being aborted: without a cancel + // path this login hangs there and the picker never opens. + let abortSignal: AbortSignal | undefined; + let fetchEntered: () => void = () => {}; + const fetchStarted = new Promise((resolve) => { + fetchEntered = resolve; + }); + vi.stubGlobal( + "fetch", + vi.fn(async (_url: string, init: { signal?: AbortSignal }) => { + abortSignal = init.signal; + fetchEntered(); + return new Promise((_resolve, reject) => { + init.signal?.addEventListener( + "abort", + () => { + reject(new Error("aborted")); + }, + { once: true }, + ); + }); + }), + ); + host.showQuickPick.mockResolvedValue(undefined as never); + host.withProgress.mockImplementation( + async ( + _options: unknown, + task: (progress: unknown, token: unknown) => Promise, + ) => { + const source = new host.CancellationTokenSource(); + const running = task({ report: vi.fn() }, source.token); + // Cancel once the flow is parked on the catalog fetch. + await fetchStarted; + source.cancel(); + return running; + }, + ); + + const result = await bridge.handle({ id: "rpc-login", method: Methods.Login }, "view-1"); + + expect(abortSignal?.aborted).toBe(true); + expect(result).toEqual({ id: "rpc-login", result: { success: false } }); + expect(host.harness.setConfig).not.toHaveBeenCalled(); + }); + + it("joins a concurrent login instead of opening a second set of prompts", async () => { + let openPickers = 0; + host.showQuickPick.mockImplementation(async () => { + openPickers += 1; + return undefined; + }); + + const [first, second] = await Promise.all([ + bridge.handle({ id: "rpc-login-1", method: Methods.Login }, "view-1"), + bridge.handle({ id: "rpc-login-2", method: Methods.Login }, "view-1"), + ]); + + // Two flows would race to write credentials behind two competing pickers. + expect(openPickers).toBe(1); + expect(first).toEqual({ id: "rpc-login-1", result: { success: false } }); + expect(second).toEqual({ id: "rpc-login-2", result: { success: false } }); + + // The slot is released, so a later login still runs. + await bridge.handle({ id: "rpc-login-3", method: Methods.Login }, "view-1"); + expect(openPickers).toBe(2); + }); + + it("keeps a completed login successful when the status refresh fails", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ ok: true, status: 200, json: async () => CATALOG_RESPONSE })), + ); + host.harness.getConfig.mockResolvedValue({ providers: {}, models: {} } as never); + host.showInputBox.mockResolvedValue("sk-typed-in" as never); + host.showQuickPick + .mockImplementationOnce( + async (items: Array<{ value: string }>) => + items.find((item) => item.value === "catalog:anthropic"), + ) + .mockImplementationOnce(async (items: unknown[]) => items[0]) + .mockImplementationOnce(async (items: Array<{ label: string }>) => + items.find((item) => item.label === "medium"), + ); + // The refresh runs after credentials are already on disk, so its failure is + // a stale badge — reporting it as a failed login would send the user back + // to the sign-in screen they just completed. + host.harness.auth.status.mockRejectedValue(new Error("status backend down") as never); + + const result = await bridge.handle({ id: "rpc-login", method: Methods.Login }, "view-1"); + + expect(result).toEqual({ id: "rpc-login", result: { success: true } }); + expect(host.harness.setConfig).toHaveBeenCalled(); + }); +}); diff --git a/apps/vscode/test/event-adapter.test.ts b/apps/vscode/test/event-adapter.test.ts index 04a18520..265a8f1d 100644 --- a/apps/vscode/test/event-adapter.test.ts +++ b/apps/vscode/test/event-adapter.test.ts @@ -484,6 +484,31 @@ describe('event adapter (projects SDK events into the legacy Webview contract)', }); }); + it('maps a workflow warning SDK event to a WorkflowWarning wire event carrying every field', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'workflow.warning', + sessionId: 'session-1', + agentId: 'main', + workflowRunId: 'run-1', + parentToolCallId: 'wf-1', + agentCount: 12, + threshold: 8, + message: 'This Dynamic Workflow will launch 12 subagents, above the advisory ceiling of 8; the run is proceeding anyway.', + }); + + expect(result.event).toEqual({ + type: 'WorkflowWarning', + payload: { + parent_tool_call_id: 'wf-1', + workflow_run_id: 'run-1', + agent_count: 12, + threshold: 8, + message: 'This Dynamic Workflow will launch 12 subagents, above the advisory ceiling of 8; the run is proceeding anyway.', + }, + _sessionId: 'session-1', + }); + }); + it('emits compaction begin when SDK compaction starts', () => { const result = adaptSdkEvent(createEventAdapterState(), { type: 'compaction.started', diff --git a/apps/vscode/test/event-handlers.test.ts b/apps/vscode/test/event-handlers.test.ts index 82b6b7cb..cdbffcfa 100644 --- a/apps/vscode/test/event-handlers.test.ts +++ b/apps/vscode/test/event-handlers.test.ts @@ -154,6 +154,44 @@ describe("Webview DynamicWorkflow per-agent lanes", () => { expect(typeof status.endedAt).toBe("number"); }); + it("stores a WorkflowWarning on the workflow's own tool item", () => { + startWorkflowTurn(); + + useChatStore.getState().processEvent({ + type: "WorkflowWarning", + payload: { + parent_tool_call_id: "wf-1", + workflow_run_id: "wfr-test-001", + agent_count: 26, + threshold: 25, + message: "This Dynamic Workflow will launch 26 subagents, above the advisory ceiling of 25; the run is proceeding anyway.", + }, + }); + + expect(workflowToolItem().workflow_warning).toEqual({ + message: "This Dynamic Workflow will launch 26 subagents, above the advisory ceiling of 25; the run is proceeding anyway.", + agentCount: 26, + threshold: 25, + }); + }); + + it("drops a WorkflowWarning naming a tool call that is not on screen", () => { + startWorkflowTurn(); + + useChatStore.getState().processEvent({ + type: "WorkflowWarning", + payload: { + parent_tool_call_id: "wf-other", + workflow_run_id: "wfr-test-002", + agent_count: 26, + threshold: 25, + message: "belongs to a different workflow", + }, + }); + + expect(workflowToolItem().workflow_warning).toBeUndefined(); + }); + it("targets the main agent's steps by array tail when no agent is scoped", () => { // The only remaining no-agent case: applyEventToSteps called directly for // the main agent's own turn, not through a SubagentEvent wrapper. diff --git a/apps/vscode/tsdown.config.ts b/apps/vscode/tsdown.config.ts index c5438fb0..185a68ff 100644 --- a/apps/vscode/tsdown.config.ts +++ b/apps/vscode/tsdown.config.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { resolve } from 'node:path'; @@ -9,6 +10,17 @@ const require = createRequire(import.meta.url); const pkg = require('./package.json') as { version: string }; const root = import.meta.dirname; +// Bundled models.dev catalog seed for offline login, injected the same way +// the CLI injects it: read from `PYTHINKER_CODE_BUILT_IN_CATALOG_FILE` at +// build time, replaced with the `undefined` token when the env var is absent. +const BUILT_IN_CATALOG_DEFINE = '__PYTHINKER_CODE_BUILT_IN_CATALOG__'; +const BUILT_IN_CATALOG_ENV = 'PYTHINKER_CODE_BUILT_IN_CATALOG_FILE'; +function builtInCatalogDefine(): string { + const file = process.env[BUILT_IN_CATALOG_ENV]; + if (file === undefined || file.length === 0) return 'undefined'; + return JSON.stringify(readFileSync(file, 'utf-8')); +} + export default defineConfig({ entry: ['./src/extension.ts'], format: ['esm'], @@ -27,6 +39,7 @@ export default defineConfig({ }, define: { __EXTENSION_VERSION__: JSON.stringify(pkg.version), + [BUILT_IN_CATALOG_DEFINE]: builtInCatalogDefine(), }, banner: { js: [ diff --git a/apps/vscode/webview-ui/src/components/ChatMessage.tsx b/apps/vscode/webview-ui/src/components/ChatMessage.tsx index 00373f8f..67534678 100644 --- a/apps/vscode/webview-ui/src/components/ChatMessage.tsx +++ b/apps/vscode/webview-ui/src/components/ChatMessage.tsx @@ -72,7 +72,7 @@ function StepItemRenderer({ item }: { item: UIStepItem }) { case "text": return ; case "tool_use": - return ; + return ; case "compaction": return ; case "steer": diff --git a/apps/vscode/webview-ui/src/components/LoginScreen.tsx b/apps/vscode/webview-ui/src/components/LoginScreen.tsx index 2e399b43..854629a0 100644 --- a/apps/vscode/webview-ui/src/components/LoginScreen.tsx +++ b/apps/vscode/webview-ui/src/components/LoginScreen.tsx @@ -3,35 +3,18 @@ import { IconLoader2, IconCopy, IconCheck, IconExternalLink, IconArrowRight } fr import { Button } from "@/components/ui/button"; import { PythinkerMascot } from "./PythinkerMascot"; import { bridge, Events } from "@/services"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; +import { loginOutcomeState, type LoginState } from "./login-outcome"; interface LoginScreenProps { onLoginSuccess: () => void; onSkip: () => void; } -type LoginState = "idle" | "pending" | "error"; - -function isPaymentRequiredError(error: string | null): boolean { - if (!error) return false; - return error.includes("402") || error.toLowerCase().includes("payment required"); -} - export function LoginScreen({ onLoginSuccess, onSkip }: LoginScreenProps) { const [state, setState] = useState("idle"); const [url, setUrl] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); - const [showSubscribeDialog, setShowSubscribeDialog] = useState(false); useEffect(() => { return bridge.on<{ url: string }>(Events.LoginUrl, ({ url }) => { @@ -48,32 +31,16 @@ export function LoginScreen({ onLoginSuccess, onSkip }: LoginScreenProps) { if (result.success) { onLoginSuccess(); } else { - const errorMessage = result.error || "Login failed"; - if (isPaymentRequiredError(errorMessage)) { - setShowSubscribeDialog(true); - setState("idle"); - } else { - setState("error"); - setError(errorMessage); - } + const outcome = loginOutcomeState(result); + setState(outcome.state); + setError(outcome.error); } } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - if (isPaymentRequiredError(errorMessage)) { - setShowSubscribeDialog(true); - setState("idle"); - } else { - setState("error"); - setError(errorMessage); - } + setState("error"); + setError(error instanceof Error ? error.message : String(error)); } }; - const handleSubscribe = () => { - window.open("https://www.kimi.com/code", "_blank"); - setShowSubscribeDialog(false); - }; - const handleCopyUrl = async () => { if (!url) return; await navigator.clipboard.writeText(url); @@ -161,21 +128,6 @@ export function LoginScreen({ onLoginSuccess, onSkip }: LoginScreenProps) { - - - - - Subscription Required - - Your Kimi account does not have an active Kimi Code Plan subscription. Please subscribe to continue using Pythinker Code with Kimi. - - - - setShowSubscribeDialog(false)}>Skip - Subscribe - - - ); } diff --git a/apps/vscode/webview-ui/src/components/ToolRenderers.tsx b/apps/vscode/webview-ui/src/components/ToolRenderers.tsx index f7809666..9026683a 100644 --- a/apps/vscode/webview-ui/src/components/ToolRenderers.tsx +++ b/apps/vscode/webview-ui/src/components/ToolRenderers.tsx @@ -21,7 +21,7 @@ import { formatContentOutput } from "shared/legacy-sdk"; import { cleanSystemTags } from "shared/utils"; import { ThinkingBlock } from "./ThinkingBlock"; import { WorkflowCard } from "./WorkflowCard"; -import type { UIToolCall, UIStep, UIStepItem, UISubagentStatus } from "@/stores/chat.store"; +import type { UIToolCall, UIStep, UIStepItem, UISubagentStatus, UIWorkflowWarning } from "@/stores/chat.store"; import type { ToolResult, DisplayBlock, TodoBlock } from "shared/legacy-sdk"; type ToolResultValue = ToolResult["return_value"]; @@ -31,6 +31,7 @@ interface ToolRendererProps { result?: ToolResultValue; subagentSteps?: UIStep[]; subagentStatus?: Record; + workflowWarning?: UIWorkflowWarning; } function formatOutput(output: string | object | object[]): string { @@ -307,7 +308,7 @@ export function SubagentStepItemRenderer({ item }: { item: UIStepItem }) { return ; } if (item.type === "tool_use") { - return ; + return ; } return null; } @@ -377,7 +378,7 @@ function TaskTool({ call, result, subagentSteps }: ToolRendererProps) { ); } -export function ToolCallCard({ call, result, subagentSteps, subagentStatus }: ToolRendererProps) { +export function ToolCallCard({ call, result, subagentSteps, subagentStatus, workflowWarning }: ToolRendererProps) { const [expanded, setExpanded] = useState(false); const status = !result ? "pending" : !result.is_error ? "success" : "error"; @@ -390,6 +391,7 @@ export function ToolCallCard({ call, result, subagentSteps, subagentStatus }: To result={result} subagentSteps={subagentSteps ?? []} subagentStatus={subagentStatus ?? {}} + workflowWarning={workflowWarning} renderStepItem={(item) => } /> ); diff --git a/apps/vscode/webview-ui/src/components/WorkflowCard.tsx b/apps/vscode/webview-ui/src/components/WorkflowCard.tsx index 9ee8b26b..4b1d690a 100644 --- a/apps/vscode/webview-ui/src/components/WorkflowCard.tsx +++ b/apps/vscode/webview-ui/src/components/WorkflowCard.tsx @@ -3,7 +3,7 @@ import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"; import { cn } from "@/lib/utils"; import { deriveWorkflowLanes, maxLaneStepCount, type WorkflowLane } from "@/lib/workflow-lanes"; import { getToolLabel, parseArgs } from "@/lib/tool-args"; -import type { UIToolCall, UIStep, UIStepItem, UISubagentStatus } from "@/stores/chat.store"; +import type { UIToolCall, UIStep, UIStepItem, UISubagentStatus, UIWorkflowWarning } from "@/stores/chat.store"; import type { ToolResult } from "shared/legacy-sdk"; interface WorkflowCardProps { @@ -11,6 +11,7 @@ interface WorkflowCardProps { result?: ToolResult["return_value"]; subagentSteps: UIStep[]; subagentStatus: Record; + workflowWarning?: UIWorkflowWarning; /** Injected by ToolRenderers: tool rendering is mutually recursive, and importing * it here would close an import cycle that oxlint rejects. */ renderStepItem: (item: UIStepItem) => ReactNode; @@ -96,7 +97,7 @@ function LaneRow({ lane, maxSteps, renderStepItem }: { lane: WorkflowLane; maxSt ); } -export function WorkflowCard({ call, result, subagentSteps, subagentStatus, renderStepItem }: WorkflowCardProps) { +export function WorkflowCard({ call, result, subagentSteps, subagentStatus, workflowWarning, renderStepItem }: WorkflowCardProps) { const description = (parseArgs(call.arguments).description as string) || "Dynamic workflow"; const lanes = useMemo( () => deriveWorkflowLanes(subagentSteps, subagentStatus), @@ -127,6 +128,11 @@ export function WorkflowCard({ call, result, subagentSteps, subagentStatus, rend )} + {workflowWarning && ( +
+ {workflowWarning.message} +
+ )}
{lanes.map((lane) => ( diff --git a/apps/vscode/webview-ui/src/components/login-outcome.ts b/apps/vscode/webview-ui/src/components/login-outcome.ts new file mode 100644 index 00000000..ae772a82 --- /dev/null +++ b/apps/vscode/webview-ui/src/components/login-outcome.ts @@ -0,0 +1,14 @@ +export type LoginState = "idle" | "pending" | "error"; + +/** + * How a finished login maps onto the login screen. Login is multi-provider, so + * a falsy `success` is usually a cancelled picker rather than a failure: only a + * result carrying an error message may render the error screen. + */ +export function loginOutcomeState(result: { success: boolean; error?: string }): { + state: LoginState; + error: string | null; +} { + if (result.error) return { state: "error", error: result.error }; + return { state: "idle", error: null }; +} diff --git a/apps/vscode/webview-ui/src/stores/chat.store.ts b/apps/vscode/webview-ui/src/stores/chat.store.ts index 6825ae33..bdee7f2f 100644 --- a/apps/vscode/webview-ui/src/stores/chat.store.ts +++ b/apps/vscode/webview-ui/src/stores/chat.store.ts @@ -37,6 +37,12 @@ export interface UISubagentStatus { resultSummary?: string; } +export interface UIWorkflowWarning { + message: string; + agentCount: number; + threshold: number; +} + export interface InlineError { code: string; message: string; @@ -55,6 +61,7 @@ export type UIStepItem = result?: ToolResult["return_value"]; subagent_steps?: UIStep[]; subagent_status?: Record; + workflow_warning?: UIWorkflowWarning; }; export interface ChatMessage { diff --git a/apps/vscode/webview-ui/src/stores/event-handlers.ts b/apps/vscode/webview-ui/src/stores/event-handlers.ts index 2bd1ab0b..f6822e74 100644 --- a/apps/vscode/webview-ui/src/stores/event-handlers.ts +++ b/apps/vscode/webview-ui/src/stores/event-handlers.ts @@ -3,7 +3,7 @@ import { useApprovalStore } from "./approval.store"; import { useSettingsStore } from "./settings.store"; import { isPreflightError, isUserInterrupt } from "shared/errors"; import type { ChatMessage, UIStep, UIStepItem, UISubagentStatus, ChatState, TokenUsage } from "./chat.store"; -import type { ContentPart, ToolCall, ToolResult, TurnBegin, SubagentEvent, SubagentStatusPayload, ApprovalRequestPayload, DiffBlock, RunResult, QuestionRequest } from "shared/legacy-sdk"; +import type { ContentPart, ToolCall, ToolResult, TurnBegin, SubagentEvent, SubagentStatusPayload, WorkflowWarningPayload, ApprovalRequestPayload, DiffBlock, RunResult, QuestionRequest } from "shared/legacy-sdk"; import type { UIStreamEvent, StreamError } from "shared/types"; type EventHandler = (draft: ChatState, payload: any) => void; @@ -603,6 +603,24 @@ const eventHandlers: Record = { applySubagentStatus(toolItem, payload); }, + WorkflowWarning: (draft, payload: WorkflowWarningPayload) => { + const last = getLastAssistant(draft); + if (!last?.steps) { + return; + } + + const toolItem = findToolUseItem(last.steps, payload.parent_tool_call_id); + if (!toolItem) { + return; + } + + toolItem.workflow_warning = { + message: payload.message, + agentCount: payload.agent_count, + threshold: payload.threshold, + }; + }, + ApprovalRequest: (_, payload: ApprovalRequestPayload) => { useApprovalStore.getState().addRequest({ id: payload.id, diff --git a/flake.nix b/flake.nix index 27c405f1..674e910b 100644 --- a/flake.nix +++ b/flake.nix @@ -148,7 +148,7 @@ inherit (finalAttrs) pname version src pnpmWorkspaces; inherit pnpm; fetcherVersion = 3; - hash = "sha256-9DehARrDfyGqxEqDmfeYTw1tIFqNQVxpOpiaW6QY4Zw="; + hash = "sha256-GKfwTPY1YSCXyTICa/T2SrkqitM1EL/7l3Ry0VfNtY0="; }; nativeBuildInputs = [ diff --git a/packages/acp-adapter/src/auth-methods.ts b/packages/acp-adapter/src/auth-methods.ts index aaa3980d..ee23592e 100644 --- a/packages/acp-adapter/src/auth-methods.ts +++ b/packages/acp-adapter/src/auth-methods.ts @@ -36,8 +36,8 @@ export function buildTerminalAuthMethod( const method: AuthMethod = { id: 'login', type: 'terminal', - name: 'Login with Pythinker account', - description: 'Open the device-code login flow in a terminal.', + name: 'Login with a provider', + description: 'Choose a provider and complete login in a terminal.', // Appended to the agent's configured args by spec-compliant clients // (e.g. `args:['acp']` + `args:['--login']` → `acp --login`). The // `--login` flag on `pythinker acp` pivots into the login flow before @@ -49,7 +49,7 @@ export function buildTerminalAuthMethod( (method as AuthMethod & { _meta: { 'terminal-auth': unknown } })._meta = { 'terminal-auth': { type: 'terminal', - label: 'Login with Pythinker account', + label: 'Login with a provider', // Legacy clients use this verbatim as the executable path, NOT // combined with the agent server's configured command (per Zed's // `meta_terminal_auth_task` in `agent_servers/src/acp.rs`). diff --git a/packages/acp-adapter/test/server.test.ts b/packages/acp-adapter/test/server.test.ts index 451e1cfd..be60b5fa 100644 --- a/packages/acp-adapter/test/server.test.ts +++ b/packages/acp-adapter/test/server.test.ts @@ -182,7 +182,7 @@ describe('AcpServer + AgentSideConnection', () => { // Legacy _meta fallback uses absolute command + 'login' subcommand. expect(method._meta?.['terminal-auth']).toEqual({ type: 'terminal', - label: 'Login with Pythinker account', + label: 'Login with a provider', command: '/abs/path/to/pythinker', args: ['login'], env: { PYTHINKER_CODE_HOME: '/tmp/pythinker-debug' }, diff --git a/packages/agent-core/src/agent/dynamic-workflow/index.ts b/packages/agent-core/src/agent/dynamic-workflow/index.ts index c2593074..4d8e4127 100644 --- a/packages/agent-core/src/agent/dynamic-workflow/index.ts +++ b/packages/agent-core/src/agent/dynamic-workflow/index.ts @@ -2,6 +2,7 @@ import type { Agent } from '..'; import DYNAMIC_WORKFLOW_MODE_ENTER_REMINDER from './enter-reminder.md?raw'; import DYNAMIC_WORKFLOW_MODE_EXIT_REMINDER from './exit-reminder.md?raw'; +import { resolveWorkflowSizeGuideline, workflowSizeGuidelineNote } from './size-guideline'; /** * manual = persistent toggle; @@ -20,10 +21,18 @@ export class DynamicWorkflowMode { this.agent.records.logRecord({ type: 'dynamic_workflow_mode.enter', trigger }); this.active = trigger; if (trigger !== 'tool') { - this.agent.context.appendSystemReminder(DYNAMIC_WORKFLOW_MODE_ENTER_REMINDER, { - kind: 'injection', - variant: 'dynamic_workflow_mode', - }); + const sizeNote = workflowSizeGuidelineNote( + resolveWorkflowSizeGuideline(this.agent.pythinkerConfig), + ); + this.agent.context.appendSystemReminder( + sizeNote === undefined + ? DYNAMIC_WORKFLOW_MODE_ENTER_REMINDER + : `${DYNAMIC_WORKFLOW_MODE_ENTER_REMINDER}\n\n${sizeNote}`, + { + kind: 'injection', + variant: 'dynamic_workflow_mode', + }, + ); } this.agent.emitStatusUpdated(); } diff --git a/packages/agent-core/src/agent/dynamic-workflow/run-id.ts b/packages/agent-core/src/agent/dynamic-workflow/run-id.ts new file mode 100644 index 00000000..50f4dbf4 --- /dev/null +++ b/packages/agent-core/src/agent/dynamic-workflow/run-id.ts @@ -0,0 +1,14 @@ +import { randomBytes } from 'node:crypto'; + +/** Matches a generated run id. Also the guard for a run id that arrives as tool input. */ +const WORKFLOW_RUN_ID_PATTERN = /^wfr-[0-9a-z]{1,32}-[0-9a-z]{1,16}$/u; + +export function isWorkflowRunId(value: string): boolean { + return WORKFLOW_RUN_ID_PATTERN.test(value); +} + +/** Timestamp plus crypto randomness, both lowercase — no character that means + * anything to a filesystem or a shell. */ +export function generateWorkflowRunId(): string { + return `wfr-${Date.now().toString(36)}-${randomBytes(6).toString('hex')}`; +} diff --git a/packages/agent-core/src/agent/dynamic-workflow/size-guideline.ts b/packages/agent-core/src/agent/dynamic-workflow/size-guideline.ts new file mode 100644 index 00000000..cdb30321 --- /dev/null +++ b/packages/agent-core/src/agent/dynamic-workflow/size-guideline.ts @@ -0,0 +1,55 @@ +import { resolveConfigValue, type PythinkerConfig, type WorkflowSizeGuideline } from '../../config'; + +export const WORKFLOW_SIZE_GUIDELINE_ENV = 'PYTHINKER_CODE_WORKFLOW_SIZE_GUIDELINE'; + +export const DEFAULT_WORKFLOW_SIZE_GUIDELINE: WorkflowSizeGuideline = 'medium'; + +/** Advisory subagent-count target per guideline. `unrestricted` has none. */ +const GUIDELINE_TARGETS: Record = { + small: 5, + medium: 15, + large: 40, + unrestricted: undefined, +}; + +// Derived from the targets rather than listed again: the record is +// exhaustiveness-checked against WorkflowSizeGuideline, so a new guideline +// cannot be accepted by the env parser without also getting a target. +const WORKFLOW_SIZE_GUIDELINE_NAMES = new Set(Object.keys(GUIDELINE_TARGETS)); + +/** The numeric subagent-count target for a guideline, or `undefined` for `unrestricted`. */ +export function workflowSizeGuidelineTarget(guideline: WorkflowSizeGuideline): number | undefined { + return GUIDELINE_TARGETS[guideline]; +} + +function parseWorkflowSizeGuidelineEnv(value: string | undefined): WorkflowSizeGuideline | undefined { + const normalized = value?.trim().toLowerCase(); + if (normalized === undefined || !WORKFLOW_SIZE_GUIDELINE_NAMES.has(normalized)) return undefined; + return normalized as WorkflowSizeGuideline; +} + +export function resolveWorkflowSizeGuideline( + config: Pick | undefined, + env: Readonly> = process.env, +): WorkflowSizeGuideline { + return resolveConfigValue({ + env, + envKey: WORKFLOW_SIZE_GUIDELINE_ENV, + configValue: config?.workflowSizeGuideline, + defaultValue: DEFAULT_WORKFLOW_SIZE_GUIDELINE, + parseEnv: parseWorkflowSizeGuidelineEnv, + }); +} + +/** + * Note appended to the model-facing Dynamic Workflow guidance, or `undefined` for + * `unrestricted` (which keeps the existing decompose-freely wording). + * + * The base guidance tells the model to decompose as finely as possible, so the note has to + * say out loud that it supersedes that — otherwise the model reads two conflicting targets. + */ +export function workflowSizeGuidelineNote(guideline: WorkflowSizeGuideline): string | undefined { + const target = GUIDELINE_TARGETS[guideline]; + if (target === undefined) return undefined; + return `Workflow size guideline: this supersedes the guidance above about decomposing as finely as possible. Unless the user explicitly asks for more, aim for at most about ${String(target)} subagents in one workflow, preferring fewer, larger items over many tiny ones.`; +} diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts index b8efbb8e..da144b43 100644 --- a/packages/agent-core/src/agent/tool/index.ts +++ b/packages/agent-core/src/agent/tool/index.ts @@ -12,6 +12,8 @@ import { isMcpToolName, qualifyMcpToolName } from '../../mcp/tool-naming'; import type { MCPClient } from '../../mcp/types'; import { extendWorkspaceWithSkillRoots } from '../../skill'; import * as b from '../../tools/builtin'; +import { isDynamicWorkflowDisabled } from '../../tools/builtin/collaboration/dynamic-workflow'; +import { resolveWorkflowSizeGuideline } from '../dynamic-workflow/size-guideline'; import type { ToolStore, ToolStoreData, ToolStoreKey } from '../../tools/store'; import type { BuiltinTool, @@ -466,6 +468,8 @@ export class ToolManager { this.agent.lsp !== undefined && this.agent.lsp.hasServers && this.agent.experimentalFlags.enabled('lsp'); + const dynamicWorkflowEnabled = !isDynamicWorkflowDisabled(this.agent.pythinkerConfig); + const workflowSizeGuideline = resolveWorkflowSizeGuideline(this.agent.pythinkerConfig); const builtinTools = new Map( [ new b.ReadTool(kaos, workspace, this.fileReadState), @@ -556,7 +560,13 @@ export class ToolManager { }, ), this.agent.subagentHost && - new b.DynamicWorkflowTool(this.agent.subagentHost, this.agent.dynamicWorkflowMode), + dynamicWorkflowEnabled && + new b.DynamicWorkflowTool( + this.agent.subagentHost, + this.agent.dynamicWorkflowMode, + workflowSizeGuideline, + (event) => this.agent.emitEvent(event), + ), toolServices?.webSearcher && new b.WebSearchTool(toolServices.webSearcher), toolServices?.urlFetcher && new b.FetchURLTool(toolServices.urlFetcher, kaos), ] diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index b60a185b..47cda036 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -191,8 +191,13 @@ export class TurnFlow { return this.launch(input, origin); } - retry(trigger?: string): number | null { - return this.prompt([], { kind: 'retry', trigger }); + /** + * Re-runs the turn. The schema must be passed again: a retried turn builds a + * fresh StructuredOutputState, and without one the model is never offered the + * StructuredOutput tool, so a schema'd subagent would quietly answer in prose. + */ + retry(trigger?: string, outputSchema?: Record): number | null { + return this.prompt([], { kind: 'retry', trigger }, outputSchema); } private launch( diff --git a/packages/agent-core/src/config/schema.ts b/packages/agent-core/src/config/schema.ts index a504c336..257812ce 100644 --- a/packages/agent-core/src/config/schema.ts +++ b/packages/agent-core/src/config/schema.ts @@ -74,6 +74,10 @@ export type ThinkingConfig = z.infer; export const PermissionModeSchema = z.enum(['yolo', 'manual', 'auto']); +export const WorkflowSizeGuidelineSchema = z.enum(['small', 'medium', 'large', 'unrestricted']); + +export type WorkflowSizeGuideline = z.infer; + export const PermissionRuleDecisionSchema = z.enum(['allow', 'deny', 'ask']); export const PermissionRuleScopeSchema = z.enum([ 'turn-override', @@ -306,6 +310,8 @@ export const PythinkerConfigSchema = z.object({ background: BackgroundConfigSchema.optional(), experimental: ExperimentalConfigSchema.optional(), telemetry: z.boolean().optional(), + disableWorkflows: z.boolean().optional(), + workflowSizeGuideline: WorkflowSizeGuidelineSchema.optional(), raw: z.record(z.string(), z.unknown()).optional(), }); @@ -349,6 +355,8 @@ export const PythinkerConfigPatchSchema = z background: BackgroundConfigPatchSchema.optional(), experimental: ExperimentalConfigPatchSchema.optional(), telemetry: z.boolean().optional(), + disableWorkflows: z.boolean().optional(), + workflowSizeGuideline: WorkflowSizeGuidelineSchema.optional(), }) .strict(); diff --git a/packages/agent-core/src/config/toml.ts b/packages/agent-core/src/config/toml.ts index 8270e4aa..2aaffe99 100644 --- a/packages/agent-core/src/config/toml.ts +++ b/packages/agent-core/src/config/toml.ts @@ -486,6 +486,8 @@ export function configToTomlData(config: PythinkerConfig): Record = { readonly signal?: AbortSignal; readonly modelAlias?: string; readonly thinkingLevel?: string; + readonly workflowRunId?: string; + readonly workflowName?: string; + readonly outputSchema?: Record; }; export type SpawnQueuedSubagentTask = BaseQueuedSubagentTask & { @@ -70,7 +74,7 @@ export type QueuedSubagentTask = export type SubagentResult = { readonly task: QueuedSubagentTask; readonly agentId?: string; - readonly status: 'completed' | 'failed' | 'aborted'; + readonly status: 'completed' | 'failed' | 'aborted' | 'schema_error'; readonly state?: 'started' | 'not_started'; readonly result?: string; readonly usage?: TokenUsage; @@ -96,6 +100,20 @@ type RateLimitedOutcome = { readonly error: string; }; +/** + * A child turn failed because it never produced valid structured output + * (ErrorCodes.STRUCTURED_OUTPUT_MAX_RETRIES). The batch reports that child as + * status 'schema_error' instead of 'failed': the child ran, but its + * deliverable was the structured output, so a schema miss is not a generic + * child failure and must not fail the rest of the batch. + */ +export class StructuredOutputMaxRetriesError extends Error { + constructor(message: string) { + super(message); + this.name = 'StructuredOutputMaxRetriesError'; + } +} + type AttemptOutcome = SubagentResult | RateLimitedOutcome; type TaskState = { @@ -120,6 +138,7 @@ export class SubagentBatch { private readonly states: Array>; private readonly pending: Array>; private readonly results: Array | undefined>; + private readonly concurrencyLimit: number; private readonly active = new Set>(); private readonly controller = new AbortController(); private readonly batchSignal: AbortSignal | undefined; @@ -143,7 +162,15 @@ export class SubagentBatch { constructor( private readonly launcher: SubagentBatchLauncher, tasks: readonly QueuedSubagentTask[], + concurrencyLimit: number = MAX_CONCURRENT_WORKFLOW_SUBAGENTS, ) { + // Clamped rather than validated: a limit of 0 would stall the batch forever with no + // error to read, which is a worse failure than quietly running one at a time. + // NaN survives both Math.trunc and Math.max, and every `running < limit` test + // against it is false, so it stalls the batch the same way 0 would. + this.concurrencyLimit = Number.isFinite(concurrencyLimit) + ? Math.max(1, Math.trunc(concurrencyLimit)) + : MAX_CONCURRENT_WORKFLOW_SUBAGENTS; this.states = tasks.map((task, index) => ({ index, task, @@ -205,7 +232,8 @@ export class SubagentBatch { while ( this.normalLaunchCount < INITIAL_LAUNCH_LIMIT && this.pending.length > 0 && - !this.rateLimitMode + !this.rateLimitMode && + this.active.size < this.concurrencyLimit ) { this.startAttempt(this.pending.shift()!); this.normalLaunchCount += 1; @@ -222,6 +250,12 @@ export class SubagentBatch { this.normalLaunchTimer = setTimeout(() => { this.normalLaunchTimer = undefined; if (this.finished || this.rateLimitMode || this.pending.length === 0) return; + if (this.active.size >= this.concurrencyLimit) { + // The concurrency cap blocks the ramp; re-arm rather than dropping + // the wakeup so the batch retries once a slot frees. + this.schedule(); + return; + } this.startAttempt(this.pending.shift()!); this.normalLaunchCount += 1; this.schedule(); @@ -234,7 +268,7 @@ export class SubagentBatch { const now = Date.now(); this.recoverRateLimitCapacity(now); - if (this.active.size >= this.rateLimitCapacity) { + if (this.active.size >= this.effectiveRateLimitCapacity()) { this.scheduleRateLimitWakeup(this.nextRateLimitCapacityRecoveryAt(), now); return; } @@ -290,6 +324,9 @@ export class SubagentBatch { runInBackground: task.runInBackground, modelAlias: task.modelAlias, thinkingLevel: task.thinkingLevel, + workflowRunId: task.workflowRunId, + workflowName: task.workflowName, + outputSchema: task.outputSchema, signal: attempt.controller.signal, onReady: () => { this.markAttemptReady(attempt); @@ -342,7 +379,7 @@ export class SubagentBatch { const status = attempt.controller.signal.aborted && isUserCancellation(attempt.controller.signal.reason) ? 'aborted' - : 'failed'; + : this.attemptFailureStatus(error); return { task: attempt.state.task, agentId: attempt.state.agentId, @@ -352,6 +389,10 @@ export class SubagentBatch { }; } + private attemptFailureStatus(error: unknown): SubagentResult['status'] { + return error instanceof StructuredOutputMaxRetriesError ? 'schema_error' : 'failed'; + } + private markAttemptReady(attempt: ActiveAttempt): void { if (this.finished || attempt.ready || !this.active.has(attempt)) return; @@ -394,7 +435,7 @@ export class SubagentBatch { this.results[attempt.state.index] = { task: attempt.state.task, agentId: attempt.state.agentId, - status: 'failed', + status: this.attemptFailureStatus(error), error: error instanceof Error ? error.message : String(error), }; this.schedule(); @@ -472,6 +513,14 @@ export class SubagentBatch { this.lastCapacityShrinkAt = now; } + /** + * Rate-limit capacity grows by 1 per quiet window, so on a long batch it can drift past + * the concurrency cap. The cap holds in both phases. + */ + private effectiveRateLimitCapacity(): number { + return Math.min(this.rateLimitCapacity, this.concurrencyLimit); + } + private recoverRateLimitCapacity(now: number): void { const nextRecoveryAt = this.nextRateLimitCapacityRecoveryAt(); if (nextRecoveryAt > now) return; @@ -505,7 +554,7 @@ export class SubagentBatch { if (this.pending.length === 0) return; const nextWakeupAt = - this.active.size >= this.rateLimitCapacity + this.active.size >= this.effectiveRateLimitCapacity() ? this.nextRateLimitCapacityRecoveryAt() : Math.min( Math.max(this.nextRateLimitLaunchAt, this.nextPendingReadyAt()), diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index d9e1d554..1c092389 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -30,9 +30,10 @@ import { type SettledSubagentWorktree, type SubagentWorktree, } from './subagent-worktree'; -import type { Session } from './index'; +import type { AgentMeta, Session } from './index'; import { SubagentBatch, + StructuredOutputMaxRetriesError, type SubagentResult, type SubagentSuspendedEvent, type QueuedSubagentTask, @@ -41,6 +42,8 @@ import SUMMARY_CONTINUATION_PROMPT from './summary-continuation.md?raw'; export const DEFAULT_SUBAGENT_TIMEOUT_MS = 30 * 60 * 1000; export const DEFAULT_SUBAGENT_TIMEOUT_DESCRIPTION = '30 minutes'; +export const MAX_SUBAGENTS_PER_SESSION = 200; +export const MAX_SUBAGENT_SPAWN_DEPTH = 3; export type { SubagentResult as QueuedSubagentRunResult, @@ -91,6 +94,9 @@ export interface RunSubagentOptions { readonly suppressRateLimitFailureEvent?: boolean; readonly modelAlias?: string; readonly thinkingLevel?: string; + readonly workflowRunId?: string; + readonly workflowName?: string; + readonly outputSchema?: Record; readonly allowedTools?: readonly string[]; readonly cwd?: string; readonly forkContext?: boolean; @@ -135,6 +141,7 @@ export class SessionSubagentHost { if (options.forkContext === true && parent.type !== 'main') { throw new Error('Fork is not available inside a forked worker'); } + this.enforceSpawnCaps(); const profile = options.forkContext === true ? undefined : this.resolveProfile(parent, options.profileName); @@ -232,6 +239,51 @@ export class SessionSubagentHost { }; } + /** + * Runaway guards for new subagents. Both throw, which the batch scheduler turns into a + * single failed task rather than a failed batch, so the error text is written for the model + * that will read it. Only `spawn` is guarded — `resume` and `retry` reuse an agent that + * already exists and was already counted. + * + * The per-session count is read before `createAgent` awaits, so concurrent spawns can + * overshoot the cap by up to the batch concurrency limit. That is accepted: these are + * runaway guards, not quotas, and a reservation counter that has to be released on every + * failure path is a likelier source of leaks than the bounded overshoot it prevents. + */ + private enforceSpawnCaps(): void { + const agents = this.session.metadata.agents; + + let subagentCount = 0; + for (const meta of Object.values(agents)) { + if (meta.type === 'sub') subagentCount += 1; + } + if (subagentCount >= MAX_SUBAGENTS_PER_SESSION) { + throw new Error( + `This session is limited to ${String(MAX_SUBAGENTS_PER_SESSION)} subagents and can create no more; split the remaining work across fewer, larger subagents.`, + ); + } + + // The owner's depth is its number of ancestors, so the main agent is depth 0 and its + // child lands at depth 1. The walk is bounded by the agent count so malformed metadata + // with a parent cycle cannot hang the process. + let ownerDepth = 0; + let cursor: string | null = this.ownerAgentId; + const agentCount = Object.keys(agents).length; + for (let steps = 0; cursor !== null && steps <= agentCount; steps += 1) { + // Annotated because the Session -> Agent -> SessionSubagentHost module cycle makes tsc + // infer this read circularly (TS7022) when left implicit. + const meta: AgentMeta | undefined = agents[cursor]; + if (meta === undefined) break; + cursor = meta.parentAgentId; + if (cursor !== null) ownerDepth += 1; + } + if (ownerDepth + 1 > MAX_SUBAGENT_SPAWN_DEPTH) { + throw new Error( + `Subagents may nest at most ${String(MAX_SUBAGENT_SPAWN_DEPTH)} levels deep (the main agent is depth 0), and this parent agent is already at depth ${String(ownerDepth)}; split the work across fewer, larger subagents instead of nesting deeper.`, + ); + } + } + async resume(agentId: string, options: RunSubagentOptions): Promise { options.signal.throwIfAborted(); const { parent, child, profileName } = await this.ensureIdleSubagent(agentId); @@ -259,8 +311,11 @@ export class SessionSubagentHost { child.config.update( this.childModelConfig(parent, child, this.tryResolveProfile(parent, profileName), runOptions), ); - this.emitSubagentStarted(parent, agentId, runOptions.parentToolCallId); - const turnId = child.turn.retry('agent-host'); + this.emitSubagentStarted(parent, agentId, runOptions); + // The schema has to ride along: waitForChildCompletion still branches on + // runOptions.outputSchema, so a retry that dropped it would skip the + // continuation AND find no structured output, silently yielding prose. + const turnId = child.turn.retry('agent-host', runOptions.outputSchema); if (turnId === null) { throw new Error(`Agent instance "${agentId}" could not start a retry turn`); } @@ -460,8 +515,12 @@ export class SessionSubagentHost { if (gitContext) childPrompt = `${gitContext}\n\n${childPrompt}`; } - this.emitSubagentStarted(parent, childId, options.parentToolCallId); - const turnId = child.turn.prompt([{ type: 'text', text: childPrompt }], SUBAGENT_PROMPT_ORIGIN); + this.emitSubagentStarted(parent, childId, options); + const turnId = child.turn.prompt( + [{ type: 'text', text: childPrompt }], + SUBAGENT_PROMPT_ORIGIN, + options.outputSchema, + ); if (turnId === null) { throw new Error(`Agent instance "${childId}" could not start a turn`); } @@ -476,26 +535,33 @@ export class SessionSubagentHost { profileName: string, options: RunSubagentOptions, ): Promise { - await runChildTurnToCompletion(child, options.signal); + const turnResult = await runChildTurnToCompletion(child, options.signal); // A subagent that returns an overly terse summary leaves the parent // agent under-informed. Give it a bounded number of chances to expand // the handoff; if it is still short after that, accept it as-is rather - // than retrying indefinitely. + // than retrying indefinitely. When a schema is in effect the structured + // output — not the prose — is the deliverable, so the continuation never + // runs and the turn would cost an extra request without a schema. let result = lastAssistantText(child); - let remainingContinuations = SUMMARY_CONTINUATION_ATTEMPTS; - while (remainingContinuations > 0 && result.length < SUMMARY_MIN_LENGTH) { - remainingContinuations -= 1; - options.signal.throwIfAborted(); - child.turn.prompt([{ type: 'text', text: SUMMARY_CONTINUATION_PROMPT }], SUBAGENT_PROMPT_ORIGIN); - await runChildTurnToCompletion(child, options.signal); - result = lastAssistantText(child); + if (options.outputSchema === undefined) { + let remainingContinuations = SUMMARY_CONTINUATION_ATTEMPTS; + while (remainingContinuations > 0 && result.length < SUMMARY_MIN_LENGTH) { + remainingContinuations -= 1; + options.signal.throwIfAborted(); + child.turn.prompt([{ type: 'text', text: SUMMARY_CONTINUATION_PROMPT }], SUBAGENT_PROMPT_ORIGIN); + await runChildTurnToCompletion(child, options.signal); + result = lastAssistantText(child); + } + } else if (turnResult.structuredOutput !== undefined) { + result = JSON.stringify(turnResult.structuredOutput); } const usage = child.usage.data().total; parent.emitEvent({ type: 'subagent.completed', subagentId: childId, parentToolCallId: options.parentToolCallId, + workflowRunId: options.workflowRunId, resultSummary: result, usage, contextTokens: child.context.tokenCount, @@ -625,23 +691,27 @@ export class SessionSubagentHost { parentAgentId: this.ownerAgentId, description: options.description, dynamicWorkflowIndex: options.dynamicWorkflowIndex, + workflowRunId: options.workflowRunId, + workflowName: options.workflowName, runInBackground: options.runInBackground, }); parent.telemetry.track('subagent_created', { subagent_name: profileName, run_in_background: options.runInBackground, + workflow_run_id: options.workflowRunId, }); } private emitSubagentStarted( parent: Agent, childId: string, - parentToolCallId: string, + options: RunSubagentOptions, ): void { parent.emitEvent({ type: 'subagent.started', subagentId: childId, - parentToolCallId, + parentToolCallId: options.parentToolCallId, + workflowRunId: options.workflowRunId, }); } @@ -656,18 +726,27 @@ export class SessionSubagentHost { type: 'subagent.failed', subagentId: childId, parentToolCallId: options.parentToolCallId, + workflowRunId: options.workflowRunId, error: error instanceof Error ? error.message : String(error), }); } } -async function runChildTurnToCompletion(child: Agent, signal: AbortSignal): Promise { +async function runChildTurnToCompletion( + child: Agent, + signal: AbortSignal, +): Promise<{ readonly structuredOutput?: unknown }> { const completion = await child.turn.waitForCurrentTurn(signal); const turnEnded = completion.event; if (turnEnded.reason !== 'completed') { if (turnEnded.error?.code === ErrorCodes.PROVIDER_RATE_LIMIT) { throw providerRateLimitErrorFromPayload(turnEnded.error); } + if (turnEnded.error?.code === ErrorCodes.STRUCTURED_OUTPUT_MAX_RETRIES) { + throw new StructuredOutputMaxRetriesError( + `[${turnEnded.error.code}] ${turnEnded.error.message}`, + ); + } throw new Error( turnEnded.error === undefined ? `Subagent turn ${turnEnded.reason}` @@ -677,6 +756,7 @@ async function runChildTurnToCompletion(child: Agent, signal: AbortSignal): Prom if (completion.stopReason === 'max_tokens') { throw new Error(`${SUBAGENT_MAX_TOKENS_ERROR}.`); } + return { structuredOutput: turnEnded.structuredOutput }; } function providerRateLimitErrorFromPayload(error: PythinkerErrorPayload): APIProviderRateLimitError { diff --git a/packages/agent-core/src/tools/builtin/collaboration/dynamic-workflow.md b/packages/agent-core/src/tools/builtin/collaboration/dynamic-workflow.md index 35b81104..0b7b9120 100644 --- a/packages/agent-core/src/tools/builtin/collaboration/dynamic-workflow.md +++ b/packages/agent-core/src/tools/builtin/collaboration/dynamic-workflow.md @@ -6,6 +6,8 @@ Use `resume_agent_ids` to continue subagents that already exist from earlier wor Use `model` and `effort` to run this workflow's subagents on a different model than the one orchestrating them, such as a cheaper or faster model for mechanical work while the orchestration stays on the current model. Both apply to every subagent in the call. Omitting either falls back to the subagent type's own setting, and then to your current setting. A `model` that is not a configured alias also falls back to your current model rather than failing the workflow. +The result carries a `run_id` attribute on its root element identifying the whole workflow run. Cite that id when referring to this run in later calls. + Use enough subagents to keep the work focused and parallel. DynamicWorkflow supports up to 128 subagents, and launches are queued automatically, so it is safe to split large tasks into many clear, independent items. Workflow subagents have no automatic timeout; they run until completion, failure, or user cancellation. If `DynamicWorkflow` is called, that call must be the only tool call in the response. diff --git a/packages/agent-core/src/tools/builtin/collaboration/dynamic-workflow.ts b/packages/agent-core/src/tools/builtin/collaboration/dynamic-workflow.ts index bcf2b325..190ec7a5 100644 --- a/packages/agent-core/src/tools/builtin/collaboration/dynamic-workflow.ts +++ b/packages/agent-core/src/tools/builtin/collaboration/dynamic-workflow.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +import type { WorkflowWarningEvent } from '@pythoughts/protocol'; + import type { DynamicWorkflowMode } from '../../../agent/dynamic-workflow'; import type { BuiltinTool } from '../../../agent/tool'; import type { @@ -8,12 +10,43 @@ import type { } from '../../../session/subagent-host'; import { ToolAccesses } from '../../../loop/tool-access'; import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { parseBooleanEnv, resolveConfigValue, type PythinkerConfig, type WorkflowSizeGuideline } from '../../../config'; +import { + DEFAULT_WORKFLOW_SIZE_GUIDELINE, + workflowSizeGuidelineNote, + workflowSizeGuidelineTarget, +} from '../../../agent/dynamic-workflow/size-guideline'; +import { generateWorkflowRunId } from '../../../agent/dynamic-workflow/run-id'; import { toInputJsonSchema } from '../../support/input-schema'; import DYNAMIC_WORKFLOW_DESCRIPTION from './dynamic-workflow.md?raw'; const DEFAULT_SUBAGENT_TYPE = 'coder'; const PROMPT_TEMPLATE_PLACEHOLDER = '{{item}}'; const MAX_DYNAMIC_WORKFLOW_SUBAGENTS = 128; +/** + * Warning threshold when the operator chose `unrestricted`, so even an + * unrestricted operator still hears about a very large fan-out. + */ +const UNRESTRICTED_WARNING_THRESHOLD = 25; + +export const DISABLE_WORKFLOWS_ENV = 'PYTHINKER_CODE_DISABLE_WORKFLOWS'; + +/** + * Dynamic Workflow is off when the env var says so, else when the config key says so. + * Env wins so an operator can force it off without editing config. + */ +export function isDynamicWorkflowDisabled( + config: Pick | undefined, + env: Readonly> = process.env, +): boolean { + return resolveConfigValue({ + env, + envKey: DISABLE_WORKFLOWS_ENV, + configValue: config?.disableWorkflows, + defaultValue: false, + parseEnv: parseBooleanEnv, + }); +} export const DynamicWorkflowToolInputSchema = z .object({ @@ -39,8 +72,12 @@ export const DynamicWorkflowToolInputSchema = z `Optional prompt template for each subagent. The ${PROMPT_TEMPLATE_PLACEHOLDER} placeholder is replaced with each item value. When omitted, each item is used as a complete prompt.`, ), items: z - .array(z.string().trim().min(1)) - .max(MAX_DYNAMIC_WORKFLOW_SUBAGENTS) + // Deliberately unconstrained per item and unbounded in length: argument + // validation rejects the WHOLE call before the tool runs, so a trailing + // empty string — or one blank entry pushing a full list one over the cap + // — would cost a re-send of every prompt. The tool drops blanks, reports + // how many, and enforces the real cap against the surviving count. + .array(z.string()) .optional() .describe( `Each item launches one new subagent. Items fill ${PROMPT_TEMPLATE_PLACEHOLDER} when prompt_template is provided; otherwise they are complete prompts.`, @@ -67,6 +104,12 @@ export const DynamicWorkflowToolInputSchema = z .describe( 'Reasoning effort for every subagent in this workflow. Defaults to the subagent type profile effort, then this agent effort.', ), + output_schema: z + .record(z.string(), z.unknown()) + .optional() + .describe( + 'JSON Schema every subagent in this workflow must satisfy. Each subagent returns its result by calling the StructuredOutput tool instead of writing a prose summary. A subagent that cannot produce conforming output is reported with outcome="schema_error" and does not fail the batch.', + ), }) .strict(); @@ -92,7 +135,7 @@ type DynamicWorkflowSpec = DynamicWorkflowSpawnSpec | DynamicWorkflowResumeSpec; interface DynamicWorkflowRunResult { readonly spec: DynamicWorkflowSpec; readonly agentId?: string; - readonly status: 'completed' | 'failed' | 'aborted'; + readonly status: 'completed' | 'failed' | 'aborted' | 'schema_error'; readonly state?: 'started' | 'not_started'; readonly result?: string; readonly error?: string; @@ -100,16 +143,28 @@ interface DynamicWorkflowRunResult { export class DynamicWorkflowTool implements BuiltinTool { readonly name = 'DynamicWorkflow' as const; - readonly description = DYNAMIC_WORKFLOW_DESCRIPTION; + readonly description: string; readonly parameters: Record = toInputJsonSchema(DynamicWorkflowToolInputSchema); constructor( private readonly subagentHost: SessionSubagentHost, private readonly dynamicWorkflowMode: DynamicWorkflowMode, - ) {} + private readonly sizeGuideline: WorkflowSizeGuideline = DEFAULT_WORKFLOW_SIZE_GUIDELINE, + private readonly emitEvent?: (event: WorkflowWarningEvent) => void, + ) { + const sizeNote = workflowSizeGuidelineNote(this.sizeGuideline); + this.description = + sizeNote === undefined + ? DYNAMIC_WORKFLOW_DESCRIPTION + : `${DYNAMIC_WORKFLOW_DESCRIPTION}\n\n${sizeNote}`; + } resolveExecution(args: DynamicWorkflowToolInput): ToolExecution { - const agentCount = (args.items?.length ?? 0) + Object.keys(args.resume_agent_ids ?? {}).length; + // Count the items that will actually launch, so the panel never advertises + // a subagent that was never going to run. + const agentCount = + normalizeWorkflowItems(args.items).items.length + + Object.keys(args.resume_agent_ids ?? {}).length; return { accesses: ToolAccesses.all(), description: `Launching Dynamic Workflow: ${args.description}`, @@ -148,6 +203,19 @@ export class DynamicWorkflowTool implements BuiltinTool { const profileName = normalizeOptionalString(args.subagent_type) ?? DEFAULT_SUBAGENT_TYPE; const specs = createDynamicWorkflowSpecs(args, (agentId) => this.subagentHost.getDynamicWorkflowItem(agentId)); + const runId = generateWorkflowRunId(); + const threshold = + workflowSizeGuidelineTarget(this.sizeGuideline) ?? UNRESTRICTED_WARNING_THRESHOLD; + if (specs.length > threshold) { + this.emitEvent?.({ + type: 'workflow.warning', + workflowRunId: runId, + parentToolCallId: toolCallId, + agentCount: specs.length, + threshold, + message: `This Dynamic Workflow will launch ${String(specs.length)} subagents, above the advisory ceiling of ${String(threshold)}; the run is proceeding anyway.`, + }); + } // Workflow tasks intentionally carry no timeout: they run until they // complete, fail, or the user cancels (see dynamic-workflow.md). const tasks = specs.map((spec): QueuedSubagentTask => { @@ -166,6 +234,13 @@ export class DynamicWorkflowTool implements BuiltinTool ({ spec: task.data, ...result }))); + const rendered = renderDynamicWorkflowResults( + results.map(({ task, ...result }) => ({ spec: task.data, ...result })), + runId, + ); + const { dropped } = normalizeWorkflowItems(args.items); + if (dropped === 0) return rendered; + // After the envelope, never before it: consumers match the result document + // anchored at the start of the output, so a prefix makes a successful run + // parse as an unsupported result. + return `${rendered}\n${droppedItemsNote(dropped)}`; } } @@ -193,12 +277,17 @@ function createDynamicWorkflowSpecs( agentId: agentId.trim(), prompt: prompt.trim(), })); - const items = (args.items ?? []).map((item) => item.trim()); + const { items, dropped } = normalizeWorkflowItems(args.items); const itemCount = items.length; const resumeCount = resumeEntries.length; const totalCount = resumeCount + itemCount; if (!hasMinimumDynamicWorkflowInputs(itemCount, resumeCount)) { - throw new Error('DynamicWorkflow requires at least 2 items unless resume_agent_ids is provided.'); + // Name the dropped items here: without it the caller reads "requires at + // least 2 items" while looking at a list that had enough entries. + const droppedNote = dropped === 0 ? '' : ` ${droppedItemsNote(dropped)}`; + throw new Error( + `DynamicWorkflow requires at least 2 items unless resume_agent_ids is provided.${droppedNote}`, + ); } if (totalCount > MAX_DYNAMIC_WORKFLOW_SUBAGENTS) { throw new Error(`DynamicWorkflow supports at most ${String(MAX_DYNAMIC_WORKFLOW_SUBAGENTS)} subagents.`); @@ -248,6 +337,28 @@ function hasMinimumDynamicWorkflowInputs(itemCount: number, resumeCount: number) return resumeCount > 0 || itemCount >= 2; } +/** + * Trims items and drops the blank ones, reporting how many went. + * + * Models routinely emit a trailing empty string when building the list. Per + * item that is not worth failing the call over — but it is worth saying out + * loud, because a silently shorter workflow looks identical to one the model + * sized correctly. + */ +function normalizeWorkflowItems(raw: readonly string[] | undefined): { + items: string[]; + dropped: number; +} { + const trimmed = (raw ?? []).map((item) => item.trim()); + const items = trimmed.filter((item) => item.length > 0); + return { items, dropped: trimmed.length - items.length }; +} + +function droppedItemsNote(dropped: number): string { + const plural = dropped === 1 ? 'item was' : 'items were'; + return `Note: ${String(dropped)} empty ${plural} ignored; the workflow ran without them.`; +} + function childDescription(workflowDescription: string, index: number, profileName: string): string { return `${workflowDescription} #${String(index)} (${profileName})`; } @@ -255,16 +366,20 @@ function childDescription(workflowDescription: string, index: number, profileNam // Render results as an XML block that the consumer parses, so every // interpolated value (agent ids, item names, result/error bodies — all user // data) must be escaped; an unescaped `<` or `>` would break the structure. -function renderDynamicWorkflowResults(results: readonly DynamicWorkflowRunResult[]): string { +function renderDynamicWorkflowResults( + results: readonly DynamicWorkflowRunResult[], + runId: string, +): string { const completed = results.filter((result) => result.status === 'completed').length; const failed = results.filter((result) => result.status === 'failed').length; const aborted = results.filter((result) => result.status === 'aborted').length; + const schemaError = results.filter((result) => result.status === 'schema_error').length; const shouldRenderResumeHint = results.some( (result) => result.status !== 'completed' && result.agentId !== undefined, ); const lines = [ - '', - `${renderDynamicWorkflowSummary(completed, failed, aborted)}`, + ``, + `${renderDynamicWorkflowSummary(completed, failed, aborted, schemaError)}`, ]; if (shouldRenderResumeHint) { @@ -294,11 +409,19 @@ function normalizeOptionalString(value: string | undefined): string | undefined return trimmed.length > 0 ? trimmed : undefined; } -function renderDynamicWorkflowSummary(completed: number, failed: number, aborted = 0): string { +function renderDynamicWorkflowSummary( + completed: number, + failed: number, + aborted = 0, + schemaError = 0, +): string { const parts: string[] = []; if (completed > 0) parts.push(`completed: ${String(completed)}`); if (failed > 0) parts.push(`failed: ${String(failed)}`); if (aborted > 0) parts.push(`aborted: ${String(aborted)}`); + // A schema_error child DID run, so it is neither completed nor aborted; + // it is counted separately and rendered with outcome="schema_error". + if (schemaError > 0) parts.push(`schema_error: ${String(schemaError)}`); return parts.join(', '); } diff --git a/packages/agent-core/test/agent/tool.test.ts b/packages/agent-core/test/agent/tool.test.ts index 373431bb..b4f8514a 100644 --- a/packages/agent-core/test/agent/tool.test.ts +++ b/packages/agent-core/test/agent/tool.test.ts @@ -405,6 +405,34 @@ describe('Agent tools', () => { expect(ctx.agent.tools.loopTools.some((tool) => tool.name === 'AgentSwarm')).toBe(false); }); + it('skips DynamicWorkflow registration when disableWorkflows is set', () => { + const subagentHost = { getProfiles: () => ({}) } as unknown as SessionSubagentHost; + + const ctx = testAgent({ + subagentHost, + experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS), + initialConfig: { providers: {}, disableWorkflows: true }, + }); + ctx.configure({ tools: ['DynamicWorkflow'] }); + + expect(ctx.agent.tools.loopTools.some((tool) => tool.name === 'DynamicWorkflow')).toBe(false); + }); + + it('registers DynamicWorkflow with the configured size guideline in its description', () => { + const subagentHost = { getProfiles: () => ({}) } as unknown as SessionSubagentHost; + + const ctx = testAgent({ + subagentHost, + experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS), + initialConfig: { providers: {}, workflowSizeGuideline: 'small' }, + }); + ctx.configure({ tools: ['DynamicWorkflow'] }); + + const tool = ctx.agent.tools.loopTools.find((tool) => tool.name === 'DynamicWorkflow'); + expect(tool).toBeDefined(); + expect(tool!.description).toContain('about 5 subagents'); + }); + it('rejects a user tool whose name collides with a builtin before recording it', () => { const ctx = testAgent(); ctx.configure(); diff --git a/packages/agent-core/test/agent/turn.test.ts b/packages/agent-core/test/agent/turn.test.ts index f5a98508..4a0a06e9 100644 --- a/packages/agent-core/test/agent/turn.test.ts +++ b/packages/agent-core/test/agent/turn.test.ts @@ -413,6 +413,32 @@ describe('Agent turn flow', () => { await ctx.expectResumeMatches(); }); + it('carries the workflow size guideline into the dynamic workflow mode reminder', async () => { + const restricted = testAgent({ initialConfig: { providers: {}, workflowSizeGuideline: 'small' } }); + restricted.configure(); + await restricted.rpc.enterDynamicWorkflow({ trigger: 'manual' }); + + const restrictedReminder = restricted.agent.context.history.at(-1); + expect(restrictedReminder?.origin).toEqual({ + kind: 'injection', + variant: 'dynamic_workflow_mode', + }); + expect(JSON.stringify(restrictedReminder)).toContain('about 5 subagents'); + + const unrestricted = testAgent({ + initialConfig: { providers: {}, workflowSizeGuideline: 'unrestricted' }, + }); + unrestricted.configure(); + await unrestricted.rpc.enterDynamicWorkflow({ trigger: 'manual' }); + + const unrestrictedReminder = unrestricted.agent.context.history.at(-1); + expect(unrestrictedReminder?.origin).toEqual({ + kind: 'injection', + variant: 'dynamic_workflow_mode', + }); + expect(JSON.stringify(unrestrictedReminder)).not.toContain('Workflow size guideline:'); + }); + it('exits task dynamic workflow mode after a turn completes normally', async () => { const ctx = testAgent(); ctx.configure(); diff --git a/packages/agent-core/test/config/configs.test.ts b/packages/agent-core/test/config/configs.test.ts index b452bad4..e88e4fe4 100644 --- a/packages/agent-core/test/config/configs.test.ts +++ b/packages/agent-core/test/config/configs.test.ts @@ -304,6 +304,38 @@ micro_compaction = false expect(parseConfigString(text, configPath).experimental).toEqual(config.experimental); }); + it('round-trips disableWorkflows as disable_workflows', async () => { + const dir = makeTempDir(); + const configPath = join(dir, 'disable-workflows.toml'); + + expect(parseConfigString('disable_workflows = true\n', configPath).disableWorkflows).toBe(true); + + // Written from an in-memory config with no `raw`, so the key only reaches the + // file through the scalar-field writer rather than being copied from `raw`. + await writeConfigFile(configPath, { providers: {}, disableWorkflows: true }); + const text = await readFile(configPath, 'utf-8'); + + expect(text).toContain('disable_workflows = true'); + expect(parseConfigString(text, configPath).disableWorkflows).toBe(true); + }); + + it('round-trips workflowSizeGuideline as workflow_size_guideline', async () => { + const dir = makeTempDir(); + const configPath = join(dir, 'workflow-size-guideline.toml'); + + expect( + parseConfigString('workflow_size_guideline = "small"\n', configPath).workflowSizeGuideline, + ).toBe('small'); + + // Written from an in-memory config with no `raw`, so the key only reaches the + // file through the scalar-field writer rather than being copied from `raw`. + await writeConfigFile(configPath, { providers: {}, workflowSizeGuideline: 'small' }); + const text = await readFile(configPath, 'utf-8'); + + expect(text).toContain('workflow_size_guideline = "small"'); + expect(parseConfigString(text, configPath).workflowSizeGuideline).toBe('small'); + }); + it('accepts obsolete experimental feature keys as inert config', async () => { const dir = makeTempDir(); const configPath = join(dir, 'obsolete-experimental.toml'); diff --git a/packages/agent-core/test/harness/skill-session.test.ts b/packages/agent-core/test/harness/skill-session.test.ts index 772fd93c..3d047e0a 100644 --- a/packages/agent-core/test/harness/skill-session.test.ts +++ b/packages/agent-core/test/harness/skill-session.test.ts @@ -458,20 +458,25 @@ describe('HarnessAPI session skills', () => { }); await waitForEvent(events, (event) => event.type === 'skill.activated'); - expect(telemetryRecords).toContainEqual({ - event: 'skill_invoked', - sessionId: created.id, - properties: { - skill_name: 'review-flow', - trigger: 'user-slash', - }, - }); - expect(telemetryRecords).toContainEqual({ - event: 'flow_invoked', - sessionId: created.id, - properties: { - flow_name: 'review-flow', - }, + // The telemetry records are written independently of `skill.activated`, so + // the event is not a barrier for them — asserting straight after it raced + // and failed intermittently on a loaded machine. + await vi.waitFor(() => { + expect(telemetryRecords).toContainEqual({ + event: 'skill_invoked', + sessionId: created.id, + properties: { + skill_name: 'review-flow', + trigger: 'user-slash', + }, + }); + expect(telemetryRecords).toContainEqual({ + event: 'flow_invoked', + sessionId: created.id, + properties: { + flow_name: 'review-flow', + }, + }); }); }); diff --git a/packages/agent-core/test/session/subagent-batch.test.ts b/packages/agent-core/test/session/subagent-batch.test.ts index 6905a4c6..5f181fe9 100644 --- a/packages/agent-core/test/session/subagent-batch.test.ts +++ b/packages/agent-core/test/session/subagent-batch.test.ts @@ -66,6 +66,211 @@ describe('SubagentBatch scheduling contract', () => { } }); + it('caps live attempts at the concurrency limit and completes all tasks in order', async () => { + vi.useFakeTimers(); + try { + const { runBatch, attempts } = createMockBatchRunner({}, 2); + const running = runBatch( + Array.from({ length: 6 }, (_, index) => queuedTask(index + 1)), + { signal }, + ); + + await vi.advanceTimersByTimeAsync(0); + expect(attempts).toHaveLength(2); + + // Each completion frees a slot for the next queued task; at most two + // attempts are ever live at once. + attempts[0]!.outcome.resolve({ + task: attempts[0]!.task, + agentId: 'agent-1', + status: 'completed', + result: 'completed 1', + }); + await vi.advanceTimersByTimeAsync(0); + expect(attempts).toHaveLength(3); + expect(attempts[2]!.task.data).toBe(3); + + attempts[1]!.outcome.resolve({ + task: attempts[1]!.task, + agentId: 'agent-2', + status: 'completed', + result: 'completed 2', + }); + await vi.advanceTimersByTimeAsync(0); + expect(attempts).toHaveLength(4); + expect(attempts[3]!.task.data).toBe(4); + + attempts[2]!.outcome.resolve({ + task: attempts[2]!.task, + agentId: 'agent-3', + status: 'completed', + result: 'completed 3', + }); + await vi.advanceTimersByTimeAsync(0); + expect(attempts).toHaveLength(5); + expect(attempts[4]!.task.data).toBe(5); + + attempts[3]!.outcome.resolve({ + task: attempts[3]!.task, + agentId: 'agent-4', + status: 'completed', + result: 'completed 4', + }); + await vi.advanceTimersByTimeAsync(0); + expect(attempts).toHaveLength(5); + + // The initial launch limit of 5 is exhausted, so task 6 starts when + // the ramp timer fires with a free slot. + await vi.advanceTimersByTimeAsync(700); + expect(attempts).toHaveLength(6); + expect(attempts[5]!.task.data).toBe(6); + + attempts[4]!.outcome.resolve({ + task: attempts[4]!.task, + agentId: 'agent-5', + status: 'completed', + result: 'completed 5', + }); + attempts[5]!.outcome.resolve({ + task: attempts[5]!.task, + agentId: 'agent-6', + status: 'completed', + result: 'completed 6', + }); + await vi.advanceTimersByTimeAsync(0); + + const results = await running; + expect(results.map((result) => result.task.data)).toEqual([1, 2, 3, 4, 5, 6]); + expect(results.every((result) => result.status === 'completed')).toBe(true); + } finally { + vi.useRealTimers(); + } + }); + + it.each([ + ['NaN', Number.NaN], + ['Infinity', Number.POSITIVE_INFINITY], + ])('falls back to the default limit for a %s concurrency limit', async (_label, limit) => { + vi.useFakeTimers(); + try { + // NaN survives Math.trunc and Math.max, and `running < NaN` is always + // false, so the batch would launch nothing and never finish. + const { runBatch, attempts } = createMockBatchRunner({}, limit); + const running = runBatch([queuedTask(1), queuedTask(2)], { signal }); + + await vi.advanceTimersByTimeAsync(0); + expect(attempts).toHaveLength(2); + + for (const [index, attempt] of attempts.entries()) { + attempt.outcome.resolve({ + task: attempt.task, + agentId: `agent-${String(index + 1)}`, + status: 'completed', + result: `completed ${String(index + 1)}`, + }); + } + await vi.advanceTimersByTimeAsync(0); + + const results = await running; + expect(results.map((result) => result.task.data)).toEqual([1, 2]); + } finally { + vi.useRealTimers(); + } + }); + + it('re-arms the launch timer while the concurrency cap blocks the ramp', async () => { + vi.useFakeTimers(); + try { + const { runBatch, attempts } = createMockBatchRunner({}, 2); + const running = runBatch( + Array.from({ length: 6 }, (_, index) => queuedTask(index + 1)), + { signal }, + ); + + await vi.advanceTimersByTimeAsync(0); + expect(attempts).toHaveLength(2); + + attempts[0]!.outcome.resolve({ + task: attempts[0]!.task, + agentId: 'agent-1', + status: 'completed', + result: 'completed 1', + }); + await vi.advanceTimersByTimeAsync(0); + expect(attempts).toHaveLength(3); + expect(attempts[2]!.task.data).toBe(3); + + // The t=0 timer fires while both slots are full. The batch must re-arm + // it rather than drop the wakeup; a dropped wakeup is only replaced by + // the next completion, which shifts every later launch one timer period + // later and strands the last task behind the exhausted initial launch + // limit. + await vi.advanceTimersByTimeAsync(700); + expect(attempts).toHaveLength(3); + + await vi.advanceTimersByTimeAsync(1); + attempts[1]!.outcome.resolve({ + task: attempts[1]!.task, + agentId: 'agent-2', + status: 'completed', + result: 'completed 2', + }); + await vi.advanceTimersByTimeAsync(0); + expect(attempts).toHaveLength(4); + expect(attempts[3]!.task.data).toBe(4); + + // Second blocked fire at t=1400 (re-armed from t=700). + await vi.advanceTimersByTimeAsync(699); + expect(attempts).toHaveLength(4); + + await vi.advanceTimersByTimeAsync(1); + attempts[2]!.outcome.resolve({ + task: attempts[2]!.task, + agentId: 'agent-3', + status: 'completed', + result: 'completed 3', + }); + attempts[3]!.outcome.resolve({ + task: attempts[3]!.task, + agentId: 'agent-4', + status: 'completed', + result: 'completed 4', + }); + await vi.advanceTimersByTimeAsync(0); + expect(attempts).toHaveLength(5); + expect(attempts[4]!.task.data).toBe(5); + + // Task 6 can no longer be started by a completion (the initial launch + // limit of 5 is exhausted), so it depends entirely on the re-armed + // timer firing after a slot frees. + await vi.advanceTimersByTimeAsync(698); + expect(attempts).toHaveLength(5); + + await vi.advanceTimersByTimeAsync(1); + expect(attempts).toHaveLength(6); + expect(attempts[5]!.task.data).toBe(6); + + attempts[4]!.outcome.resolve({ + task: attempts[4]!.task, + agentId: 'agent-5', + status: 'completed', + result: 'completed 5', + }); + attempts[5]!.outcome.resolve({ + task: attempts[5]!.task, + agentId: 'agent-6', + status: 'completed', + result: 'completed 6', + }); + await vi.advanceTimersByTimeAsync(0); + + const results = await running; + expect(results.map((result) => result.task.data)).toEqual([1, 2, 3, 4, 5, 6]); + } finally { + vi.useRealTimers(); + } + }); + it('rate-limit phase starts when the first provider rate limit stops the normal ramp', async () => { vi.useFakeTimers(); try { @@ -739,6 +944,7 @@ type MockBatchRunnerOptions = { function createMockBatchRunner( options: MockBatchRunnerOptions = {}, + concurrencyLimit?: number, ): { readonly runBatch: ( tasks: readonly QueuedSubagentTask[], @@ -809,7 +1015,7 @@ function createMockBatchRunner( ...task, signal: task.signal ?? runOptions?.signal, })); - return new SubagentBatch(host, activeTasks as readonly QueuedSubagentTask[]).run(); + return new SubagentBatch(host, activeTasks as readonly QueuedSubagentTask[], concurrencyLimit).run(); }, attempts, }; diff --git a/packages/agent-core/test/session/subagent-host.test.ts b/packages/agent-core/test/session/subagent-host.test.ts index 2e087652..680d3c2c 100644 --- a/packages/agent-core/test/session/subagent-host.test.ts +++ b/packages/agent-core/test/session/subagent-host.test.ts @@ -17,6 +17,8 @@ import { settleSubagentWorktree, } from '../../src/session/subagent-worktree'; import { + MAX_SUBAGENTS_PER_SESSION, + MAX_SUBAGENT_SPAWN_DEPTH, SessionSubagentHost, type QueuedSubagentTask, } from '../../src/session/subagent-host'; @@ -689,6 +691,7 @@ describe('SessionSubagentHost', () => { const host = new SessionSubagentHost( { agents: new Map([['main', parent.agent]]), + metadata: { agents: {} }, ensureAgentResumed: vi.fn(async () => parent.agent), createAgent, } as never, @@ -715,6 +718,7 @@ describe('SessionSubagentHost', () => { const host = new SessionSubagentHost( { agents: new Map([['main', parent.agent]]), + metadata: { agents: {} }, ensureAgentResumed: vi.fn(async () => parent.agent), createAgent, } as never, @@ -734,6 +738,148 @@ describe('SessionSubagentHost', () => { expect(createAgent).not.toHaveBeenCalled(); }); + it('rejects a spawn when the session has reached its subagent cap', async () => { + const parent = testAgent(); + parent.configure(); + const child = testAgent({ type: 'sub' }); + const metadataAgents = Object.fromEntries( + Array.from({ length: MAX_SUBAGENTS_PER_SESSION }, (_, index) => [ + `agent-${String(index)}`, + { type: 'sub' as const, parentAgentId: 'main' }, + ]), + ); + const session = fakeSession(parent.agent, child.agent, metadataAgents); + const host = new SessionSubagentHost(session, 'main'); + + await expect( + host.spawn({ + profileName: 'coder', + parentToolCallId: 'call_agent', + prompt: 'Implement the fix', + description: 'Fix bug', + runInBackground: false, + signal, + }), + ).rejects.toThrow(String(MAX_SUBAGENTS_PER_SESSION)); + expect(session.createAgent).not.toHaveBeenCalled(); + }); + + it('rejects a spawn when the owner is already at the nesting depth cap', async () => { + const parent = testAgent(); + parent.configure(); + const child = testAgent({ type: 'sub' }); + const session = Object.assign( + fakeSession(parent.agent, child.agent, { + main: { type: 'main', parentAgentId: null }, + 'agent-0': { type: 'sub', parentAgentId: 'main' }, + 'agent-1': { type: 'sub', parentAgentId: 'agent-0' }, + 'agent-2': { type: 'sub', parentAgentId: 'agent-1' }, + }), + { + ensureAgentResumed: vi.fn(async () => parent.agent), + }, + ); + // 'agent-2' is at depth 3 (main is depth 0), so its child would be at + // depth 4 — beyond MAX_SUBAGENT_SPAWN_DEPTH. + const host = new SessionSubagentHost(session, 'agent-2'); + + await expect( + host.spawn({ + profileName: 'coder', + parentToolCallId: 'call_agent', + prompt: 'Nest too deep', + description: 'Too deep', + runInBackground: false, + signal, + }), + ).rejects.toThrow(String(MAX_SUBAGENT_SPAWN_DEPTH)); + expect(session.createAgent).not.toHaveBeenCalled(); + }); + + it('allows a spawn one nesting level shallower than the depth cap', async () => { + const parent = testAgent(); + parent.configure(); + parent.newEvents(); + + const summary = + 'Completed the delegated subagent task from the shallower nesting level and returned a detailed technical handoff to the parent agent, so the work continues without repeating any of it. '.repeat( + 2, + ); + const child = testAgent({ type: 'sub' }); + child.mockNextResponse({ type: 'text', text: summary }); + const session = Object.assign( + fakeSession(parent.agent, child.agent, { + main: { type: 'main', parentAgentId: null }, + 'agent-0': { type: 'sub', parentAgentId: 'main' }, + 'agent-1': { type: 'sub', parentAgentId: 'agent-0' }, + }), + { + ensureAgentResumed: vi.fn(async () => parent.agent), + }, + ); + // 'agent-1' is at depth 2, so its child lands at depth 3 — exactly the + // cap, and therefore allowed. + const host = new SessionSubagentHost(session, 'agent-1'); + + const handle = await host.spawn({ + profileName: 'coder', + parentToolCallId: 'call_agent', + prompt: 'Work at the allowed depth', + description: 'Allowed depth', + runInBackground: false, + signal, + }); + await expect(handle.completion).resolves.toMatchObject({ result: summary.trim() }); + expect(session.createAgent).toHaveBeenCalledTimes(1); + }); + + it('resume is not blocked by the per-session subagent cap', async () => { + const parent = testAgent(); + parent.configure(); + + const child = testAgent({ type: 'sub' }); + child.configure({ tools: ['Read'] }); + child.agent.useProfile( + profile({ name: 'explore', tools: ['Read'], systemPrompt: 'explore prompt' }), + ); + child.agent.context.appendUserMessage([{ type: 'text', text: 'Earlier context' }]); + child.mockNextResponse({ + type: 'text', + text: 'Resumed the subagent from its earlier context and carried the task through to completion, then reported a full and detailed technical summary so the parent agent can continue without repeating prior work.', + }); + + const otherAgents = Object.fromEntries( + Array.from({ length: MAX_SUBAGENTS_PER_SESSION - 1 }, (_, index) => [ + `agent-${String(index + 1)}`, + { type: 'sub' as const, parentAgentId: 'main' }, + ]), + ); + const session = fakeSession(parent.agent, child.agent, { + 'agent-0': { type: 'sub', parentAgentId: 'main' }, + ...otherAgents, + }); + const host = new SessionSubagentHost(session, 'main'); + + const handle = await host.resume('agent-0', { + parentToolCallId: 'call_agent', + prompt: 'Continue from context', + description: 'Continue work', + runInBackground: false, + signal, + }); + + expect(handle).toMatchObject({ + agentId: 'agent-0', + profileName: 'explore', + resumed: true, + }); + await expect(handle.completion).resolves.toMatchObject({ + result: + 'Resumed the subagent from its earlier context and carried the task through to completion, then reported a full and detailed technical summary so the parent agent can continue without repeating prior work.', + }); + expect(session.createAgent).not.toHaveBeenCalled(); + }); + it('cancels the child turn when the caller signal aborts', async () => { const parent = testAgent(); parent.configure(); @@ -1022,6 +1168,126 @@ describe('SessionSubagentHost', () => { expect(child.llmCalls).toHaveLength(1); }); + it('passes an output schema to the child turn and returns the structured output', async () => { + const parent = testAgent(); + parent.configure(); + parent.newEvents(); + + const outputSchema = { + type: 'object', + properties: { answer: { type: 'string' } }, + required: ['answer'], + }; + const child = testAgent({ type: 'sub' }); + child.mockNextResponse( + { type: 'text', text: 'I will return structured output.' }, + { + type: 'function', + id: 'call_structured_output', + name: 'StructuredOutput', + arguments: JSON.stringify({ answer: 'done' }), + }, + ); + const session = fakeSession(parent.agent, child.agent); + const host = new SessionSubagentHost(session, 'main'); + const promptSpy = vi.spyOn(child.agent.turn, 'prompt'); + + const handle = await host.spawn({ + profileName: 'coder', + outputSchema, + parentToolCallId: 'call_agent', + prompt: 'Return structured output', + description: 'Structured task', + runInBackground: false, + signal, + }); + + await expect(handle.completion).resolves.toMatchObject({ + result: JSON.stringify({ answer: 'done' }), + }); + expect(promptSpy).toHaveBeenCalledTimes(1); + expect(promptSpy.mock.calls[0]?.[2]).toBe(outputSchema); + }); + + it('skips the short-summary continuation when an output schema is set', async () => { + const parent = testAgent(); + parent.configure(); + parent.newEvents(); + + const outputSchema = { type: 'object', properties: { answer: { type: 'string' } } }; + const child = testAgent({ type: 'sub' }); + for (let i = 0; i < 6; i += 1) { + child.mockNextResponse({ type: 'text', text: 'short' }); + } + const session = fakeSession(parent.agent, child.agent); + const host = new SessionSubagentHost(session, 'main'); + const promptSpy = vi.spyOn(child.agent.turn, 'prompt'); + + const handle = await host.spawn({ + profileName: 'coder', + outputSchema, + parentToolCallId: 'call_agent', + prompt: 'Return structured output', + description: 'Structured task', + runInBackground: false, + signal, + }); + + // The model never calls StructuredOutput, so the turn exhausts its + // completion reminders and fails with structured_output.max_retries. The + // assistant text stays well under SUMMARY_MIN_LENGTH, so a second + // turn.prompt call would mean the continuation ran. + await expect(handle.completion).rejects.toThrow('structured_output.max_retries'); + expect(promptSpy).toHaveBeenCalledTimes(1); + }); + + it('reports structured_output.max_retries as schema_error and other failures as failed', async () => { + const parent = testAgent(); + parent.configure(); + parent.newEvents(); + + const child = testAgent({ type: 'sub' }); + child.configure(); + for (let i = 0; i < 6; i += 1) { + child.mockNextResponse({ type: 'text', text: 'short' }); + } + const session = fakeSession(parent.agent, child.agent); + const host = new SessionSubagentHost(session, 'main'); + + const schemaErrorResults = await host.runQueued([ + { + ...queuedTask(1), + outputSchema: { type: 'object', properties: { answer: { type: 'string' } } }, + signal, + }, + ]); + const schemaErrorResult = schemaErrorResults[0]!; + + expect(schemaErrorResult).toMatchObject({ + status: 'schema_error', + state: 'started', + }); + expect(schemaErrorResult.error).toContain('structured_output.max_retries'); + + child.mockNextProviderResponse({ + parts: [ + { type: 'think', think: 'The child used its output budget before writing a summary.' }, + ], + finishReason: 'truncated', + rawFinishReason: 'length', + }); + const failedResults = await host.runQueued([{ ...queuedTask(2), signal }]); + const failedResult = failedResults[0]!; + + expect(failedResult).toMatchObject({ + status: 'failed', + state: 'started', + }); + expect(failedResult.error).toContain( + 'Subagent turn failed before completing its final summary', + ); + }); + it('prepends git context to the prompt for explore subagents', async () => { vi.mocked(collectGitContext).mockResolvedValueOnce( '\nWorking directory: /repo\nBranch: main\n', @@ -1269,6 +1535,56 @@ describe('SessionSubagentHost', () => { ); }); + it('runQueued carries a workflow run id to the child launch options', async () => { + const parent = testAgent(); + parent.configure(); + parent.newEvents(); + + const child = testAgent({ type: 'sub' }); + child.configure(); + const summary = + 'Completed the queued dynamic workflow item and returned a detailed technical handoff so the parent can map the result back to the original dynamic workflow input. '.repeat( + 2, + ); + child.mockNextResponse({ type: 'text', text: summary }); + + const session = fakeSession(parent.agent, child.agent); + const host = new SessionSubagentHost(session, 'main'); + const spawnSpy = vi.spyOn(host, 'spawn'); + + await expect( + host.runQueued([ + { + ...queuedTask(1), + workflowRunId: 'wfr-test-001', + workflowName: 'Review files', + signal, + }, + ]), + ).resolves.toMatchObject([ + { + agentId: 'agent-0', + status: 'completed', + result: summary.trim(), + }, + ]); + + expect(spawnSpy).toHaveBeenCalledTimes(1); + expect(spawnSpy.mock.calls[0]?.[0]?.workflowRunId).toBe('wfr-test-001'); + expect(spawnSpy.mock.calls[0]?.[0]?.workflowName).toBe('Review files'); + expect(parent.allEvents).toContainEqual( + expect.objectContaining({ + type: '[rpc]', + event: 'subagent.spawned', + args: expect.objectContaining({ + subagentId: 'agent-0', + workflowRunId: 'wfr-test-001', + workflowName: 'Review files', + }), + }), + ); + }); + it('retries a rate-limited child turn without appending the original prompt again', async () => { const parent = testAgent(); parent.configure(); @@ -1330,6 +1646,65 @@ describe('SessionSubagentHost', () => { expect(userTextMessages(histories[1] ?? [])).toEqual(['Implement the retry-safe change']); }); + it('keeps the output schema when a rate-limited subagent is retried', async () => { + const parent = testAgent(); + parent.configure(); + parent.newEvents(); + + const outputSchema = { + type: 'object', + properties: { answer: { type: 'string' } }, + required: ['answer'], + }; + const toolNamesPerCall: string[][] = []; + let generateCalls = 0; + const generate: GenerateFn = async ( + _provider, + _systemPrompt, + tools, + _history, + callbacks, + ) => { + toolNamesPerCall.push(tools.map((tool) => tool.name)); + generateCalls += 1; + if (generateCalls === 1) { + throw new APIStatusError(429, 'Rate limited', 'req-429'); + } + // Answers in prose instead of calling StructuredOutput. + await callbacks?.onMessagePart?.({ type: 'text', text: 'plain prose answer' }); + return textResult('plain prose answer'); + }; + const child = testAgent({ + generate, + initialConfig: { providers: {}, loopControl: { maxRetriesPerStep: 1 } }, + }); + child.configure(); + + const session = fakeSession(parent.agent, child.agent); + const host = new SessionSubagentHost(session, 'main'); + const retrySpy = vi.spyOn(child.agent.turn, 'retry'); + + const options = { + profileName: 'coder', + outputSchema, + parentToolCallId: 'call_agent', + prompt: 'Return structured output', + description: 'Structured task', + runInBackground: false, + signal, + }; + const handle = await host.spawn(options); + await expect(handle.completion).rejects.toThrow('Rate limited'); + + const retryHandle = await host.retry(handle.agentId, options); + + // Dropping the schema here used to let the retried turn answer in prose and + // report completed, silently voiding the structured-output contract. + await expect(retryHandle.completion).rejects.toThrow('structured_output.max_retries'); + expect(retrySpy.mock.calls[0]?.[1]).toBe(outputSchema); + expect(toolNamesPerCall.at(-1)).toContain('StructuredOutput'); + }); + it('realigns a resumed subagent to the parent agent current model', async () => { const parent = testAgent(); parent.configure(); diff --git a/packages/agent-core/test/tools/builtin-current.test.ts b/packages/agent-core/test/tools/builtin-current.test.ts index 71120182..13124c83 100644 --- a/packages/agent-core/test/tools/builtin-current.test.ts +++ b/packages/agent-core/test/tools/builtin-current.test.ts @@ -8,10 +8,16 @@ import { Readable, type Writable } from 'node:stream'; import type { Kaos, KaosProcess } from '@pythoughts/kaos'; +import type { WorkflowWarningEvent } from '@pythoughts/protocol'; import { describe, expect, it, vi } from 'vitest'; import type { Agent } from '../../src/agent'; import type { DynamicWorkflowMode } from '../../src/agent/dynamic-workflow'; +import { + generateWorkflowRunId, + isWorkflowRunId, +} from '../../src/agent/dynamic-workflow/run-id'; +import { resolveWorkflowSizeGuideline } from '../../src/agent/dynamic-workflow/size-guideline'; import { FLAG_DEFINITIONS, FlagResolver } from '../../src/flags'; import type { QueuedSubagentRunResult, @@ -41,6 +47,7 @@ import { createBackgroundManager } from '../agent/background/helpers'; import { DynamicWorkflowTool, DynamicWorkflowToolInputSchema, + isDynamicWorkflowDisabled, } from '../../src/tools/builtin/collaboration/dynamic-workflow'; const signal = new AbortController().signal; @@ -315,6 +322,96 @@ describe('current builtin collaboration tools', () => { expect(result.output).toContain('child result'); }); + it('DynamicWorkflow ignores empty items instead of rejecting the whole call', async () => { + // A model that emits a trailing empty string used to fail argument + // validation, which rejects the entire call before the tool ever runs and + // costs a full re-send of every prompt. + const input = { + description: 'Review files', + prompt_template: 'Review {{item}}', + items: ['src/a.ts', 'src/b.ts', ''], + subagent_type: 'explore', + }; + expect(DynamicWorkflowToolInputSchema.safeParse(input).success).toBe(true); + expect( + DynamicWorkflowToolInputSchema.safeParse({ ...input, items: ['src/a.ts', ' '] }).success, + ).toBe(true); + + const host = mockSubagentHost({ + runQueued: vi.fn().mockResolvedValue([ + { + task: { + kind: 'spawn', + data: { kind: 'spawn', index: 1, item: 'src/a.ts', prompt: 'Review src/a.ts' }, + profileName: 'explore', + parentToolCallId: 'call_dynamic_workflow', + prompt: 'Review src/a.ts', + description: 'Review files #1 (explore)', + runInBackground: false, + }, + agentId: 'agent-explore-1', + status: 'completed', + result: 'explore result a', + }, + { + task: { + kind: 'spawn', + data: { kind: 'spawn', index: 2, item: 'src/b.ts', prompt: 'Review src/b.ts' }, + profileName: 'explore', + parentToolCallId: 'call_dynamic_workflow', + prompt: 'Review src/b.ts', + description: 'Review files #2 (explore)', + runInBackground: false, + }, + agentId: 'agent-explore-2', + status: 'completed', + result: 'explore result b', + }, + ]), + }); + const tool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode()); + + // The panel must advertise the two subagents that will actually launch, + // not the three entries that were sent. + const execution = tool.resolveExecution(input as never); + if (execution.isError === true) throw new Error('expected runnable execution'); + expect(execution.display).toMatchObject({ + agent_name: 'Dynamic Workflow (2 subagents)', + }); + + const result = await executeTool(tool, context(input, 'call_dynamic_workflow')); + + expect(result.isError).not.toBe(true); + const queued = host.runQueued.mock.calls[0]?.[0] as Array<{ prompt: string }>; + expect(queued.map((task) => task.prompt)).toEqual(['Review src/a.ts', 'Review src/b.ts']); + // A quietly shorter workflow must not read as one the model sized right. + expect(result.output).toContain('1 empty item was ignored'); + // The note must not precede the envelope: consumers match the result + // document anchored at the start, so a prefix renders a successful run as + // an unsupported result. + const outputText = typeof result.output === 'string' ? result.output : ''; + expect(outputText.trimStart().startsWith(' { + const host = mockSubagentHost({ runQueued: vi.fn() }); + const tool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode()); + const input = { + description: 'Review files', + items: ['src/a.ts', '', ''], + subagent_type: 'explore', + }; + + const result = await executeTool(tool, context(input, 'call_dynamic_workflow')); + + expect(result.isError).toBe(true); + // Without the second half the caller reads "requires at least 2 items" + // while looking at a list that had three. + expect(result.output).toContain('requires at least 2 items'); + expect(result.output).toContain('2 empty items were ignored'); + expect(host.runQueued).not.toHaveBeenCalled(); + }); + it('DynamicWorkflow applies one subagent_type without automatic timeouts', async () => { const host = mockSubagentHost({ runQueued: vi.fn().mockResolvedValue([ @@ -364,12 +461,15 @@ describe('current builtin collaboration tools', () => { items: Array.from({ length: 128 }, (_, index) => `src/${String(index + 1)}.ts`), }).success, ).toBe(true); + // Over the cap now passes argument validation and fails inside the tool + // with a readable message. Rejecting at the schema would discard the whole + // call -- including the 128 valid prompts -- over one surplus entry. expect( DynamicWorkflowToolInputSchema.safeParse({ ...input, items: Array.from({ length: 129 }, (_, index) => `src/${String(index + 1)}.ts`), }).success, - ).toBe(false); + ).toBe(true); expect(tool.parameters).toMatchObject({ type: 'object', properties: { @@ -384,12 +484,18 @@ describe('current builtin collaboration tools', () => { 'resume_agent_ids', 'model', 'effort', + 'output_schema', ]); const result = await executeTool(tool, context(input, 'call_dynamic_workflow')); expect(dynamicWorkflowMode.enter).toHaveBeenCalledWith('tool'); expect(host.runQueued).toHaveBeenCalledTimes(1); + const queuedTasks = vi.mocked(host.runQueued).mock.calls[0]![0]; + const runId = queuedTasks[0]!.workflowRunId!; + expect(isWorkflowRunId(runId)).toBe(true); + expect(queuedTasks[1]!.workflowRunId).toBe(runId); + expect(queuedTasks.every((task: QueuedSubagentTask) => task.workflowName === 'Review files')).toBe(true); expect(host.runQueued).toHaveBeenCalledWith( [ { @@ -402,6 +508,8 @@ describe('current builtin collaboration tools', () => { dynamicWorkflowIndex: 1, dynamicWorkflowItem: 'src/a.ts', runInBackground: false, + workflowRunId: runId, + workflowName: 'Review files', signal, }, { @@ -414,12 +522,14 @@ describe('current builtin collaboration tools', () => { dynamicWorkflowIndex: 2, dynamicWorkflowItem: 'src/b.ts', runInBackground: false, + workflowRunId: runId, + workflowName: 'Review files', signal, }, ], ); expect(result.output).toBe([ - '', + ``, 'completed: 2', 'explore result a', 'explore result b', @@ -428,6 +538,250 @@ describe('current builtin collaboration tools', () => { expect(result.isError).toBeUndefined(); }); + it('DynamicWorkflow accepts output_schema and passes it to every queued task', async () => { + const outputSchema = { + type: 'object', + properties: { summary: { type: 'string' } }, + required: ['summary'], + }; + const runQueued = vi.fn( + async ( + tasks: readonly QueuedSubagentTask[], + ): Promise>> => { + return tasks.map((task) => ({ + task, + agentId: 'agent-1', + status: 'completed' as const, + result: 'done', + })); + }, + ); + const host = mockSubagentHost({ + runQueued: runQueued as unknown as SessionSubagentHost['runQueued'], + }); + const tool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode()); + const input = { + description: 'Review files', + prompt_template: 'Review {{item}}', + items: ['src/a.ts', 'src/b.ts'], + output_schema: outputSchema, + }; + + expect(DynamicWorkflowToolInputSchema.safeParse(input).success).toBe(true); + expect( + DynamicWorkflowToolInputSchema.safeParse({ ...input, output_schema: { type: 'string' } }) + .success, + ).toBe(true); + + const result = await executeTool(tool, context(input, 'call_dynamic_workflow')); + + expect(result.isError).toBeUndefined(); + expect(runQueued).toHaveBeenCalledTimes(1); + const queuedTasks = vi.mocked(runQueued).mock.calls[0]![0]; + expect(queuedTasks).toHaveLength(2); + expect(queuedTasks.every((task) => task.outputSchema === outputSchema)).toBe(true); + }); + + it('DynamicWorkflow renders a schema_error child alongside the other outcomes', async () => { + const runQueued = vi.fn( + async ( + tasks: readonly QueuedSubagentTask[], + ): Promise>> => { + // One outcome per item, positionally: a schema miss, a clean run, and + // an ordinary failure — so the render covers all three side by side. + const outcomes = [ + { + agentId: 'agent-schema-error', + status: 'schema_error' as const, + error: + '[structured_output.max_retries] Failed to provide valid structured output after the maximum number of retries.', + }, + { + agentId: 'agent-clean', + status: 'completed' as const, + result: 'clean result', + }, + { + agentId: 'agent-timed-out', + status: 'failed' as const, + error: 'Agent timed out after 30s.', + }, + ]; + return tasks.map((task, index) => ({ task, ...outcomes[index]! })); + }, + ); + const host = mockSubagentHost({ + runQueued: runQueued as unknown as SessionSubagentHost['runQueued'], + }); + const tool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode()); + + const result = await executeTool( + tool, + context({ + description: 'Review files', + items: ['src/a.ts', 'src/b.ts', 'src/c.ts'], + }), + ); + + expect(result.output).toContain( + '[structured_output.max_retries] Failed to provide valid structured output after the maximum number of retries.', + ); + expect(result.output).toContain( + 'clean result', + ); + expect(result.output).toContain( + 'Agent timed out after 30s.', + ); + expect(result.output).toContain('completed: 1, failed: 1, schema_error: 1'); + expect(result.isError).toBeUndefined(); + }); + + it('DynamicWorkflow emits exactly one workflow.warning above the size guideline and still runs', async () => { + const runQueued = vi.fn( + async ( + tasks: readonly QueuedSubagentTask[], + ): Promise>> => { + return tasks.map((task) => ({ + task, + agentId: 'agent-1', + status: 'completed' as const, + result: 'done', + })); + }, + ); + const host = mockSubagentHost({ + runQueued: runQueued as unknown as SessionSubagentHost['runQueued'], + }); + const emitEvent = vi.fn<(event: WorkflowWarningEvent) => void>(); + const tool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode(), 'small', emitEvent); + + const result = await executeTool( + tool, + context({ + description: 'Review files', + items: Array.from({ length: 6 }, (_, index) => `src/${String(index + 1)}.ts`), + }), + ); + + expect(emitEvent).toHaveBeenCalledTimes(1); + expect(host.runQueued).toHaveBeenCalledTimes(1); + const warning = emitEvent.mock.calls[0]![0]; + expect(isWorkflowRunId(warning.workflowRunId)).toBe(true); + expect(warning).toMatchObject({ + type: 'workflow.warning', + parentToolCallId: 'call_1', + agentCount: 6, + threshold: 5, + }); + expect(warning.message).toContain('6'); + expect(warning.message).toContain('5'); + expect(result.isError).toBeUndefined(); + }); + + it('DynamicWorkflow emits exactly one workflow.warning above the unrestricted fallback threshold', async () => { + const runQueued = vi.fn( + async ( + tasks: readonly QueuedSubagentTask[], + ): Promise>> => { + return tasks.map((task) => ({ + task, + agentId: 'agent-1', + status: 'completed' as const, + result: 'done', + })); + }, + ); + const host = mockSubagentHost({ + runQueued: runQueued as unknown as SessionSubagentHost['runQueued'], + }); + const emitEvent = vi.fn<(event: WorkflowWarningEvent) => void>(); + const tool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode(), 'unrestricted', emitEvent); + + const result = await executeTool( + tool, + context({ + description: 'Review files', + items: Array.from({ length: 26 }, (_, index) => `src/${String(index + 1)}.ts`), + }), + ); + + expect(emitEvent).toHaveBeenCalledTimes(1); + expect(host.runQueued).toHaveBeenCalledTimes(1); + const warning = emitEvent.mock.calls[0]![0]; + expect(isWorkflowRunId(warning.workflowRunId)).toBe(true); + expect(warning).toMatchObject({ + type: 'workflow.warning', + parentToolCallId: 'call_1', + agentCount: 26, + threshold: 25, + }); + expect(warning.message).toContain('26'); + expect(warning.message).toContain('25'); + expect(result.isError).toBeUndefined(); + }); + + it('DynamicWorkflow emits no warning at or below the size guideline', async () => { + const runQueued = vi.fn( + async ( + tasks: readonly QueuedSubagentTask[], + ): Promise>> => { + return tasks.map((task) => ({ + task, + agentId: 'agent-1', + status: 'completed' as const, + result: 'done', + })); + }, + ); + const host = mockSubagentHost({ + runQueued: runQueued as unknown as SessionSubagentHost['runQueued'], + }); + const emitEvent = vi.fn<(event: WorkflowWarningEvent) => void>(); + const tool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode(), 'small', emitEvent); + + const result = await executeTool( + tool, + context({ + description: 'Review files', + items: Array.from({ length: 5 }, (_, index) => `src/${String(index + 1)}.ts`), + }), + ); + + expect(emitEvent).not.toHaveBeenCalled(); + expect(host.runQueued).toHaveBeenCalledTimes(1); + expect(result.isError).toBeUndefined(); + }); + + it('DynamicWorkflow above the size guideline without an emitter does not throw', async () => { + const runQueued = vi.fn( + async ( + tasks: readonly QueuedSubagentTask[], + ): Promise>> => { + return tasks.map((task) => ({ + task, + agentId: 'agent-1', + status: 'completed' as const, + result: 'done', + })); + }, + ); + const host = mockSubagentHost({ + runQueued: runQueued as unknown as SessionSubagentHost['runQueued'], + }); + const tool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode(), 'small'); + + const result = await executeTool( + tool, + context({ + description: 'Review files', + items: Array.from({ length: 6 }, (_, index) => `src/${String(index + 1)}.ts`), + }), + ); + + expect(host.runQueued).toHaveBeenCalledTimes(1); + expect(result.isError).toBeUndefined(); + }); + it('DynamicWorkflow escapes XML-sensitive agent, item, and result text', async () => { const runQueued = vi.fn( async ( @@ -510,6 +864,24 @@ describe('current builtin collaboration tools', () => { expect(execution.matchesRule).toBeUndefined(); }); + it('DynamicWorkflow accepts a full item list carrying a blank entry', async () => { + // 128 real prompts plus one blank used to trip the schema's raw-length cap, + // which rejected the entire call -- the very hole the blank-item handling + // exists to close, reopened at the boundary. + const items = [...Array.from({ length: 128 }, (_, index) => `src/${String(index + 1)}.ts`), '']; + const input = { description: 'Review files', prompt_template: 'Review {{item}}', items }; + + expect(DynamicWorkflowToolInputSchema.safeParse(input).success).toBe(true); + + const host = mockSubagentHost({ runQueued: vi.fn().mockResolvedValue([]) }); + const tool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode()); + const result = await executeTool(tool, context(input, 'call_dynamic_workflow')); + + expect(result.isError).not.toBe(true); + expect(host.runQueued).toHaveBeenCalledTimes(1); + expect((host.runQueued.mock.calls[0]?.[0] as unknown[]).length).toBe(128); + }); + it('DynamicWorkflow rejects more than 128 subagents at execution time', async () => { const host = mockSubagentHost({ runQueued: vi.fn() }); const dynamicWorkflowMode = mockDynamicWorkflowMode(); @@ -614,6 +986,8 @@ describe('current builtin collaboration tools', () => { ).toBe(true); const result = await executeTool(tool, context(input, 'call_dynamic_workflow')); + const runId = vi.mocked(host.runQueued).mock.calls[0]![0]![0]!.workflowRunId!; + expect(isWorkflowRunId(runId)).toBe(true); expect(host.runQueued).toHaveBeenCalledTimes(1); expect(host.runQueued).toHaveBeenCalledWith( @@ -634,6 +1008,8 @@ describe('current builtin collaboration tools', () => { dynamicWorkflowIndex: 1, dynamicWorkflowItem: 'src/old-a.ts', runInBackground: false, + workflowRunId: runId, + workflowName: 'Finish review', resumeAgentId: 'agent-old-1', signal, }, @@ -653,6 +1029,8 @@ describe('current builtin collaboration tools', () => { dynamicWorkflowIndex: 2, dynamicWorkflowItem: 'src/old-b.ts', runInBackground: false, + workflowRunId: runId, + workflowName: 'Finish review', resumeAgentId: 'agent-old-2', signal, }, @@ -671,12 +1049,14 @@ describe('current builtin collaboration tools', () => { dynamicWorkflowIndex: 3, dynamicWorkflowItem: 'src/new.ts', runInBackground: false, + workflowRunId: runId, + workflowName: 'Finish review', signal, }, ], ); expect(result.output).toBe([ - '', + ``, 'completed: 3', 'result 1', 'result 2', @@ -717,6 +1097,8 @@ describe('current builtin collaboration tools', () => { expect(DynamicWorkflowToolInputSchema.safeParse(input).success).toBe(true); const result = await executeTool(tool, context(input, 'call_dynamic_workflow')); + const runId = vi.mocked(host.runQueued).mock.calls[0]![0]![0]!.workflowRunId!; + expect(isWorkflowRunId(runId)).toBe(true); expect(host.runQueued).toHaveBeenCalledTimes(1); expect(host.runQueued).toHaveBeenCalledWith([ @@ -736,12 +1118,14 @@ describe('current builtin collaboration tools', () => { dynamicWorkflowIndex: 1, dynamicWorkflowItem: 'src/old-a.ts', runInBackground: false, + workflowRunId: runId, + workflowName: 'Resume review', resumeAgentId: 'agent-old-1', signal, }, ]); expect(result.output).toBe([ - '', + ``, 'completed: 1', 'resumed result', '', @@ -797,8 +1181,10 @@ describe('current builtin collaboration tools', () => { ), ); + const runId = vi.mocked(host.runQueued).mock.calls[0]![0]![0]!.workflowRunId!; + expect(isWorkflowRunId(runId)).toBe(true); expect(result.output).toBe([ - '', + ``, 'completed: 1, failed: 1', 'Call DynamicWorkflow with resume_agent_ids using the agent_id values in this result to continue unfinished work.', 'imports are stable', @@ -855,8 +1241,10 @@ describe('current builtin collaboration tools', () => { ), ); + const runId = vi.mocked(host.runQueued).mock.calls[0]![0]![0]!.workflowRunId!; + expect(isWorkflowRunId(runId)).toBe(true); expect(result.output).toBe([ - '', + ``, 'failed: 2', 'Agent did not start.', 'Agent also did not start.', @@ -908,8 +1296,10 @@ describe('current builtin collaboration tools', () => { }), ); + const runId = vi.mocked(host.runQueued).mock.calls[0]![0]![0]!.workflowRunId!; + expect(isWorkflowRunId(runId)).toBe(true); expect(result.output).toBe([ - '', + ``, 'completed: 1, failed: 1', 'imports are stable', 'Agent did not start.', @@ -980,8 +1370,10 @@ describe('current builtin collaboration tools', () => { ), ); + const runId = vi.mocked(host.runQueued).mock.calls[0]![0]![0]!.workflowRunId!; + expect(isWorkflowRunId(runId)).toBe(true); expect(result.output).toBe([ - '', + ``, 'completed: 1, aborted: 2', 'Call DynamicWorkflow with resume_agent_ids using the agent_id values in this result to continue unfinished work.', 'imports are stable', @@ -1015,6 +1407,84 @@ describe('current builtin collaboration tools', () => { }); }); +describe('isDynamicWorkflowDisabled', () => { + it('resolves the switch from config and env with env winning', () => { + expect(isDynamicWorkflowDisabled(undefined, {})).toBe(false); + expect(isDynamicWorkflowDisabled({ disableWorkflows: true }, {})).toBe(true); + expect(isDynamicWorkflowDisabled(undefined, { PYTHINKER_CODE_DISABLE_WORKFLOWS: '1' })).toBe(true); + expect( + isDynamicWorkflowDisabled({ disableWorkflows: true }, { PYTHINKER_CODE_DISABLE_WORKFLOWS: 'false' }), + ).toBe(false); + expect( + isDynamicWorkflowDisabled({ disableWorkflows: true }, { PYTHINKER_CODE_DISABLE_WORKFLOWS: 'maybe' }), + ).toBe(true); + }); +}); + +describe('workflowSizeGuideline', () => { + it('resolves the guideline from config and env with env winning', () => { + expect(resolveWorkflowSizeGuideline(undefined, {})).toBe('medium'); + expect(resolveWorkflowSizeGuideline({ workflowSizeGuideline: 'small' }, {})).toBe('small'); + expect( + resolveWorkflowSizeGuideline(undefined, { PYTHINKER_CODE_WORKFLOW_SIZE_GUIDELINE: 'large' }), + ).toBe('large'); + expect( + resolveWorkflowSizeGuideline( + { workflowSizeGuideline: 'small' }, + { PYTHINKER_CODE_WORKFLOW_SIZE_GUIDELINE: 'LARGE' }, + ), + ).toBe('large'); + expect( + resolveWorkflowSizeGuideline( + { workflowSizeGuideline: 'small' }, + { PYTHINKER_CODE_WORKFLOW_SIZE_GUIDELINE: 'huge' }, + ), + ).toBe('small'); + }); + + it('appends the advisory note to DynamicWorkflowTool descriptions unless unrestricted', () => { + const host = mockSubagentHost({}); + + const smallTool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode(), 'small'); + expect(smallTool.description).toContain('about 5 subagents'); + + const unrestrictedTool = new DynamicWorkflowTool(host, mockDynamicWorkflowMode(), 'unrestricted'); + expect(unrestrictedTool.description).toContain('DynamicWorkflow supports up to 128 subagents'); + expect(unrestrictedTool.description).not.toContain('Workflow size guideline:'); + }); +}); + +describe('workflow run ids', () => { + it('generates ids that satisfy the validator', () => { + expect(isWorkflowRunId(generateWorkflowRunId())).toBe(true); + }); + + it('generates distinct ids on consecutive calls', () => { + expect(generateWorkflowRunId()).not.toBe(generateWorkflowRunId()); + }); + + it.each([ + { name: 'an empty string', value: '' }, + { name: 'a path traversal', value: 'wfr-../etc/passwd' }, + { name: 'a value containing a slash', value: 'wfr-a/b-c' }, + { name: 'uppercase letters', value: 'WFR-ABC-DEF' }, + { name: 'a trailing space', value: 'wfr-abc-def ' }, + { name: 'a plain string without the wfr- prefix', value: 'plain-string' }, + { name: 'an overlong string', value: 'x'.repeat(500) }, + // Structurally valid, so this reaches the component-length cap instead of + // failing earlier on a missing prefix. + { name: 'an overlong first component', value: `wfr-${'a'.repeat(33)}-def` }, + { name: 'an overlong second component', value: `wfr-abc-${'a'.repeat(17)}` }, + // A run id becomes a filename segment, so anything that could terminate or + // split a path has to be rejected outright rather than merely unexpected. + { name: 'a trailing newline', value: `wfr-abc-def${String.fromCodePoint(10)}` }, + { name: 'an embedded NUL', value: `wfr-abc-def${String.fromCodePoint(0)}` }, + { name: 'a NUL followed by traversal', value: `wfr-abc-def${String.fromCodePoint(0)}/../x` }, + ])('rejects $name', ({ value }) => { + expect(isWorkflowRunId(value)).toBe(false); + }); +}); + describe('current builtin background tool schemas', () => { it('background task schemas and manager-backed tools are covered', () => { const manager = createBackgroundManager().manager; diff --git a/packages/node-sdk/src/catalog.ts b/packages/node-sdk/src/catalog.ts index b55dc889..417be817 100644 --- a/packages/node-sdk/src/catalog.ts +++ b/packages/node-sdk/src/catalog.ts @@ -38,7 +38,13 @@ export async function fetchCatalog( if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) { throw new Error(`Unexpected catalog response from ${url}.`); } - return payload as Catalog; + // Drop entries that are not objects instead of casting the payload whole. A + // `null` entry reaches `catalogConnectionWire` and throws well past the + // caller's fallback, which turns one malformed record into a dead login. + const entries = Object.entries(payload).filter( + ([, entry]) => typeof entry === 'object' && entry !== null && !Array.isArray(entry), + ); + return Object.fromEntries(entries) as Catalog; } function capabilityToStrings( @@ -82,6 +88,8 @@ export interface ApplyCatalogProviderOptions { readonly models: readonly CatalogModel[]; readonly selectedModelId: string; readonly thinking: boolean; + /** The effort level the user picked; only the on/off bit persists without it. */ + readonly effort?: string; } /** @@ -134,6 +142,13 @@ export function applyCatalogProvider( const defaultModel = `${options.providerId}/${options.selectedModelId}`; config.defaultModel = defaultModel; config.defaultThinking = options.thinking; + // defaultThinking is a boolean, so without this the picked level is lost and + // the session reopens at 'high' regardless of what the user chose. 'off' is + // written too rather than skipped: `setConfig` deep-merges and cannot delete a + // key, so skipping it would leave a previous login's effort on disk. + if (options.effort !== undefined) { + config.thinking = { ...config.thinking, effort: options.effort }; + } return { defaultModel }; } diff --git a/packages/node-sdk/src/error-format.ts b/packages/node-sdk/src/error-format.ts new file mode 100644 index 00000000..60f63b55 --- /dev/null +++ b/packages/node-sdk/src/error-format.ts @@ -0,0 +1,43 @@ +import { + isPythinkerError, + type PythinkerErrorPayload, +} from '@pythoughts/agent-core'; + +export function formatErrorMessage(error: unknown): string { + if (isPythinkerError(error)) { + return formatErrorPayload({ + code: error.code, + message: error.message, + details: error.details, + }); + } + return error instanceof Error ? error.message : String(error); +} + +export function formatErrorPayload( + error: Pick, +): string { + const filteredMessage = formatProviderFilteredMessage(error.details); + if (filteredMessage !== undefined) return `[${error.code}] ${filteredMessage}`; + return `[${error.code}] ${error.message}`; +} + +function formatProviderFilteredMessage( + details: Record | undefined, +): string | undefined { + const finishReason = stringDetail(details, 'finishReason'); + const rawFinishReason = stringDetail(details, 'rawFinishReason'); + if (finishReason !== 'filtered' && rawFinishReason !== 'content_filter') return undefined; + + const normalizedFinishReason = finishReason ?? 'filtered'; + const raw = rawFinishReason === undefined ? '' : `, rawFinishReason=${rawFinishReason}`; + return `Provider filtered the response before visible output (finishReason=${normalizedFinishReason}${raw}).`; +} + +function stringDetail( + details: Record | undefined, + key: string, +): string | undefined { + const value = details?.[key]; + return typeof value === 'string' ? value : undefined; +} diff --git a/packages/node-sdk/src/index.ts b/packages/node-sdk/src/index.ts index cb2e3b62..71a3931e 100644 --- a/packages/node-sdk/src/index.ts +++ b/packages/node-sdk/src/index.ts @@ -48,6 +48,36 @@ export type { export { buildSkillSlashCommands, isUserActivatableSkill } from '#/skill-commands'; export type { SkillSlashCommand, SkillSlashCommands } from '#/skill-commands'; +// Multi-provider login flows behind the LoginUi port, shared by every surface +// (CLI, TUI, VS Code extension). +export { formatErrorMessage, formatErrorPayload } from '#/error-format'; +export { + buildPlatformOptions, + KIMI_CODE_PLATFORM_ID, + resolvePlatformOption, + type PlatformOption, +} from '#/login/platform-options'; +export { + CATALOG_PLATFORM_VALUE_PREFIX, + catalogProviderIdFromPlatformValue, +} from '#/login/platform-values'; +export { connectCatalogProvider, runLogin } from '#/login/flows'; +export { managedModelToAlias } from '#/login/model-alias'; +export { + CANONICAL_EFFORT_ORDER, + coerceEffortForModel, + DEFAULT_SUPPORTED_EFFORTS, + effortLevelsForModel, + thinkingAvailability, + type ThinkingAvailability, +} from '#/thinking-levels'; +export type { + ApiKeyPromptOptions, + LoginProgressSpinnerHandle, + LoginUi, + PlatformSelection, +} from '#/login/types'; + export { ErrorCodes, PythinkerError, diff --git a/packages/node-sdk/src/login/flows.ts b/packages/node-sdk/src/login/flows.ts new file mode 100644 index 00000000..d49db382 --- /dev/null +++ b/packages/node-sdk/src/login/flows.ts @@ -0,0 +1,432 @@ +import { + applyOpenAICodexOAuthConfig, + applyOpenPlatformConfig, + fetchOpenAICodexModels, + fetchOpenPlatformModels, + filterModelsByPrefix, + getOpenPlatformById, + OPENAI_CODEX_OAUTH_PLATFORM_ID, + OPENAI_CODEX_PROVIDER_ID, + OpenAICodexApiError, + OpenPlatformApiError, + runOpenAICodexOAuthFlow, + type ManagedKimiCodeModelInfo, + type ManagedKimiConfigShape, + KIMI_CODE_PROVIDER_NAME as DEFAULT_OAUTH_PROVIDER_NAME, + type OpenPlatformDefinition, +} from '@pythoughts/pythinker-code-oauth'; +import { log } from '@pythoughts/agent-core'; +import { + applyCatalogProvider, + catalogBaseUrl, + catalogConnectionWire, + catalogProviderModels, + DEFAULT_CATALOG_URL, + fetchCatalog, + type CatalogProviderEntry, +} from '#/catalog'; + +import { formatErrorMessage } from '../error-format'; +import { KIMI_CODE_PLATFORM_ID } from './platform-options'; +import { catalogProviderIdFromPlatformValue } from './platform-values'; +import type { LoginProgressSpinnerHandle, LoginUi } from './types'; + +// --------------------------------------------------------------------------- +// Login flows behind the LoginUi port (shared with non-TUI renderers) +// --------------------------------------------------------------------------- + + +/** + * Run the provider picker and the selected provider's login flow. + * + * Resolves `true` only when credentials were written to config. Callers such as + * `pythinker login` use that for their exit code, so it must never be inferred + * from a side effect like telemetry: every early return here is a user + * cancellation or a failure the flow already reported. + */ +export async function runLogin(ui: LoginUi): Promise { + const selection = await ui.promptPlatformSelection(); + if (selection === undefined) return false; + const { platformId, catalog } = selection; + + const catalogProviderId = catalogProviderIdFromPlatformValue(platformId); + if (catalogProviderId !== undefined) { + return connectCatalogProvider(ui, catalogProviderId, catalog[catalogProviderId]); + } + + if (platformId === KIMI_CODE_PLATFORM_ID) { + return handlePythinkerCodeOAuthLogin(ui); + } + + if (platformId === OPENAI_CODEX_OAUTH_PLATFORM_ID) { + return handleOpenAICodexOAuthLogin(ui); + } + + const platform = getOpenPlatformById(platformId); + if (platform === undefined) return false; + + if (platform.catalogProviderId !== undefined) { + return connectCatalogProvider( + ui, + platform.catalogProviderId, + catalog[platform.catalogProviderId], + platform.name, + ); + } + + return handleOpenPlatformLogin(ui, platform); +} + +async function handlePythinkerCodeOAuthLogin(ui: LoginUi): Promise { + const status = await ui.harness.auth.status(DEFAULT_OAUTH_PROVIDER_NAME); + const alreadyLoggedIn = status.providers.some( + (provider) => provider.providerName === DEFAULT_OAUTH_PROVIDER_NAME && provider.hasToken, + ); + + let spinner: LoginProgressSpinnerHandle | undefined; + const controller = new AbortController(); + const cancelLogin = (): void => { + controller.abort(); + }; + ui.cancelInFlight = cancelLogin; + try { + await ui.harness.auth.login(DEFAULT_OAUTH_PROVIDER_NAME, { + signal: controller.signal, + onDeviceCode: (data) => { + spinner = ui.showLoginAuthorizationPrompt(data); + }, + }); + spinner?.stop({ ok: true, label: 'Logged in.' }); + spinner = undefined; + try { + await ui.refreshConfigAfterLogin(); + } catch (refreshError) { + const message = formatErrorMessage(refreshError); + ui.showError(`Authentication successful, but failed to refresh config: ${message}`); + return false; + } + ui.track('login', { + provider: DEFAULT_OAUTH_PROVIDER_NAME, + already_logged_in: alreadyLoggedIn, + }); + if (alreadyLoggedIn) { + ui.showStatus('Already logged in. Model configuration refreshed.'); + } + return true; + } catch (error) { + const cancelled = controller.signal.aborted; + spinner?.stop({ + ok: false, + label: cancelled ? 'Login cancelled.' : 'Login failed.', + }); + spinner = undefined; + if (cancelled) return false; + log.warn('login failed', { + providerName: DEFAULT_OAUTH_PROVIDER_NAME, + alreadyLoggedIn, + sessionId: ui.sessionId, + error, + }); + const message = formatErrorMessage(error); + ui.showError(`Login failed: ${message}`); + return false; + } finally { + if (ui.cancelInFlight === cancelLogin) { + ui.cancelInFlight = undefined; + } + } +} + +async function handleOpenPlatformLogin( + ui: LoginUi, + platform: OpenPlatformDefinition, +): Promise { + const platformName = platform.name; + const subtitleLines = [ + `${'base_url'.padEnd(12)}${platform.baseUrl}`, + `${'saved to'.padEnd(12)}~/.pythinker-code/config.toml`, + ]; + const apiKey = await ui.promptApiKey(platformName, subtitleLines); + if (apiKey === undefined) return false; + + const controller = new AbortController(); + const cancelLogin = (): void => { + controller.abort(); + }; + ui.cancelInFlight = cancelLogin; + + let models: ManagedKimiCodeModelInfo[]; + try { + models = await fetchOpenPlatformModels(platform, apiKey, fetch, controller.signal); + models = filterModelsByPrefix(models, platform); + } catch (error) { + if (controller.signal.aborted) return false; + const msg = formatErrorMessage(error); + ui.showError(`Failed to verify API key: ${msg}`); + if ( + error instanceof OpenPlatformApiError && + error.status === 401 + ) { + ui.showStatus( + 'Hint: If your API key was obtained from Pythinker OAuth, please select "Pythinker (OAuth)" instead.', + ); + } + return false; + } finally { + if (ui.cancelInFlight === cancelLogin) { + ui.cancelInFlight = undefined; + } + } + + if (models.length === 0) { + ui.showError('No models available for this platform.'); + return false; + } + + const selection = await ui.promptModelSelectionForOpenPlatform(models, platform); + if (selection === undefined) return false; + + const existingConfig = await ui.harness.getConfig(); + if (existingConfig.providers[platform.id] !== undefined) { + await ui.harness.removeProvider(platform.id); + } + + const config = await ui.harness.getConfig(); + applyOpenPlatformConfig(config as ManagedKimiConfigShape, { + platform, + models, + selectedModel: selection.model, + thinking: selection.effort !== 'off', + effort: selection.effort, + apiKey, + }); + + await ui.harness.setConfig({ + providers: config.providers, + models: config.models, + defaultModel: config.defaultModel, + defaultThinking: config.defaultThinking, + // `applyOpenPlatformConfig` writes the picked effort here. Leaving it out of + // the patch dropped it, so only the on/off bit ever reached disk. + thinking: config.thinking, + }); + + await ui.refreshConfigAfterLogin(); + ui.track('login', { provider: platform.id, method: 'api_key' }); + ui.showStatus(`Setup complete: ${platform.name} · ${selection.model.id}`); + return true; +} + +export async function connectCatalogProvider( + ui: LoginUi, + providerId: string, + selectedCatalogEntry?: CatalogProviderEntry, + displayName?: string, +): Promise { + let catalogEntry = selectedCatalogEntry; + if (catalogEntry === undefined) { + const controller = new AbortController(); + const cancelLogin = (): void => { + controller.abort(); + }; + ui.cancelInFlight = cancelLogin; + try { + const catalog = await fetchCatalog(DEFAULT_CATALOG_URL, controller.signal); + catalogEntry = catalog[providerId]; + } catch (error) { + if (controller.signal.aborted) return false; + ui.showError(`Failed to load model catalog: ${formatErrorMessage(error)}`); + return false; + } finally { + if (ui.cancelInFlight === cancelLogin) { + ui.cancelInFlight = undefined; + } + } + } + + if (catalogEntry === undefined) { + ui.showError(`Catalog provider "${providerId}" was not found.`); + return false; + } + const wire = catalogConnectionWire(catalogEntry); + if (wire === undefined) { + ui.showError(`Catalog provider "${providerId}" is not supported for login.`); + return false; + } + + const baseUrl = catalogBaseUrl(catalogEntry, wire); + const platformName = displayName ?? catalogEntry.name ?? providerId; + + const apiKeyEnvVar = catalogEntry.env?.[0]?.trim(); + const envVarHasValue = + apiKeyEnvVar !== undefined && + apiKeyEnvVar.length > 0 && + (process.env[apiKeyEnvVar]?.trim().length ?? 0) > 0; + let apiKey: string | undefined; + if (!envVarHasValue) { + const subtitleLines = [ + ...(baseUrl === undefined ? [] : [`${'base_url'.padEnd(12)}${baseUrl}`]), + `${'saved to'.padEnd(12)}~/.pythinker-code/config.toml`, + ]; + apiKey = await ui.promptApiKey(platformName, subtitleLines); + if (apiKey === undefined) return false; + } + + const models = catalogProviderModels(catalogEntry); + if (models.length === 0) { + ui.showError('No models available for this platform.'); + return false; + } + + const selection = await ui.promptModelSelectionForCatalog(providerId, models); + if (selection === undefined) return false; + + const existingConfig = await ui.harness.getConfig(); + if (existingConfig.providers[providerId] !== undefined) { + await ui.harness.removeProvider(providerId); + } + + const config = await ui.harness.getConfig(); + applyCatalogProvider(config, { + providerId, + catalogUrl: DEFAULT_CATALOG_URL, + wire, + baseUrl, + apiKey, + apiKeyEnvVar: envVarHasValue ? apiKeyEnvVar : undefined, + models, + selectedModelId: selection.model.id, + thinking: selection.effort !== 'off', + effort: selection.effort, + }); + + await ui.harness.setConfig({ + providers: config.providers, + models: config.models, + defaultModel: config.defaultModel, + defaultThinking: config.defaultThinking, + // `applyCatalogProvider` writes the picked effort here. Leaving it out of + // the patch dropped it, so only the on/off bit ever reached disk. + thinking: config.thinking, + }); + + await ui.refreshConfigAfterLogin(); + ui.track('login', { provider: providerId, method: envVarHasValue ? 'api_key_env' : 'api_key' }); + ui.showStatus(`Setup complete: ${platformName} · ${selection.model.id}`); + return true; +} + +async function handleOpenAICodexOAuthLogin(ui: LoginUi): Promise { + const controller = new AbortController(); + const cancelLogin = (): void => { + controller.abort(); + }; + ui.cancelInFlight = cancelLogin; + // Stay armed for the whole flow: the model fetch and the model picker + // below are cancellable too, and disarming here left SIGINT with + // nothing to abort. + try { + + ui.showStatus('Opening browser for OpenAI Codex sign-in…'); + + let tokens; + try { + tokens = await runOpenAICodexOAuthFlow({ + signal: controller.signal, + openBrowser: (url) => { + ui.showStatus('Opening browser for OpenAI Codex sign-in…'); + ui.openBrowser(url); + }, + onManualInput: async () => + ui.promptApiKey( + 'OpenAI Codex (OAuth)', + [ + 'Sign in with your ChatGPT account in the browser.', + 'If the browser callback fails, paste the full redirect URL here.', + ], + { + title: 'Paste OpenAI Codex redirect URL', + secret: false, + emptyMessage: 'Redirect URL cannot be empty.', + }, + ), + }); + } catch (error) { + if (controller.signal.aborted) { + ui.showStatus('OpenAI Codex login cancelled.'); + return false; + } + ui.showError(`OpenAI Codex login failed: ${formatErrorMessage(error)}`); + return false; + } + + let models: ManagedKimiCodeModelInfo[]; + try { + models = await fetchOpenAICodexModels({ + accessToken: tokens.accessToken, + accountId: tokens.accountId, + signal: controller.signal, + }); + } catch (error) { + if (controller.signal.aborted) return false; + ui.showError(`Failed to list OpenAI Codex models: ${formatErrorMessage(error)}`); + if (error instanceof OpenAICodexApiError && error.status === 401) { + ui.showStatus('Hint: Sign in again with /login if your OpenAI Codex session expired.'); + } + return false; + } + + if (models.length === 0) { + ui.showError('No models available for OpenAI Codex.'); + return false; + } + + const codexPlatform: OpenPlatformDefinition = { + id: OPENAI_CODEX_PROVIDER_ID, + name: 'OpenAI Codex (OAuth)', + defaultContextLength: 256_000, + }; + const selection = await ui.promptModelSelectionForOpenPlatform(models, codexPlatform); + if (selection === undefined) return false; + // Ctrl-C while the picker was open aborts the flow; the picker may still + // resolve with a value, and an aborted login must not write credentials. + if (controller.signal.aborted) return false; + + // Drop the previous provider only once the replacement is certain. Removing + // it earlier loses the user's working config on any of the early returns + // above — most easily by cancelling the model picker. Matches the ordering + // in handleOpenPlatformLogin and connectCatalogProvider. + const existingConfig = await ui.harness.getConfig(); + if (existingConfig.providers[OPENAI_CODEX_PROVIDER_ID] !== undefined) { + await ui.harness.removeProvider(OPENAI_CODEX_PROVIDER_ID); + } + + const config = await ui.harness.getConfig(); + applyOpenAICodexOAuthConfig(config as ManagedKimiConfigShape, { + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + accountId: tokens.accountId, + models, + selectedModel: selection.model, + thinking: selection.effort !== 'off', + effort: selection.effort, + }); + + await ui.harness.setConfig({ + providers: config.providers, + models: config.models, + defaultModel: config.defaultModel, + defaultThinking: config.defaultThinking, + thinking: config.thinking, + }); + + await ui.refreshConfigAfterLogin(); + ui.track('login', { provider: OPENAI_CODEX_OAUTH_PLATFORM_ID, method: 'oauth' }); + ui.showStatus(`Logged in to OpenAI Codex · ${selection.model.id}`); + return true; + } finally { + if (ui.cancelInFlight === cancelLogin) { + ui.cancelInFlight = undefined; + } + } +} diff --git a/packages/node-sdk/src/login/model-alias.ts b/packages/node-sdk/src/login/model-alias.ts new file mode 100644 index 00000000..c92be162 --- /dev/null +++ b/packages/node-sdk/src/login/model-alias.ts @@ -0,0 +1,32 @@ +import type { ModelAlias } from '@pythoughts/agent-core'; +import { + capabilitiesForModel, + type ManagedKimiCodeModelInfo, +} from '@pythoughts/pythinker-code-oauth'; + +/** + * Builds a pythinker-code model alias from an open-platform model entry, the + * managed-provider counterpart of `catalogModelToAlias`. + * + * Renderers need this to ask `effortLevelsForModel` what a managed model + * supports without depending on the oauth package themselves. + */ +export function managedModelToAlias( + platformId: string, + model: ManagedKimiCodeModelInfo, +): ModelAlias { + return { + provider: platformId, + model: model.id, + maxContextSize: model.contextLength, + capabilities: capabilitiesForModel(model), + // Carry the declared efforts through, so the login picker offers what the + // model actually supports rather than the low/medium/high fallback. The + // config written after the picker records this same list. + supportEfforts: + model.supportedReasoningEfforts === undefined + ? undefined + : [...model.supportedReasoningEfforts], + displayName: model.displayName, + }; +} diff --git a/packages/node-sdk/src/login/platform-options.ts b/packages/node-sdk/src/login/platform-options.ts new file mode 100644 index 00000000..ddba30b5 --- /dev/null +++ b/packages/node-sdk/src/login/platform-options.ts @@ -0,0 +1,118 @@ +/** + * The list of platforms a user can log in to, built once and rendered by every + * surface: the TUI platform selector and the `pythinker login` terminal picker. + * + * Pure data — no renderer imports — so both surfaces offer the same providers in + * the same order. `PlatformOption` is structurally compatible with the TUI's + * `ChoiceOption`, which is why the selector can pass these straight through. + */ + +import { OPENAI_CODEX_OAUTH_LOGIN, OPEN_PLATFORMS } from '@pythoughts/pythinker-code-oauth'; +import { + catalogConnectionWire, + type Catalog, + type CatalogProviderEntry, +} from '#/catalog'; + +import { CATALOG_PLATFORM_VALUE_PREFIX } from './platform-values'; + +export interface PlatformOption { + /** Platform id, or a `catalog:`-prefixed catalog provider id. */ + readonly value: string; + /** Display name shown in the picker. */ + readonly label: string; + /** Secondary line: the auth kind (`OAuth` / `API key`) or the platform base URL. */ + readonly description?: string; +} + +/** Kimi's managed OAuth platform id — one option among many, never a default. */ +export const KIMI_CODE_PLATFORM_ID = 'kimi-code'; + +const FEATURED_CATALOG_PROVIDERS = [ + { id: 'deepseek', label: 'DeepSeek API' }, + { id: 'zai-coding-plan', label: 'GLM Coding Plan' }, + { id: 'minimax-coding-plan', label: 'MiniMax Token Plan' }, + { id: 'kimi-for-coding', label: 'Kimi For Coding' }, +] as const; + +const REPLACED_OPEN_PLATFORM_IDS = new Set(['moonshot-ai', 'minimax-token']); + +function catalogOption( + providerId: string, + entry: CatalogProviderEntry | undefined, + label = entry?.name ?? providerId, +): PlatformOption { + return { + value: `${CATALOG_PLATFORM_VALUE_PREFIX}${providerId}`, + label, + description: + typeof entry?.api === 'string' && entry.api.length > 0 ? entry.api : 'API key', + }; +} + +export function buildPlatformOptions(catalog: Catalog): readonly PlatformOption[] { + const options: PlatformOption[] = [ + { + value: OPENAI_CODEX_OAUTH_LOGIN.id, + label: OPENAI_CODEX_OAUTH_LOGIN.name, + description: 'OAuth', + }, + { value: KIMI_CODE_PLATFORM_ID, label: 'Kimi (OAuth)', description: 'OAuth' }, + ]; + const seen = new Set([KIMI_CODE_PLATFORM_ID, OPENAI_CODEX_OAUTH_LOGIN.id]); + + for (const featured of FEATURED_CATALOG_PROVIDERS) { + const entry = catalog[featured.id]; + if (entry === undefined || catalogConnectionWire(entry) === undefined) continue; + options.push(catalogOption(featured.id, entry, featured.label)); + seen.add(featured.id); + } + + const catalogEntries = Object.entries(catalog) + .filter(([id, entry]) => !seen.has(id) && catalogConnectionWire(entry) !== undefined) + .toSorted(([aId, a], [bId, b]) => (a.name ?? aId).localeCompare(b.name ?? bId)); + for (const [id, entry] of catalogEntries) { + options.push(catalogOption(id, entry)); + seen.add(id); + } + + for (const platform of OPEN_PLATFORMS) { + if ( + platform.catalogProviderId !== undefined || + REPLACED_OPEN_PLATFORM_IDS.has(platform.id) || + seen.has(platform.id) + ) { + continue; + } + options.push({ + value: platform.id, + label: platform.name, + description: platform.baseUrl, + }); + } + return options; +} + +/** + * Resolve a user-supplied `--provider` value to a platform id, matching by id + * first and then by case-insensitive display name — the same rule the reference + * CLI uses. Returns `undefined` when nothing matches, so the caller can fail + * with the list of valid choices rather than silently picking one. + */ +export function resolvePlatformOption( + options: readonly PlatformOption[], + input: string, +): PlatformOption | undefined { + const wanted = input.trim(); + const byId = options.find((option) => option.value === wanted); + if (byId !== undefined) return byId; + const lowered = wanted.toLowerCase(); + const byLabel = options.find((option) => option.label.toLowerCase() === lowered); + if (byLabel !== undefined) return byLabel; + // A catalog provider's value carries the internal `catalog:` prefix and its + // label is a product name ("DeepSeek API"), so the id the user actually knows + // — `deepseek` — matches neither rule above. Ids are unique across the whole + // option list, so accepting the bare form cannot become ambiguous. + const catalogValue = `${CATALOG_PLATFORM_VALUE_PREFIX}${lowered}`; + return options.find((option) => option.value.toLowerCase() === catalogValue); +} diff --git a/packages/node-sdk/src/login/platform-values.ts b/packages/node-sdk/src/login/platform-values.ts new file mode 100644 index 00000000..15fbe19f --- /dev/null +++ b/packages/node-sdk/src/login/platform-values.ts @@ -0,0 +1,19 @@ +/** + * Platform-selection value vocabulary, shared by the login flows and every + * renderer that presents them. + * + * These live in the SDK on purpose: the login flows are renderer + * agnostic, and importing them from the TUI's platform selector would pull the + * whole dialog stack (choice picker, theme, searchable list) into non-TUI + * callers such as the `pythinker login` subcommand. + */ + +/** Marks a platform id as referring to a catalog provider rather than a built-in platform. */ +export const CATALOG_PLATFORM_VALUE_PREFIX = 'catalog:'; + +/** The catalog provider id behind a platform value, or `undefined` for a non-catalog platform. */ +export function catalogProviderIdFromPlatformValue(value: string): string | undefined { + if (!value.startsWith(CATALOG_PLATFORM_VALUE_PREFIX)) return undefined; + const providerId = value.slice(CATALOG_PLATFORM_VALUE_PREFIX.length); + return providerId.length > 0 ? providerId : undefined; +} diff --git a/packages/node-sdk/src/login/types.ts b/packages/node-sdk/src/login/types.ts new file mode 100644 index 00000000..ac5f8edb --- /dev/null +++ b/packages/node-sdk/src/login/types.ts @@ -0,0 +1,54 @@ +import type { + DeviceAuthorization, + ManagedKimiCodeModelInfo, + OpenPlatformDefinition, +} from '@pythoughts/pythinker-code-oauth'; +import type { Catalog, CatalogModel } from '#/catalog'; +import type { PythinkerHarness } from '#/pythinker-harness'; + +export interface LoginProgressSpinnerHandle { + stop(opts: { ok: boolean; label: string }): void; +} + +export interface ApiKeyPromptOptions { + readonly title?: string; + readonly subtitleLines?: readonly string[]; + readonly secret?: boolean; + readonly emptyMessage?: string; +} + +export interface PlatformSelection { + readonly platformId: string; + readonly catalog: Catalog; +} + +export interface LoginUi { + readonly harness: PythinkerHarness; + readonly sessionId?: string; + cancelInFlight: (() => void) | undefined; + /** + * Open a URL in the user's browser. Renderer-owned on purpose: a terminal + * shells out, while an editor extension uses its own host API. + */ + openBrowser(url: string): void; + showStatus(message: string): void; + showError(message: string): void; + showLoginProgressSpinner(label: string): LoginProgressSpinnerHandle; + showLoginAuthorizationPrompt(auth: DeviceAuthorization): LoginProgressSpinnerHandle; + promptPlatformSelection(): Promise; + promptApiKey( + platformName: string, + subtitleLines?: readonly string[], + options?: ApiKeyPromptOptions, + ): Promise; + promptModelSelectionForOpenPlatform( + models: ManagedKimiCodeModelInfo[], + platform: OpenPlatformDefinition, + ): Promise<{ model: ManagedKimiCodeModelInfo; effort: string } | undefined>; + promptModelSelectionForCatalog( + providerId: string, + models: CatalogModel[], + ): Promise<{ model: CatalogModel; effort: string } | undefined>; + refreshConfigAfterLogin(): Promise; + track(event: string, props?: Record): void; +} diff --git a/packages/node-sdk/src/thinking-levels.ts b/packages/node-sdk/src/thinking-levels.ts new file mode 100644 index 00000000..b6f613d6 --- /dev/null +++ b/packages/node-sdk/src/thinking-levels.ts @@ -0,0 +1,81 @@ +/** + * Thinking-effort level rules, shared by every surface. + * + * Models declare their selectable effort levels via `ModelAlias.supportEfforts`; + * when that metadata is missing the fallback set is low / medium / high. + * Clamping semantics mirror the Web UI's `coerceThinkingForModel` + * (apps/pythinker-web/src/lib/modelThinking.ts). + * + * This lives in the SDK rather than in a single app because the login flows + * are rendered by more than one front end: the terminal renderer and the VS + * Code renderer must offer the same levels for the same model. + */ + +import type { ModelAlias } from '@pythoughts/agent-core'; + +export type ThinkingAvailability = 'toggle' | 'always-on' | 'unsupported'; + +export const CANONICAL_EFFORT_ORDER = [ + 'off', + 'minimal', + 'low', + 'medium', + 'high', + 'xhigh', + 'max', +] as const; + +/** Fallback effort set for models without `supportEfforts` metadata. */ +export const DEFAULT_SUPPORTED_EFFORTS: readonly string[] = ['low', 'medium', 'high']; + +export function thinkingAvailability(model: ModelAlias | undefined): ThinkingAvailability { + if (model === undefined) return 'toggle'; + const caps = model.capabilities ?? []; + if (caps.includes('always_thinking')) return 'always-on'; + if (caps.includes('thinking') || model.adaptiveThinking === true) return 'toggle'; + return 'unsupported'; +} + +/** Effort levels the model actually supports ('off' excluded), canonical order. */ +function baseEffortsForModel(model: ModelAlias | undefined): string[] { + const supportEfforts = model?.supportEfforts; + if (supportEfforts?.length === 0) return ['high']; + const declared = new Set( + (supportEfforts ?? []).filter((effort) => + (CANONICAL_EFFORT_ORDER as readonly string[]).includes(effort), + ), + ); + const ordered = CANONICAL_EFFORT_ORDER.filter( + (effort) => effort !== 'off' && declared.has(effort), + ); + return ordered.length > 0 ? [...ordered] : [...DEFAULT_SUPPORTED_EFFORTS]; +} + +/** Selectable levels for a model, in order. `['off']` when thinking is unsupported. */ +export function effortLevelsForModel(model: ModelAlias | undefined): string[] { + const availability = thinkingAvailability(model); + if (availability === 'unsupported') return ['off']; + const base = baseEffortsForModel(model); + return availability === 'always-on' ? base : ['off', ...base]; +} + +/** + * Clamp a requested effort to what the model supports: + * always-on + 'off' → first supported level; unsupported → 'off'; a level the + * model lacks → nearest lower supported level (else the first selectable one). + * The legacy boolean-era value 'on' maps to 'high' before clamping. + */ +export function coerceEffortForModel(model: ModelAlias | undefined, requested: string): string { + const availability = thinkingAvailability(model); + if (availability === 'unsupported') return 'off'; + const levels = effortLevelsForModel(model); + const normalized = requested === 'on' ? 'high' : requested; + if (availability === 'always-on' && normalized === 'off') return levels[0]!; + if (levels.includes(normalized)) return normalized; + const requestedIdx = (CANONICAL_EFFORT_ORDER as readonly string[]).indexOf(normalized); + for (let i = requestedIdx - 1; i >= 0; i--) { + const candidate = CANONICAL_EFFORT_ORDER[i]!; + if (levels.includes(candidate)) return candidate; + } + return levels[0]!; +} diff --git a/packages/node-sdk/test/catalog.test.ts b/packages/node-sdk/test/catalog.test.ts index 411890af..0541102a 100644 --- a/packages/node-sdk/test/catalog.test.ts +++ b/packages/node-sdk/test/catalog.test.ts @@ -11,6 +11,8 @@ import { importCatalogProvider, type CatalogModel, } from '../src/catalog'; +import { managedModelToAlias } from '../src/login/model-alias'; +import { effortLevelsForModel } from '../src/thinking-levels'; function catalogResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { @@ -54,6 +56,18 @@ describe('fetchCatalog', () => { fetchCatalog('https://x', undefined, fetchMock as unknown as typeof fetch), ).rejects.toThrow(/Unexpected catalog response/); }); + + it('drops entries that are not objects instead of failing the whole catalog', async () => { + // A malformed entry reaches `catalogConnectionWire` and throws well past + // the caller's bundled-catalog fallback, so one bad record used to take the + // whole provider picker down with it. + const good = { id: 'anthropic', models: { x: { id: 'x', limit: { context: 1000 } } } }; + const fetchMock = vi.fn(async () => + catalogResponse({ anthropic: good, broken: null, alsoBroken: 'nope', listy: [1] }), + ); + const result = await fetchCatalog('https://x', undefined, fetchMock as unknown as typeof fetch); + expect(result).toEqual({ anthropic: good }); + }); }); describe('catalogModelToAlias', () => { @@ -128,6 +142,43 @@ describe('applyCatalogProvider', () => { expect(config.defaultThinking).toBe(true); }); + it('persists the picked effort level, not just the on/off bit', () => { + const config = { providers: {} } as PythinkerConfig; + applyCatalogProvider(config, { + providerId: 'anthropic', + wire: 'anthropic', + apiKey: 'test-key', + models: [model], + selectedModelId: 'm1', + thinking: true, + effort: 'medium', + }); + + // defaultThinking is a boolean, so without config.thinking.effort the + // session reopens at 'high' no matter which level the user chose. + expect(config.defaultThinking).toBe(true); + expect(config.thinking?.effort).toBe('medium'); + }); + + it('overwrites a previous effort when the user turns thinking off', () => { + const config = { providers: {}, thinking: { effort: 'high' } } as PythinkerConfig; + applyCatalogProvider(config, { + providerId: 'anthropic', + wire: 'anthropic', + apiKey: 'test-key', + models: [model], + selectedModelId: 'm1', + thinking: false, + effort: 'off', + }); + + // 'off' is written rather than deleted: callers persist through `setConfig`, + // a deep merge that cannot remove a key, so leaving it out would keep 'high' + // on disk for the next session. + expect(config.defaultThinking).toBe(false); + expect(config.thinking?.effort).toBe('off'); + }); + it('writes interleaved reasoning key from a catalog-selected model alias', () => { const models = catalogProviderModels({ id: 'deepseek', @@ -313,3 +364,44 @@ describe('importCatalogProvider', () => { ).rejects.toThrow(/is not offered/); }); }); + +describe('managedModelToAlias', () => { + const managed = { + id: 'gpt-5-codex', + contextLength: 256_000, + supportsReasoning: true, + supportedReasoningEfforts: ['minimal', 'low', 'medium', 'high', 'xhigh'], + supportsImageIn: true, + supportsVideoIn: false, + supportsThinkingType: 'both', + displayName: 'GPT-5 Codex', + } as const; + + it('carries the declared reasoning efforts into the alias', () => { + // Dropping them silently offered the low/medium/high fallback at the login + // picker while the config written right after recorded the real list. + expect(managedModelToAlias('openai-codex', managed)).toMatchObject({ + provider: 'openai-codex', + model: 'gpt-5-codex', + maxContextSize: 256_000, + supportEfforts: ['minimal', 'low', 'medium', 'high', 'xhigh'], + }); + expect(effortLevelsForModel(managedModelToAlias('openai-codex', managed))).toEqual([ + 'off', + 'minimal', + 'low', + 'medium', + 'high', + 'xhigh', + ]); + }); + + it('leaves the efforts undefined when the model declares none', () => { + const alias = managedModelToAlias('openai-codex', { + ...managed, + supportedReasoningEfforts: undefined, + }); + expect(alias.supportEfforts).toBeUndefined(); + expect(effortLevelsForModel(alias)).toEqual(['off', 'low', 'medium', 'high']); + }); +}); diff --git a/packages/node-sdk/test/session-event-types.test.ts b/packages/node-sdk/test/session-event-types.test.ts index 032a1471..eaa6be69 100644 --- a/packages/node-sdk/test/session-event-types.test.ts +++ b/packages/node-sdk/test/session-event-types.test.ts @@ -101,6 +101,7 @@ describe('Event public types', () => { case 'subagent.suspended': case 'subagent.completed': case 'subagent.failed': + case 'workflow.warning': case 'compaction.started': case 'compaction.blocked': case 'compaction.cancelled': diff --git a/packages/node-sdk/test/thinking-levels.test.ts b/packages/node-sdk/test/thinking-levels.test.ts new file mode 100644 index 00000000..51642b3e --- /dev/null +++ b/packages/node-sdk/test/thinking-levels.test.ts @@ -0,0 +1,105 @@ +import type { ModelAlias } from '@pythoughts/agent-core'; +import { describe, expect, it } from 'vitest'; + +import { coerceEffortForModel, effortLevelsForModel } from '../src/thinking-levels'; + +function model(overrides: Partial = {}): ModelAlias { + return { + provider: 'managed:kimi-code', + model: 'k2', + maxContextSize: 200_000, + capabilities: ['thinking'], + ...overrides, + } as ModelAlias; +} + +describe('effortLevelsForModel', () => { + it('falls back to low/medium/high without supportEfforts metadata', () => { + expect(effortLevelsForModel(model())).toEqual(['off', 'low', 'medium', 'high']); + expect(effortLevelsForModel(undefined)).toEqual(['off', 'low', 'medium', 'high']); + }); + + it('uses declared supportEfforts in canonical order, filtering unknown values', () => { + expect( + effortLevelsForModel( + model({ supportEfforts: ['max', 'none', 'low', 'bogus', 'minimal', 'high'] }), + ), + ).toEqual(['off', 'minimal', 'low', 'high', 'max']); + }); + + it('uses one fixed level for explicit empty effort metadata', () => { + expect( + effortLevelsForModel( + model({ capabilities: ['always_thinking'], supportEfforts: [] }), + ), + ).toEqual(['high']); + }); + + it('falls back when supportEfforts only contains unknown values', () => { + expect(effortLevelsForModel(model({ supportEfforts: ['bogus'] }))).toEqual([ + 'off', + 'low', + 'medium', + 'high', + ]); + }); + + it('returns only off for unsupported models', () => { + expect(effortLevelsForModel(model({ capabilities: ['tool_use'] }))).toEqual(['off']); + }); + + it('excludes off for always-on models', () => { + expect( + effortLevelsForModel(model({ capabilities: ['always_thinking'], supportEfforts: ['high'] })), + ).toEqual(['high']); + }); + + it('treats adaptiveThinking models as toggleable', () => { + expect(effortLevelsForModel(model({ capabilities: [], adaptiveThinking: true }))).toEqual([ + 'off', + 'low', + 'medium', + 'high', + ]); + }); +}); + +describe('coerceEffortForModel', () => { + it('passes through supported levels', () => { + expect(coerceEffortForModel(model(), 'medium')).toBe('medium'); + expect(coerceEffortForModel(model(), 'off')).toBe('off'); + }); + + it('clamps an unsupported level down to the nearest lower one', () => { + expect(coerceEffortForModel(model({ supportEfforts: ['low', 'high'] }), 'max')).toBe('high'); + expect(coerceEffortForModel(model({ supportEfforts: ['low', 'high'] }), 'medium')).toBe('low'); + }); + + it('falls back to the first level when nothing lower is supported', () => { + expect(coerceEffortForModel(model({ supportEfforts: ['max'] }), 'low')).toBe('off'); + expect( + coerceEffortForModel( + model({ capabilities: ['always_thinking'], supportEfforts: ['max'] }), + 'low', + ), + ).toBe('max'); + }); + + it('maps always-on + off to the first supported level', () => { + expect( + coerceEffortForModel( + model({ capabilities: ['always_thinking'], supportEfforts: ['high', 'max'] }), + 'off', + ), + ).toBe('high'); + }); + + it('maps unsupported models to off', () => { + expect(coerceEffortForModel(model({ capabilities: ['tool_use'] }), 'high')).toBe('off'); + }); + + it('maps the legacy on value to high before clamping', () => { + expect(coerceEffortForModel(model(), 'on')).toBe('high'); + expect(coerceEffortForModel(model({ supportEfforts: ['low'] }), 'on')).toBe('low'); + }); +}); diff --git a/packages/oauth/src/oauth.ts b/packages/oauth/src/oauth.ts index 15f16db0..963534da 100644 --- a/packages/oauth/src/oauth.ts +++ b/packages/oauth/src/oauth.ts @@ -108,6 +108,23 @@ function describeFetchFailure(error: unknown): string { return [...messages].join(': '); } +/** + * Whether a URL is safe to hand to a browser. + * + * The verification URLs come off the wire and every renderer opens them with + * the host's "open this externally" API, so a provider that answered with + * `file:`, `javascript:`, or an installed app's custom scheme would have the + * agent launch it. Checked here, at the boundary the response is parsed, rather + * than in each renderer. + */ +function isHttpsUrl(value: string): boolean { + try { + return new URL(value).protocol === 'https:'; + } catch { + return false; + } +} + // ── requestDeviceAuthorization ──────────────────────────────────────── export async function requestDeviceAuthorization( @@ -140,11 +157,19 @@ export async function requestDeviceAuthorization( if (typeof verificationUriComplete !== 'string' || verificationUriComplete.length === 0) { throw new OAuthError('Device authorization response missing verification_uri_complete'); } + if (!isHttpsUrl(verificationUriComplete)) { + throw new OAuthError('Device authorization response has a non-HTTPS verification_uri_complete'); + } + const verificationUri = data['verification_uri']; + if (typeof verificationUri === 'string' && verificationUri.length > 0 + && !isHttpsUrl(verificationUri)) { + throw new OAuthError('Device authorization response has a non-HTTPS verification_uri'); + } return { userCode, deviceCode, - verificationUri: typeof data['verification_uri'] === 'string' ? data['verification_uri'] : '', + verificationUri: typeof verificationUri === 'string' ? verificationUri : '', verificationUriComplete, expiresIn: data['expires_in'] !== undefined ? Number(data['expires_in']) : null, interval: Number(data['interval'] ?? 5), diff --git a/packages/oauth/src/open-platform.ts b/packages/oauth/src/open-platform.ts index 5ada7d12..3edb08be 100644 --- a/packages/oauth/src/open-platform.ts +++ b/packages/oauth/src/open-platform.ts @@ -244,6 +244,8 @@ export function applyOpenPlatformConfig( readonly models: readonly ManagedKimiCodeModelInfo[]; readonly selectedModel: ManagedKimiCodeModelInfo; readonly thinking: boolean; + /** The effort level the user picked; only the on/off bit persists without it. */ + readonly effort?: string; readonly apiKey: string; }, ): ApplyOpenPlatformResult { @@ -277,6 +279,13 @@ export function applyOpenPlatformConfig( config.models = existingModels; config.defaultModel = modelKey; config.defaultThinking = options.thinking; + // defaultThinking is a boolean, so without this the picked level is lost and + // the session reopens at 'high' regardless of what the user chose. 'off' is + // written too rather than skipped: `setConfig` deep-merges and cannot delete a + // key, so skipping it would leave a previous login's effort on disk. + if (options.effort !== undefined) { + config.thinking = { ...config.thinking, effort: options.effort }; + } return { defaultModel: modelKey, defaultThinking: options.thinking }; } diff --git a/packages/oauth/src/openai-codex-oauth.ts b/packages/oauth/src/openai-codex-oauth.ts index 5067fd74..3c435e0b 100644 --- a/packages/oauth/src/openai-codex-oauth.ts +++ b/packages/oauth/src/openai-codex-oauth.ts @@ -293,6 +293,14 @@ export async function startOpenAICodexCallbackServer( return new Promise<{ code: string } | null>((resolveWait, rejectWait) => { let timer: NodeJS.Timeout | undefined; const onAbort = (): void => { + // Tear down here rather than leaning on the `waitPromise.then` + // below: an already-aborted signal returns before that handler + // is ever registered, which left both the full timeout and the + // listening callback server alive to hold the host's event + // loop. Every other exit from the wait closes the server, so + // this path has to as well. + cleanup(); + close(); settleWait?.(null); settleWait = undefined; rejectWait( @@ -573,6 +581,11 @@ export function applyOpenAICodexOAuthConfig( readonly models: readonly ManagedKimiCodeModelInfo[]; readonly selectedModel: ManagedKimiCodeModelInfo; readonly thinking?: boolean | undefined; + /** + * The effort level the user picked. Omitted, the model's top supported + * effort is used, which is what callers that never asked the user get. + */ + readonly effort?: string; }, ): ApplyOpenAICodexOAuthResult { if (options.models.length === 0) { @@ -630,7 +643,15 @@ export function applyOpenAICodexOAuthConfig( ? 'max' : CODEX_REASONING_EFFORT_ORDER.findLast((effort) => supportedEfforts.includes(effort)) ?? 'max'; - config.thinking = { ...config.thinking, effort: topEffort }; + // A level the user picked wins over the derived top effort, but only when the + // model actually declares it — otherwise we would persist an unusable level. + const picked = + options.effort !== undefined && + options.effort !== 'off' && + (supportedEfforts === undefined || supportedEfforts.includes(options.effort)) + ? options.effort + : undefined; + config.thinking = { ...config.thinking, effort: picked ?? topEffort }; return { defaultModel: modelKey, defaultThinking: config.defaultThinking ?? true }; } diff --git a/packages/oauth/test/oauth.test.ts b/packages/oauth/test/oauth.test.ts index 36a4855c..2626b2e0 100644 --- a/packages/oauth/test/oauth.test.ts +++ b/packages/oauth/test/oauth.test.ts @@ -273,6 +273,44 @@ describe('requestDeviceAuthorization', () => { await expect(requestAuth()).rejects.toBeInstanceOf(OAuthError); }); + // Every renderer hands these URLs straight to the host's "open externally" + // API, so a provider answering with `file:` or an app's custom scheme would + // have the agent launch it. Rejected here, where the response is parsed, + // rather than in each renderer. + it.each([ + ['file:///etc/passwd'], + ['javascript:alert(1)'], + ['vscode://extension/install?id=evil'], + ['not a url'], + ])('rejects a non-HTTPS verification_uri_complete (%s)', async (uri) => { + server.enqueue('/api/oauth/device_authorization', { + status: 200, + body: { + user_code: 'U', + device_code: 'D', + verification_uri_complete: uri, + expires_in: 60, + interval: 5, + }, + }); + await expect(requestAuth()).rejects.toBeInstanceOf(OAuthError); + }); + + it('rejects a non-HTTPS verification_uri even when the complete one is safe', async () => { + server.enqueue('/api/oauth/device_authorization', { + status: 200, + body: { + user_code: 'U', + device_code: 'D', + verification_uri: 'file:///etc/passwd', + verification_uri_complete: 'https://auth.kimi.com/verify?user_code=U', + expires_in: 60, + interval: 5, + }, + }); + await expect(requestAuth()).rejects.toBeInstanceOf(OAuthError); + }); + it('surfaces message fields from failed device authorization responses', async () => { server.enqueue('/api/oauth/device_authorization', { status: 400, diff --git a/packages/oauth/test/open-platform.test.ts b/packages/oauth/test/open-platform.test.ts index 395b97e6..10f93a12 100644 --- a/packages/oauth/test/open-platform.test.ts +++ b/packages/oauth/test/open-platform.test.ts @@ -314,6 +314,41 @@ describe('applyOpenPlatformConfig', () => { expect(config.services).toBeUndefined(); }); + it('persists the picked effort, and overwrites a previous one when thinking is off', () => { + const platform = getOpenPlatformById('moonshot-cn')!; + const models = [ + { id: 'kimi-k2-0712-preview', contextLength: 256000, supportsReasoning: true, supportsImageIn: false, supportsVideoIn: false }, + ]; + + const picked: ManagedKimiConfigShape = { providers: {} }; + applyOpenPlatformConfig(picked, { + platform, + models, + selectedModel: models[0]!, + thinking: true, + effort: 'medium', + apiKey: 'sk-test', + }); + // defaultThinking is a boolean, so without this the level is lost and the + // session reopens at the default no matter what the user chose. + expect(picked.thinking?.effort).toBe('medium'); + + const turnedOff: ManagedKimiConfigShape = { providers: {}, thinking: { effort: 'high' } }; + applyOpenPlatformConfig(turnedOff, { + platform, + models, + selectedModel: models[0]!, + thinking: false, + effort: 'off', + apiKey: 'sk-test', + }); + // 'off' is written rather than skipped: the caller persists through a + // `setConfig` deep merge, which cannot delete a key, so omitting it would + // leave 'high' on disk for the next session. + expect(turnedOff.defaultThinking).toBe(false); + expect(turnedOff.thinking?.effort).toBe('off'); + }); + it('clears stale models for the same provider', () => { const config: ManagedKimiConfigShape = { providers: { diff --git a/packages/oauth/test/openai-codex-oauth.test.ts b/packages/oauth/test/openai-codex-oauth.test.ts index b736ec58..42893f35 100644 --- a/packages/oauth/test/openai-codex-oauth.test.ts +++ b/packages/oauth/test/openai-codex-oauth.test.ts @@ -1,4 +1,5 @@ import { createHash } from 'node:crypto'; +import { createServer as createNetServer } from 'node:net'; import { describe, expect, it, vi } from 'vitest'; import type { ManagedKimiConfigShape } from '../src/managed-kimi-code'; @@ -9,6 +10,7 @@ import { extractOpenAICodexAccountId, fetchOpenAICodexModels, parseOpenAICodexAuthorizationInput, + startOpenAICodexCallbackServer, } from '../src/openai-codex-oauth'; import { renderOpenAICodexOAuthSuccessPage } from '../src/oauth-pages'; @@ -273,3 +275,55 @@ describe('openai-codex-oauth', () => { expect(config.thinking).toEqual({ mode: 'auto', effort: 'xhigh' }); }); }); + +describe('startOpenAICodexCallbackServer', () => { + /** Resolves true once nothing is listening on the callback port. */ + async function portFreedWithin(port: number, deadlineMs: number): Promise { + const started = Date.now(); + for (;;) { + const free = await new Promise((resolve) => { + const probe = createNetServer(); + probe.once('error', () => { + resolve(false); + }); + probe.listen(port, '127.0.0.1', () => { + probe.close(() => { + resolve(true); + }); + }); + }); + if (free) return true; + if (Date.now() - started > deadlineMs) return false; + await new Promise((resolve) => { + setTimeout(resolve, 20); + }); + } + } + + it('tears the wait down when it is aborted before it starts', async () => { + const server = await startOpenAICodexCallbackServer('state-abc'); + // Fake timers only count what is scheduled while they are installed, so + // install them after the (real, I/O-bound) bind and before the wait. + vi.useFakeTimers(); + try { + // An already-aborted signal takes the early-return branch, which never + // registers the completion handler every other exit relies on. Left to + // it, both the two-minute timeout and the listening server outlive the + // cancelled login and pin the host event loop. + await expect( + server.waitForCode({ + signal: AbortSignal.abort(new Error('login cancelled')), + timeoutMs: 120_000, + }), + ).rejects.toThrow('login cancelled'); + expect(vi.getTimerCount()).toBe(0); + } finally { + vi.useRealTimers(); + } + // Probed before this test closes anything: the abort path has to release + // the port on its own, the way every other exit from the wait does. + const freed = await portFreedWithin(1455, 1_000); + server.close(); + expect(freed).toBe(true); + }); +}); diff --git a/packages/protocol/src/__tests__/events.test.ts b/packages/protocol/src/__tests__/events.test.ts index e812b89c..d8c086fc 100644 --- a/packages/protocol/src/__tests__/events.test.ts +++ b/packages/protocol/src/__tests__/events.test.ts @@ -16,6 +16,7 @@ import { subagentStartedEventSchema, subagentSuspendedEventSchema, toolCallStartedEventSchema, + workflowWarningEventSchema, } from '../events'; import type { Event } from '../events'; import type { ToolInputDisplay } from '../display'; @@ -158,6 +159,39 @@ describe('events / display re-exports', () => { } }); + it('validates workflow.warning events and requires a run id', () => { + const warning = { + type: 'workflow.warning' as const, + workflowRunId: 'wfr-test-001', + parentToolCallId: 'call_1', + agentCount: 26, + threshold: 25, + message: 'This Dynamic Workflow will launch 26 subagents, above the advisory ceiling of 25; the run is proceeding anyway.', + }; + + expect(workflowWarningEventSchema.parse(warning)).toEqual(warning); + // Also through the union: registering the schema but forgetting the + // discriminatedUnion member would leave the event unserializable. + expect(agentEventSchema.parse(warning)).toEqual(warning); + expect( + workflowWarningEventSchema.safeParse({ + type: 'workflow.warning', + agentCount: 26, + threshold: 25, + message: 'missing run id', + }).success, + ).toBe(false); + expect( + workflowWarningEventSchema.safeParse({ + type: 'workflow.warning', + workflowRunId: 'wfr-test-001', + agentCount: 26, + threshold: 25, + message: 'missing parent tool call id', + }).success, + ).toBe(false); + }); + it('validates model rates and accumulated spend while rejecting unknown status keys', () => { const status = { type: 'agent.status.updated' as const, diff --git a/packages/protocol/src/events.ts b/packages/protocol/src/events.ts index 812013fd..4b88fc5d 100644 --- a/packages/protocol/src/events.ts +++ b/packages/protocol/src/events.ts @@ -517,6 +517,10 @@ export interface SubagentSpawnedEvent { readonly parentAgentId?: string; readonly description?: string; readonly dynamicWorkflowIndex?: number; + /** Identifies the Dynamic Workflow run this subagent belongs to; absent outside a workflow. */ + readonly workflowRunId?: string; + /** The workflow's user-facing description, repeated on each subagent for correlation. */ + readonly workflowName?: string; readonly runInBackground: boolean; } @@ -525,6 +529,8 @@ export interface SubagentStartedEvent { readonly subagentId: string; /** Tool call in the parent agent that spawned the subagent; absent when spawned outside a tool call. */ readonly parentToolCallId?: string; + /** Identifies the Dynamic Workflow run this subagent belongs to; absent outside a workflow. */ + readonly workflowRunId?: string; } export interface SubagentSuspendedEvent { @@ -540,6 +546,8 @@ export interface SubagentCompletedEvent { readonly subagentId: string; /** Tool call in the parent agent that spawned the subagent; absent when spawned outside a tool call. */ readonly parentToolCallId?: string; + /** Identifies the Dynamic Workflow run this subagent belongs to; absent outside a workflow. */ + readonly workflowRunId?: string; readonly resultSummary: string; readonly usage?: TokenUsage; readonly contextTokens?: number; @@ -550,9 +558,23 @@ export interface SubagentFailedEvent { readonly subagentId: string; /** Tool call in the parent agent that spawned the subagent; absent when spawned outside a tool call. */ readonly parentToolCallId?: string; + /** Identifies the Dynamic Workflow run this subagent belongs to; absent outside a workflow. */ + readonly workflowRunId?: string; readonly error: string; } +export interface WorkflowWarningEvent { + readonly type: 'workflow.warning'; + readonly workflowRunId: string; + /** Tool call in the parent agent that launched this workflow. */ + readonly parentToolCallId: string; + /** Number of subagents this run will launch. */ + readonly agentCount: number; + /** The advisory ceiling that was exceeded. */ + readonly threshold: number; + readonly message: string; +} + export interface CompactionStartedEvent { readonly type: 'compaction.started'; readonly trigger: 'manual' | 'auto'; @@ -653,6 +675,7 @@ export type AgentEvent = | SubagentSuspendedEvent | SubagentCompletedEvent | SubagentFailedEvent + | WorkflowWarningEvent | CompactionStartedEvent | CompactionBlockedEvent | CompactionCancelledEvent @@ -1175,6 +1198,8 @@ export const subagentSpawnedEventSchema = z.object({ parentAgentId: z.string().optional(), description: z.string().optional(), dynamicWorkflowIndex: z.number().optional(), + workflowRunId: z.string().optional(), + workflowName: z.string().optional(), runInBackground: z.boolean(), }).strict() satisfies z.ZodType; @@ -1182,6 +1207,7 @@ export const subagentStartedEventSchema = z.object({ type: z.literal('subagent.started'), subagentId: z.string(), parentToolCallId: z.string().optional(), + workflowRunId: z.string().optional(), }) satisfies z.ZodType; export const subagentSuspendedEventSchema = z.object({ @@ -1195,6 +1221,7 @@ export const subagentCompletedEventSchema = z.object({ type: z.literal('subagent.completed'), subagentId: z.string(), parentToolCallId: z.string().optional(), + workflowRunId: z.string().optional(), resultSummary: z.string(), usage: tokenUsageSchema.optional(), contextTokens: z.number().optional(), @@ -1204,9 +1231,19 @@ export const subagentFailedEventSchema = z.object({ type: z.literal('subagent.failed'), subagentId: z.string(), parentToolCallId: z.string().optional(), + workflowRunId: z.string().optional(), error: z.string(), }) satisfies z.ZodType; +export const workflowWarningEventSchema = z.object({ + type: z.literal('workflow.warning'), + workflowRunId: z.string(), + parentToolCallId: z.string(), + agentCount: z.number(), + threshold: z.number(), + message: z.string(), +}) satisfies z.ZodType; + export const compactionStartedEventSchema = z.object({ type: z.literal('compaction.started'), trigger: z.enum(['manual', 'auto']), @@ -1310,6 +1347,7 @@ export const agentEventSchema = z.discriminatedUnion('type', [ subagentSuspendedEventSchema, subagentCompletedEventSchema, subagentFailedEventSchema, + workflowWarningEventSchema, compactionStartedEventSchema, compactionBlockedEventSchema, compactionCancelledEventSchema, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f9460ee..a5cfeb68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: apps/pythinker-code: dependencies: + '@clack/prompts': + specifier: 1.7.0 + version: 1.7.0 '@opentui/core': specifier: 0.4.3 version: 0.4.3(typescript@6.0.3)(web-tree-sitter@0.25.10) @@ -480,7 +483,7 @@ importers: version: 3.5.2(vite@6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.5)(yaml@2.9.0)) vitest: specifier: 4.1.4 - version: 4.1.4(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(vite@6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.5)(yaml@2.9.0)) + version: 4.1.4(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9(vitest@4.1.9))(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(vite@6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.5)(yaml@2.9.0)) docs: dependencies: @@ -1262,6 +1265,14 @@ packages: '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + '@clack/core@1.4.3': + resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.7.0': + resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} + engines: {node: '>= 20.12.0'} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -10381,6 +10392,18 @@ snapshots: '@chevrotain/types@11.1.2': {} + '@clack/core@1.4.3': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.7.0': + dependencies: + '@clack/core': 1.4.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + '@colors/colors@1.5.0': optional: true @@ -19073,7 +19096,7 @@ snapshots: - typescript - universal-cookie - vitest@4.1.4(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(vite@6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.5)(yaml@2.9.0)): + vitest@4.1.4(@types/node@22.20.1)(@vitest/coverage-v8@4.1.9(vitest@4.1.9))(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(vite@6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.5)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 '@vitest/mocker': 4.1.4(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(vite@6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.5)(yaml@2.9.0)) diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 00000000..466b8e76 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "skills": { + "stop-slop": { + "source": "hardikpandya/stop-slop", + "sourceType": "github", + "skillPath": "SKILL.md", + "computedHash": "617bf97d33162a6bd0da4f40a97aac589de58e5a6c9cf011ed6108992210e7cb" + } + } +}