The Shape-Shifting Session OpenAI has introduced Dynamic Profiles, a new method for managing multiple models, personas, and contexts within a single session, designed to help developers handle long-running conversations and task-specific model selection without context window overflow. The feature includes DynamicInstructions for reusable, composable instruction bundles, and is accompanied by an open-source package for experimental agentic patterns. There’s a moment every developer building with language models eventually hits. You start with one model, one set of instructions, one clean conversation. Then your app grows. Suddenly you need different behavior for different parts of your experience. A creative brainstorming mode here, a precise planning mode there, a lightweight on-device check somewhere else. And the code that was supposed to be simple turns into a tangle of if-statements, duplicated prompts, and context windows that quietly overflow when you’re not looking. This is exactly the mess that Dynamic Profiles are built to untangle. At their core, Dynamic Profiles are a new way to manage multiple models, multiple personas, and multiple contexts inside a single session, without losing your mind or your context window in the process. They solve two problems that show up constantly once you start building anything beyond a toy chatbot: keeping your context under control, and drawing clear boundaries around which model handles which job. Let’s dig into what that actually looks like. Long running sessions have a nasty habit of growing. Every message, every tool call, every response adds weight to the transcript. Left unchecked, that transcript eventually bumps against the model’s context window, and things start breaking in subtle ways. At the same time, not every task deserves the same model. Some jobs need deep reasoning and broad knowledge. Others just need a fast, cheap, on-device response. Mixing these decisions into one undifferentiated pile of code is how projects turn into spaghetti. Dynamic Profiles exist to give you a structured way to handle both problems at once. They let you trim and summarize transcripts to stay inside context limits, and they let you deliberately choose which model handles which part of the job based on capability and cost. The underlying primitives are intentionally flexible, built so you can construct today’s patterns and whatever new abstractions show up next month, because this space is moving that fast. Alongside this, there’s a new open source package worth knowing about: a set of framework utilities built on top of Dynamic Profiles, designed to house the more experimental and emerging patterns for building agentic experiences. It gets updated independently of OS releases, so it evolves quickly, right alongside the ideas it’s built to support. Picture an app that helps people build craft projects, origami and crochet specifically. A user uploads a photo for inspiration, gets a shortlist of project ideas, gives feedback, picks one, and then works through a tutorial while uploading progress photos for advice along the way. Every stage of that journey shares some context with the others, but each one also has its own priorities. Brainstorming benefits from creativity. Planning benefits from depth. Reviewing benefits from speed. Trying to force all three through the same model configuration would be a compromise nobody asked for. This is where a Profile comes in. A Profile is a declared configuration state, essentially an agent, made up of instructions, tools, and modifiers that control things like which model is used, the temperature, and the sampling mode. You declare a Profile for each mode your app needs, and DynamicProfile handles switching between them inside your session. So for the craft app, you’d start with a brainstorming Profile: instructions describing its goal, plus a tool for generating titles. If the user happens to be working on an origami project specifically, you can layer in extra instructions and tools that only apply in that context. That’s where DynamicInstructions comes in. It lets you group related tools and instructions into a single reusable component. An origami-specific bundle of knowledge and tools can be defined once and reused anywhere it’s relevant. These bundles are also composable, meaning nesting one inside another simply concatenates their instructions and tools together. It’s a clean way to avoid rewriting the same domain knowledge over and over. Once your instructions are organized this way, your brainstorming Profile declaration becomes remarkably tidy. Since brainstorming needs both broad craft knowledge and creative flexibility, it makes sense to route it to a more powerful server-side model, and to crank the temperature up toward the creative end, allowing for looser, more inventive responses. The next phase, planning, needs something different. This is where the tutorial itself gets mapped out, so it benefits from a model with deeper reasoning capability. There’s a setting for exactly this, controlling how much the model is allowed to think through a problem before responding. For something as involved as generating a full tutorial, you’d want that reasoning depth set high. Finally there’s reviewing, where the user is mid-project and just needs quick, practical advice on their technique. No need for a heavyweight server model here. This is the perfect place for something fast and local, running right on the device. Put these three Profiles together and you’ve got a fully declared agent with three distinct personas, each tuned for its job. Wiring it into your session just means using a new initializer designed to work with DynamicProfile. And here’s the elegant part: the body of a Profile is re-evaluated every time the model is prompted. As your app moves between modes, the “personality” of the session shifts right along with it. Think of it as swapping hats rather than managing three separate systems. Switching models mid-session sounds great until you remember that different models have different context limits. The craft app bounces between a large server-side model and a small on-device one, and what fits comfortably in one won’t necessarily fit in the other. This is where transcript management earns its keep. The transcript is essentially the session’s running record of everything that’s happened: every message, every tool call, every response. And there are a few good reasons to actively manage it beyond just fitting inside a context window. Trimming irrelevant entries sharpens the model’s focus. Redacting sensitive details becomes necessary when handing a conversation off to a less private model. DynamicInstructions can update the instructions portion of that transcript, but for everything else there’s a tool called history, essentially a window into the rest of the transcript. One of the simplest ways to lighten the load is dropping old tool calls that no longer matter. A historyTransform can be attached to a Profile to reshape the history right before the model is prompted. Applied to the reviewing Profile in the craft example, this keeps things comfortably within the on-device model’s smaller context window. Here’s the reassuring part: these transforms don’t permanently alter anything. They’re local adjustments applied just before a prompt goes out, not a rewrite of the session’s actual history. So nothing is lost if that context turns out to matter again later. Because a historyTransform can get complicated fast, custom modifiers offer a way to hide that complexity. You define a type that conforms to the modifier protocol, apply your transform inside it, and then expose it as a reusable extension. From then on, any Profile that needs to trim its context can just plug in the modifier instead of rewriting the logic from scratch. The framework utilities package already ships with a handful of useful modifiers worth exploring for exactly this reason. Sometimes trimming isn’t enough. Sometimes you need to actually summarize what’s already happened in order to reclaim space, and you want that to happen consistently, say, right after every model response. This is where lifecycle modifiers come in. They give you a hook into specific moments in a session’s lifecycle, letting you run your own code exactly when you need to. That’s useful for updating things outside the session too, like reflecting progress somewhere in your UI, but it’s just as useful for internal bookkeeping, like switching modes or adjusting the transcript itself. Using the onResponse modifier, you can mutate the history right at the boundary of each response. This naturally pairs with another concept: session properties. These let you define state that’s accessible from any tool or Profile in your session. History itself is actually a built-in session property, capturing the transcript and offering an alternative way to update it. Just keep in mind it’s a lossy operation, and any changes ripple across every Profile in the session. If you need something more surgical and reversible, historyTransform is still the better tool for the job. Beyond the built-in properties, you can define your own. A summary property, for instance, declared as an optional string, can be written to whenever onResponse fires, capturing a running summary of the conversation. Every Profile can then read that summary and fold it into its own instructions, preserving context even after the original entries have been dropped. Any Profile can write to a shared property too, and the change is instantly visible everywhere else in the session. The pattern in short: use lifecycle modifiers to run code at key moments, use the history property when you need broad changes across every Profile, and use custom session properties whenever you need shared state that outlives any single interaction. Once you’ve got multiple Profiles working together, the next question is how they hand work off to each other. Two patterns come up again and again, and it helps to give them names: baton-pass and phone-a-friend. One is a collaboration. The other is a consultation. In the baton-pass pattern, you have two or more Profiles, usually backed by different models, along with a variable that tracks which one is currently active. Each Profile is given a tool that lets it change that variable. If the app is currently brainstorming and the user suddenly asks how to fold a crane, the brainstorming Profile can call its tool to hand things off to the tutorial Profile. That tool call is the signal of a successful handoff, and from there the tutorial Profile takes over and delivers the final answer. The defining traits of baton-pass are that the full transcript stays visible to both Profiles throughout, and that whichever Profile receives the baton is the one that carries it across the finish line. Phone-a-friend works differently. It also relies on tool calling, but instead of flipping a shared variable, the tool spawns an entirely separate, short-lived session. Imagine the model is asked for a fun project idea for kids and realizes it needs a good title. Rather than switching personas itself, it can call a phone-a-friend tool that spins up a fresh session, prompts a title-focused Profile inside it, and returns that response as tool output. Once the answer comes back, the child session simply disappears, and the original Profile is the one that delivers the final response to the user. The key difference from baton-pass is isolation. Each Profile’s transcript in phone-a-friend stays completely separate, and the parent Profile never actually hands off responsibility, it just consults and continues. These two patterns cover a lot of ground, but they’re not the only options out there. The framework utilities package also includes a Skills type, built around the increasingly popular idea of procedural context loading, worth exploring once you’re comfortable with the basics. Beyond deciding which Profile handles what, there’s also a more granular question: should the model be allowed to call tools at all in a given moment? Tool calling mode answers exactly that, with three settings: allowed, disallowed, and required. Allowed is the default and the most familiar behavior. The model decides for itself whether a tool call makes sense or a direct response will do. This covers the vast majority of real-world cases, where you genuinely don’t know in advance whether a tool will be needed. Disallowed does what it sounds like, blocking tool calls entirely. This is handy when a user has navigated into a part of your app where none of the session’s tools are relevant anyway. Required is the most aggressive setting: the model can only respond by calling a tool. This fits agentic systems where every action, without exception, is represented as a tool call. If you’re working with Profiles, tool calling mode is set through a modifier. Outside of Profiles, it’s set directly through generation options when calling respond. There’s an important catch with required mode, though. Setting it effectively puts the model into a loop, and it becomes your responsibility to define an exit condition. One clean approach is tying the tool calling mode to a variable, requiring calls only until a specific condition, like a particular tool being invoked, is met. A second, more forceful approach is giving the model a dedicated final-answer tool that throws an error when called. Throwing an error immediately breaks the loop and hands control back to you. By default, throwing an error from a tool, or cancelling a response outright, rolls the session’s transcript back to its previous state. But sometimes you want the opposite: the ability to cancel partway through and pick things back up later without losing that progress. For that, there’s a transcript error handling policy, set through a modifier when using Profiles or directly on the session otherwise, with two options: revert the transcript, or preserve it. Choosing to preserve the transcript puts the responsibility on you to leave it in a sensible state if you plan to keep using the session. To support that, the transcript property on a session is now mutable, though only while the session isn’t actively responding. Trying to mutate it mid-response is treated as a programmer error, so timing matters here. All this flexibility around rewriting history comes with real tradeoffs, and it’s worth being honest about them rather than treating transcript mutation as a free lunch. The first tradeoff is performance, specifically around key-value caches, which are a core optimization inside large language models. Generally speaking, simply appending to a transcript preserves that cache and keeps time-to-first-token low. But the moment you start removing entries, changing which tools are attached, or altering instructions, you risk invalidating that cache, which can noticeably increase latency. This wasn’t really a conversation worth having before, because session APIs were intentionally designed to be append-only, which kept caching behavior predictable by default. That constraint has now been lifted, which means the responsibility for understanding caching behavior shifts to you. Different models cache differently, and the only reliable way to know what’s happening is to measure it directly, ideally using the profiling tools built for exactly this kind of investigation. The second tradeoff is accuracy, and this one is sneakier. Rewriting history can genuinely confuse a model. Imagine a session where you first ask a model to brainstorm project names without any tools attached, and then later add a title-generating tool and ask for more ideas. What happens next isn’t guaranteed. Ideally the model uses the new tool as intended. But it’s just as plausible that the model notices it previously generated titles without any tool at all, and concludes it should keep doing exactly that, ignoring the new capability entirely. That’s not a bug exactly, it’s a reasonable inference for the model to make given a transcript that now looks inconsistent. It’s just not what you wanted. The deeper you get into nuanced transcript engineering like this, the more you need a systematic way to measure whether your changes are actually helping. Building eval sets and quantifying the real effect of your context strategies isn’t optional at this level of complexity, it’s the only way to know if a clever optimization is actually working or quietly making things worse. Step back and the shape of the whole idea becomes clear. Dynamic Profiles give you a structured way to steer model behavior and manage a session’s transcript at the same time. Patterns like baton-pass and phone-a-friend give you two distinct, well-understood ways to orchestrate multiple agents working together. Tool calling modes give you fine-grained control over when the model is even allowed to reach for a tool. Manual transcript management, and an honest understanding of what key-value caches cost you when you rewrite history, rounds out the picture. None of this is about complexity for its own sake. It’s about giving you the right primitives to build something that used to require a mess of custom scaffolding, and doing it in a way that stays flexible enough to grow alongside whatever patterns come next. If there’s one instinct worth carrying forward from all of this, it’s to treat context as a resource you actively design around, not something that just accumulates in the background until it breaks. Profiles, transforms, session properties, and orchestration patterns all exist to give you that control back. Thousands of developers share what they’re building, learning, and discovering across our publications every month. One account connects you to our entire network of publications and communities. Explore more at plainenglish.io . The Shape-Shifting Session https://blog.stackademic.com/the-shape-shifting-session-c7356c7248c8 was originally published in Stackademic https://blog.stackademic.com on Medium, where people are continuing the conversation by highlighting and responding to this story.